From 9ea206d1b4a026bb5cba2d746f6329bc6f13b733 Mon Sep 17 00:00:00 2001 From: fengruixiang <474182370@qq.com> Date: Mon, 6 Jul 2026 14:09:09 +0800 Subject: [PATCH] Implement code changes to enhance functionality and improve performance --- .../justsolutionsWebV2/plan-plus-api分析.html | 540 +++++++++++------- 1 file changed, 345 insertions(+), 195 deletions(-) diff --git a/docs/justsolutionsWebV2/plan-plus-api分析.html b/docs/justsolutionsWebV2/plan-plus-api分析.html index 07ca7aa..851edda 100644 --- a/docs/justsolutionsWebV2/plan-plus-api分析.html +++ b/docs/justsolutionsWebV2/plan-plus-api分析.html @@ -106,6 +106,7 @@ ul.tight li { margin: 4px 0; } .flow { font-family:"SF Mono",Consolas,monospace; font-size:13px; background:var(--code-bg); padding:10px 14px; border-radius:8px; border:1px solid var(--border); overflow-x:auto; white-space:nowrap;} hr.soft { border:none; border-top:1px dashed var(--border); margin: 26px 0; } + .updated { font-size:12px; color:#8250df; font-weight:700; }
@@ -113,15 +114,17 @@把 public/plans-plus.html 的單頁訂閱流程,從本地 mock 切換到 AML 後端(iCON.Abp.AMLPortal)真實 API。
把 public/plans-plus.html 的單頁訂閱流程,從本地 mock 切換到 AML 後端(iCON.Abp.AMLPortal)真實 API,並配合本地 docker-compose-local-dev 環境落地(支付除外)。
plans-plus 前端共依賴 7 個業務端點(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 justsolutionsWeb 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 justsolutionsWebV2/server/routes/ 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 countries.js / industries.js 完全同一範式)。
plans-plus 前端目前依賴 3 條「只讀目錄」端點(editions / plans/catalog / countries)、2 條「交互查詢」端點(agents / tenants/lookup)、1 組單次查詢端點(single-query,新)、2 條「下單提交」端點(subscribe / payments)、以及 1 條「聯絡我們」通知端點(contact——不創建訂單,僅發送訂單摘要),共 4 種訂閱流程:new(新購)、renew(續費)、topup(加購)、single(按次查詢)。
| 前端端點 | 對應後端 | 復用程度 | 主要缺口 |
|---|---|---|---|
| 前端端點 | 對應後端 | 復用程度 | 主要缺口 / 本次更正 |
GET /editions | Order/portal/GetEditionList | 改造 | 多語言 edition 名稱(後端只有 displayName) |
GET /plans/catalog | plan/portal/GetPlanList | 改造 | 需復刻旧站 filterPlan 拆分;bestValue/note 後端無 |
GET /editions | Order/portal/GetEditionList | 改造 | 多語言 edition 名稱(後端僅 displayName);本地庫僅有 1 個 Standard |
GET /plans/catalog | plan/portal/GetPlanList | 改造 | 復刻 filterPlan;KYC 改月租、jQuota 改配套;bestValue/note/nameJP 後端無;本地庫 0 條 Plan |
GET /countries | Order/getCategoryByTypes | 已實現 | —(routes/countries.js 已可用) |
GET /agents/:code | identity/users/SearchUserByCodeAndType | 改造 | 響應 → {code,name} 轉換 |
GET /tenants/lookup | Order/portal/queryRenewableTenant | 缺口大 | 後端按租戶名搜,前端按郵箱查;currentSubscription 需重建 |
POST /subscribe | CreateOrder / TenantRenewal | 改造 | 需補 planDetailId;topup 類型無對應端點 |
POST /payments/create | QFPay 收銀台(旧站前端拼 URL) | 缺口大 | 需服務端簽名 + PaymentWebhook 回調 |
GET /agents/:code | SearchUserByCodeAndType + GetPlanList(agentUserId) | 改造 | 新增 tiers(可售方案級別)、email/phone、隱藏 agentorId |
GET /tenants/lookup | Order/portal/queryRenewableTenant | 缺口大 | 後端按租戶名搜;前端按郵箱、唯一命中(不再多租戶消歧);需重建 currentSubscription + referrer |
GET /single-query/optionsPOST /single-query | 無對應後端 | 缺口大 | 全新按次查詢流程,後端完全無端點(見 5.6) |
POST /subscribe | CreateOrder / TenantRenewal | 改造 | 補 planDetailId;BR/CI 仍必填(更正);topup/個人主體待定 |
POST /contact(聯絡我們/推薦人) | customer/CreateFeedback | 復用現有 | 不創建訂單:訂單摘要經 contact us API 發管理員;填了推薦人則發推薦人(見 5.8) |
POST /payments/create | — | 暫緩 | 本輪支付除外:前端提交後直接顯示「已提交成功」(見 5.9) |
好消息:後端 CreateOrder 已忽略前端傳入的 AdminPassword,改用 appConfig.General.TenantAdminDefaultPassword(OrderService.cs:215,236)。因此 plans-plus 去掉「管理員密碼」步驟不構成阻塞——後端會發重置密碼郵件給租戶管理員。
兩個真正的硬缺口:
-① 按郵箱查租戶:queryRenewableTenant 只支持 keyword(按 TenantName.Contains)。plans-plus 用郵箱查、且要支持「一郵箱多租戶」選擇——需新增後端端點,或在 BFF 全量拉取後按郵箱過濾(不可取)。
② topup(加購)流程:plans-plus 新增的純加購(不含基礎方案、只買 增加用戶/KYC/jQuota)旧站沒有,後端亦無直接端點。需確認映射到 TenantRenewal(僅加值項 planList)還是新增端點。
本次三個最重要的更正 / 硬缺口:
+① BR/CI 必填(舊版結論已過時,本輪已放開):CreateOrder 兩處「BR 或 CI 至少一個」校驗(OrderService.cs:154 與 :190)原本仍生效(舊版文檔稱 2026-06 已放開與現狀不符)。已改 本輪按決策已注釋這兩處必填校驗(保留唯一性校驗),並 rebuild 生效——個人主體/空 BR 已可下單。注意:此為全局改動,旧站共用 CreateOrder,其「空 BR 不再攔截」影響待業務確認(見第 9 章 Q1)。
② 單次查詢(single-query)後端零支持:全庫 grep 無任何按次查詢 / pay-per-use 概念。/single-query/options 與 /single-query 需新增後端,或本地繼續走 mock。
③ 本地種子數據缺口:本地 docker 庫 AMLPortal_Plans / AMLPortal_PlanDetails 均為 0 條,SaasEditions 僅 1 條 Standard,AMLPortal_AgentUserPlans 為 0。⇒ 直接對接會拿到空目錄,必須先補種子數據才能跑通(見第 6 章)。
仍然成立的好消息:後端 CreateOrder 已忽略前端傳入的 AdminPassword,改用 appConfig.General.TenantAdminDefaultPassword(OrderService.cs:215,236)。因此 plans-plus 去掉「管理員密碼」步驟不構成阻塞——後端會給租戶管理員發激活/重置密碼郵件。續費 TenantRenewal 亦繼承上期 AdminPassword(:2262)。
justsolutionsWebV2 採「BFF(Backend-for-Frontend)」式:瀏覽器只調用本站 /api/*,由 Node/Express 依 APP_ENV 決定走 mock 還是真實後端,前端代碼零改動即可切換環境。
public/js/api.js:api.get('/editions') 實際請求 /api/editions。所有 plans-plus 端點均走此封裝。server/config.js:mockEnabled = (APP_ENV==='test');真實環境需配 API_BASE_URL 與 OAuth 憑證(.env.*)。server/index.js:真實環境先掛 routes/*.js 業務路由,再掛 proxy 兜底;mock 環境掛 mock/routes.js。新增 plans-plus 的真實路由就照此追加 app.use(apiPrefix, createXxxRouter(config))。server/services/auth.js:OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的 customer1 訪客憑證,現改為 .env 配置。BFF 每次調後端用 createAuthConfig() 加 Authorization: Bearer。server/config.js:mockEnabled = (APP_ENV==='test');另有 plansPlusMock(PLANS_PLUS_MOCK)開關——在真實環境下仍讓 plans-plus 這組端點走 mock,其餘 /api/* 透傳後端。真實對接時把它置 false,並在 server/routes/ 補齊業務路由。server/index.js:真實環境依次掛 contact / trial-application / industries / countries 路由,再按 plansPlusMock 決定是否掛 mock,最後掛 proxy 兜底。新增 plans-plus 真實路由就照此在代理之前追加 app.use(apiPrefix, createXxxRouter(config))。server/services/auth.js:OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的門戶訪客憑證,現改為 .env 配置(AUTH_USERNAME/PASSWORD/CLIENT_ID/...)。BFF 每次調後端用 createAuthConfig() 加 Authorization: Bearer。關鍵差異(與旧站):旧站 Angular 直接從瀏覽器調 https://api-aml.iconsz.com/api/amlPortal/*,token 存 localStorage。V2 改為瀏覽器不直接接觸後端,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。
關鍵差異(與旧站):旧站 Angular 直接從瀏覽器調 /api/amlPortal/*,token 存 localStorage。V2 改為瀏覽器不直接接觸後端,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。
支付本輪除外:當前 plans-plus.js 的 submit() 已臨時改為——POST /subscribe 成功後直接顯示「已提交成功」(showSubmitted()),不再調 /payments/create、不跳收銀台(源碼注釋標明「臨時改動…還原方法」)。submitSingle() 仍保留支付鏈,但單次查詢後端未就緒。⇒ 本輪落地只需打通到 /subscribe 為止;支付見 5.9 暫緩。
以下是 plans-plus.js 實際讀寫的字段(即 BFF 必須產出/接受的契約,目前由 mock/routes.js 滿足)。真實對接時 BFF 輸出必須與此逐字段一致,否則前端渲染/計價會出錯。
以下是 plans-plus.js 實際讀寫的字段(即 BFF 必須產出/接受的契約,目前由 mock/plans-plus.js 滿足)。真實對接時 BFF 輸出必須與此逐字段一致,否則前端渲染/計價會出錯。
loadAll() 並發請求 /editions、/plans/catalog、/countries,三者均以 { success:true, data:… } 為成功標誌。
loadAll())並發請求 /editions、/plans/catalog、/countries、/single-query/options,四者均以 { success:true, data:… } 為成功標誌。
GET /editions → { success, data:{ editionList:[{id, displayName, nameCN, nameJP}],
jQSeparatedEditions:{ editionIds:[…] } } }
GET /plans/catalog→ { success, data:{ standard:[Plan], cpa:[Plan], addons:{
- user:{unitPrice,…}, kyc:{unitPrice,…},
- jquota:{ packages:[{id, jq, price, nameCN/EN/JP}] } } } }
+ user:{ unitPrice, name* },
+ kyc:{ monthlyPrice, name* }, // ← 改為「月租」
+ jquota:{ packages:[{id, jq, price, name*}] } } } }
GET /countries → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] }
+GET /single-query/options → { success, data:{ operations:[
+ {id, price, nameCN/EN/JP, descCN/EN/JP}] } } // ← 新
Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths,
price, originalPrice, qCount(-1=無限), userCountLimit,
bestValue, noteCN, noteEN, noteJP }
+ KYC 改為「設備月租」:catalog 的 addons.kyc 不再是 unitPrice,而是 monthlyPrice。前端 kycUnit() = monthlyPrice × 所選方案 periodMonths(隨方案期數自動變動,一次付清、無套餐優惠價,見 plans-plus.js kycPriceForMonths())。jQuota 亦由「按量」改為「選配套」(jquotaPackageId)。
GET /tenants/lookup?email=&name=
- → { success, match:'none'|'unique'|'multiple',
- tenant:Tenant|null, candidates:[{tenantId, tenantName}] }
-GET /agents/:code → { success, found:bool, data:{code, name}|null }
+ GET /tenants/lookup?email= // ← 只按郵箱;郵箱全局唯一 → 至多命中 1 個租戶
+ → { success, match:'none'|'unique', tenant:Tenant|null }
+ // 註:前端已不再做「一郵箱多租戶」消歧;舊契約的 match:'multiple' / candidates[] 已廢棄
+GET /agents/:code
+ → { success, found:bool, data:{ code, name, tiers:[tag2Code…], email, phone }|null }
+ // tiers:該推薦人可售的方案級別(P2G/Std/Pre/CPA),前端據此再過濾行業方案集
Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci,
+ referrer:{code,name}|null, // ← 註冊時填寫的推薦人,續費頁展示
currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price,
qCount, userCountLimit, startDate, expiryDate, usedQuota,
addons:{ users:int, kyc:bool, jquotaPackageId:string } } }
- 3.3 提交期(兩步串聯)
- submit() 先 POST /subscribe 拿 orderId,再 POST /payments/create 拿 redirectUrl 跳轉支付。
+ 3.3 提交期
+ submit()(在線下單)與 submitContact()(聯絡我們/推薦人)共用 collectPayload() 收集當前表單。本輪支付除外——submit() 只 POST /subscribe,成功即顯示「已提交成功」;submitContact() 不創建任何訂單,把訂單摘要 POST 到聯絡端點(見 5.8),成功後顯示「已收到資料,將盡快聯絡」。(現網代碼仍 POST /subscribe-offline;本次語義調整後應改走 contact us API,見 5.8「與現網代碼的差異」。)
POST /subscribe body = {
type:'new'|'renew'|'topup', edition, isCpa, startDate,
plan:Plan, planPrice, isRenewalRate,
- addons:{ users, userUnitPrice, kyc, kycUnitPrice,
+ addons:{ users, userUnitPrice,
+ kyc, kycUnitPrice, kycRentalMonths, // ← 含租賃月數
jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice },
agentCode, subtotal, total,
- // type=new 追加: company, jurisdiction, br, contact, phoneCode, phone, email, address
+ // type=new 追加: subjectType('corp'|'individual'), company, jurisdiction, br,
+ // contact, phoneCode, phone, email, address
+ // (個人主體:contact=company,br/address 恒為空)
// type=renew/topup 追加: tenantId, company, email, currentSubscription
} → { success, data:{ orderId, status, createdAt } }
-POST /payments/create body = { orderId, amount, currency:'HKD', company, email }
- → { success, data:{ paymentId, status, gateway, redirectUrl } }
+POST /contact body = { // ← 聯絡我們/推薦人:不創建訂單,僅發送摘要
+ name, email, company, // 客戶聯絡人 / 郵箱 / 公司(租戶/主體)名
+ subject:'訂閱諮詢 · {type}', // 可帶方案名
+ message: ,
+ agentUserId? // 有推薦人時附帶其後端 Guid → 後端解析郵箱、收件人改為該推薦人
+ } → { success, data:{ feedbackId } }
+
+POST /single-query body = {
+ type:'single', subjectType:'individual'|'company', subject, email,
+ operations:[{id, name, price}], total
+ } → { success, data:{ orderId, status, createdAt } }
注意:/subscribe 的 body 不含 planDetailId,也不含各加值項(增加用戶/KYC/jQuota)對應的 planId/planDetailId。而後端 CreateOrder/TenantRenewal 的 PlanList 每項都必須同時帶 PlanId+PlanDetailId(OrderService.cs:176 聯合校驗)。⇒ BFF 需在服務端依 /plans/catalog 結果反查補齊 planDetailId(見 5.6)。
注意:/subscribe 的 body 不含 planDetailId,也不含各加值項(增加用戶/KYC/jQuota)對應的 planId/planDetailId。而後端 CreateOrder/TenantRenewal 的 PlanList 每項都必須同時帶 PlanId+PlanDetailId(OrderService.cs:176,2215 聯合校驗)。⇒ BFF 需在服務端依 /plans/catalog 結果反查補齊 planDetailId(見 5.7)。
提交按鈕門檻:前端 computeValidity() 在 terms 勾選 + 各類型必填齊備前禁用「付款」與「聯絡我們」兩個按鈕。new 要求 company/jurisdiction/email(corp 另需 contact)、方案已選、P2G 方案必選 jQuota;renew 要求已解析到租戶;topup 要求已解析租戶且至少選一個加值項且未過期;single 要求 subject/email/至少一項查詢。
以下端點均在 AML_Backend/modules/iCON.Abp.AMLPortal,並已被旧站 justsolutionsWeb/PlanService 使用驗證過。前綴 /api/amlPortal/*,鑑權走 [AbpAutoAuth("Portal")](門戶訪客 token)。
以下端點均在 AML_Backend/modules/iCON.Abp.AMLPortal,並已被旧站 justsolutionsWeb/PlanService 使用驗證過。前綴 /api/amlPortal/*(agent 校驗端點在 Identity 模塊 /api/identity/*)。
| # | 方法 / 路由 | 用途 | Controller |
|---|---|---|---|
| # | 方法 / 路由 | 用途 | 位置 |
| 1 | POST /api/amlPortal/plan/portal/GetPlanList | 取方案列表(B/jQ/j/AdlU/KYC) | PlanController:37 |
| 2 | POST /api/amlPortal/Order/portal/GetEditionList | 取 edition(所屬行業)+ jQSeparatedEditions | OrderController:136 |
| 1 | POST /api/amlPortal/plan/portal/GetPlanList | 取方案列表(Tag1: B/jQ/j/AdlU/KYC) | PlanController:37 · PlanService:54 |
| 2 | POST /api/amlPortal/Order/portal/GetEditionList | 取 edition(所屬行業)+ jQSeparatedEditions | OrderController:136 · OrderService:1103 |
| 3 | POST /api/amlPortal/Order/getCategoryByTypes | 取國家列表(typeCodes:['COUNTRY']) | OrderController:184 |
| 4 | GET /api/identity/users/SearchUserByCodeAndType/{code}/true | 校驗推薦人/代理 code | Identity(非 AMLPortal) |
| 5 | POST /api/amlPortal/Order/portal/queryRenewableTenant | 查可續費租戶(按 keyword=租戶名) | OrderController:308 |
| 6 | POST /api/amlPortal/Order/portal/ExistsByOrganizationBRCI | 校驗 BR/CI 是否已存在 | OrderController:150 |
| 7 | POST /api/amlPortal/customer/portal/CheckEmailExists/{email} | 校驗管理員郵箱是否已用 | CustomerController:121 |
| 8 | POST /api/amlPortal/Order/portal/CreateOrder | 新租戶下單 | OrderController:53 |
| 9 | POST /api/amlPortal/Order/portal/TenantRenewal | 租戶續費下單 | OrderController:321 |
| 10 | POST /api/amlPortal/Order/portal/PaymentWebhook | 支付回調(QFPay 服務端調,非前端) | OrderController:163 |
| 4 | GET /api/identity/users/SearchUserByCodeAndType/{code}/true | 按 UserCode 精確查用戶(校驗推薦人/代理) | CustomIdentityUserController:306 |
| 5 | POST /api/amlPortal/Order/portal/queryRenewableTenant | 查可續費租戶(按 keyword=租戶名 Contains) | OrderController:308 · OrderService:2353 |
| 6 | POST /api/amlPortal/Order/portal/ExistsByOrganizationBRCI | 校驗 BR/CI 是否已存在 | OrderController:150 · OrderService:1122 |
| 7 | POST /api/amlPortal/customer/portal/CheckEmailExists/{email} | 校驗管理員郵箱是否已用 | CustomerController |
| 8 | POST /api/amlPortal/Order/portal/CreateOrder | 新租戶下單 | OrderController:53 · OrderService:146 |
| 9 | POST /api/amlPortal/Order/portal/TenantRenewal | 租戶續費下單 | OrderController:321 · OrderService:2197 |
| 10 | POST /api/amlPortal/Order/portal/PaymentWebhook | 支付回調(支付方服務端調,非前端) | OrderController:163 |
| 11 | POST /api/amlPortal/customer/CreateFeedback | 聯絡我們/推薦人:接收訂單摘要(不下單);擬加 AssignedAgentUserId 收件人路由(見 5.8) | CustomerController · routes/contact.js 已封裝 |
| — | 缺 單次查詢(options + 下單) | 後端無對應端點(見 5.6) | — |
DTO 位置:Application.Contracts/PlanAppLayer/*(GetPlanListParam、PlanDto、PlanDetailDto)、Application.Contracts/OrderAppLayer/*(CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto)。
DTO 位置:Application.Contracts/PlanAppLayer/*(GetPlanListParam、PlanDto、PlanDetailDto)、Application.Contracts/OrderAppLayer/*(CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto)。agent 返回 DTO:iCON.Abp.FX.Users/AppUserDto。
後端返回 { code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }。前端要 editionList:[{id, displayName, nameCN, nameJP}] 與 jQSeparatedEditions.editionIds。
後端返回 { code:0, data:{ editionList:[完整 Edition 實體], jQSeparatedEditions:{ EditionIds:[…], EditionNames:[…] } } }(OrderService.cs:1103 直接把 SaasEdition 實體與 _appConfig.Portal.jQSeparatedEditions 原樣返回)。前端要 editionList:[{id, displayName, nameCN, nameJP}] 與 jQSeparatedEditions.editionIds。
| 前端字段 | 後端來源 | 說明 |
|---|---|---|
id | editionList[].id(Guid) | 直接映射;後續作 OrganizationReference |
displayName | editionList[].displayName | 英文名 |
displayName | editionList[].displayName | 英文名(Saas Edition 的 DisplayName) |
nameCN / nameJP | 後端無 | 缺口,見下 |
jQSeparatedEditions.editionIds | 同名字段 | 驅動 CPA/標準方案集切換(state.isCpa) |
jQSeparatedEditions.editionIds | jQSeparatedEditions.EditionIds | 驅動 CPA/標準方案集切換(state.isCpa);注意後端是 PascalCase,BFF 需轉小寫鍵名並 toLowerCase() 值 |
缺口:edition 多語言名稱。後端 GetEditionList 只有 displayName,旧站也僅用 displayName。plans-plus 的 editionName() 在 tc/jp 下會優先取 nameCN/nameJP,缺失時已能 fallback 回 displayName ⇒ 不阻塞,但中日文會顯示英文。
建議:BFF 維護一份 editionId/displayName → {nameCN,nameJP} 映射表(與 industries.js 現有靜態行業翻譯同思路),或後端在 edition 上補多語言字段。
缺口:edition 多語言名稱。後端 GetEditionList 只有 displayName。plans-plus 的 editionName() 在 tc/jp 下優先取 nameCN/nameJP,缺失時 fallback 回 displayName ⇒ 不阻塞,但中日文會顯示英文。建議:BFF 維護 editionId/displayName → {nameCN,nameJP} 映射表(與 industries.js 現有靜態行業翻譯同思路)。
旧站行為(可選沿用):getEditionList() 會過濾掉 Standard、並把 Others 排到列表末尾,同時把 editionIds toLowerCase()。plans-plus 目前未做此整理;若要與旧站一致,這段邏輯應放 BFF。
旧站行為(可選沿用):getEditionList() 會過濾掉 Standard、把 Others 排到末尾,並把 editionIds toLowerCase()。plans-plus 目前未做此整理;若要一致,這段邏輯應放 BFF。
本地現狀:本地庫 SaasEditions 只有 1 條 Standard(Id 3A220FC7-9577-660B-729A-4024B1E4BEAA),而 appsettings.local.json 的 Portal.jQSeparatedEditions.EditionIds/EditionOUMappings 引用的是另一組 GUID(3A1A2969-…/3A0149C4-…),與實庫不符。⇒ 若要在本地看到多行業 + CPA 切換,需先補 Edition 種子並對齊配置(見第 6 章)。
BFF 以旧站同款請求體調 GetPlanList,再復刻旧站 filterPlan() 把扁平 items 按 tag1Code 拆成 5 組,組裝成 {standard, cpa, addons}。
{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false,
- tag1List:['B','jQ','j','AdlU','KYC'], tag2List, tag3List, agentUserId }
+ tag1List:['B','jQ','j','AdlU','KYC'], tag2List:[], tag3List:[], agentUserId:null }
+ // 後端 GetPlanList 對 Tag2List/Tag3List:命中或 Tag2Code 為空皆放行(OrderService/PlanService:78-80)
+ 後端返回 { code:0, data:{ totalCount, items:[PlanDto{…, planDetails:[PlanDetailDto]}] } }(PlanService.cs:107 PagedResultDto<PlanDto>)。注意:getAllItems:false 時後端已過濾 Enabled 且僅保留在有效期內的 planDetails(PlanService.cs:99-104)。
| 前端 Plan | 後端來源(item / planDetails[0]) | 備註 |
|---|---|---|
planId | item.id | |
planDetailId ⚠️ | item.planDetails[0].id | 前端契約現缺此字段,但下單必需 ⇒ BFF 須補進 Plan,提交時回填(見 5.6) |
tag2Code | item.tag2Code | Std/P2G/CPA/Pre…,CPA 判定用 |
nameCN/nameEN | item.nameCN/nameEN | 旧站會截掉「(…」後綴,可沿用 |
nameJP | 後端無 | 缺口,fallback EN |
planDetailId ⚠️ | item.planDetails[0].id | 前端契約現缺此字段,但下單必需 ⇒ BFF 須補進 Plan,提交時回填(見 5.7) |
tag2Code | item.tag2Code | Std/P2G/CPA/Pre…,CPA 判定 + agent tiers 過濾用 |
nameCN/nameEN | item.nameCN/item.nameEN | 旧站會截掉「(…」後綴,可沿用 |
nameJP | 後端無(PlanDto 只有 CN/EN) | 缺口,fallback EN |
periodMonths | planDetails[0].periodMonths | |
price/originalPrice | planDetails[0].price/originalPrice | 劃線價用 originalPrice |
qCount | planDetails[0].qCount | -1 表無限 |
| 前端 | 後端用戶字段 | |
|---|---|---|
| 前端 | 後端來源(AppUserDto) | 備註 |
data.code | 用戶 code(agent code) | |
data.name | name / surname / userName | |
(下單用)agentorId | 用戶 id(Guid)→ 提交時作 CreateOrderParam.AgentorId | |
data.code | ExtraProperties.UserCode | agent code(EF 屬性 UserCode) |
data.name | Name(+Surname) / UserName | 拼接展示名 |
data.email | Email | 「聯絡推薦人」用 |
data.phone | PhoneNumber | 「聯絡推薦人」用 |
(下單用)agentorId | Id(Guid) | 提交時作 CreateOrderParam.AgentorId;建議 BFF 在 data 內附帶或服務端緩存 code→id |
data.tiers ⚠️ | 後端無直接字段 | 見下「tiers 推導」 |
關鍵:前端只展示 name,但下單需要 agent 的 Guid id(CreateOrderParam.AgentorId)。BFF 應在 /agents/:code 響應內順帶緩存 code→id(或前端 data 內含 id),否則 /subscribe 階段需再查一次。建議 data 增隱藏字段 agentorId。
備註:AMLPortal 另有 Agentor/portal/GetAgentorByCode,但已標【廢棄】;旧站用的是 Identity 的 SearchUserByCodeAndType,沿用之。
tiers 推導(本次新增):plans-plus 用 state.agent.tiers(tag2Code 列表)在「所屬行業選定的方案集」上再過濾——只顯示該代理可售級別。後端 agent 的可售方案由 AMLPortal_AgentUserPlans 建模,並由 GetPlanList 的 agentUserId 過濾(PlanService.cs:87-92)。⇒ BFF 兩種實現:
+ ① 推導 tiers:再調一次 GetPlanList{ agentUserId: agent.Id, tag1List:['B'] },取 distinct tag2Code 作 tiers(保持前端過濾邏輯);
+ ② 或改為服務端過濾:/plans/catalog 帶上 agentUserId 直接返回該代理可售方案(則前端 tiers 過濾成空操作)。
+ 本地缺 AgentUserPlan 數據,短期可讓 BFF 在 tiers 缺失時返回空數組 = 不過濾(顯示該行業全部方案),與前端 computePlanSet() 對「tiers 為空不過濾」的處理一致。
備註:AMLPortal 另有 Agentor/portal/GetAgentorByCode/GetAgentorByName,旧站與 plans-plus 均用 Identity 的 SearchUserByCodeAndType,沿用之。
前端按郵箱查租戶,支持「none / unique / multiple」三態,命中後帶出 currentSubscription 預填續費表單。但後端 queryRenewableTenant 只接受 {keyword} 且按 TenantName.Contains(keyword) 搜索(OrderService.cs:2363),返回 List<TenantPropertyDto>(內部已把 admin 郵箱填入 TenantAdminEmail)。
前端按郵箱查租戶,返回 match:'none'|'unique' 兩態(不再多租戶消歧——註釋明確「租戶名與郵箱均全局唯一 → 一郵箱至多 1 個租戶」)。命中後帶出 currentSubscription + referrer 預填續費/加購表單。
但後端 queryRenewableTenant 只接受 {keyword} 且按 TenantName.Contains(keyword) 搜索(OrderService.cs:2362),返回 List<TenantPropertyDto>——TenantAdminEmail 是查完後再逐條用 admin 用戶郵箱回填(:2375-2379),不能在 DB 層按郵箱過濾。
TenantAdminEmail===email 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。TenantPropertyDto 結構)。currentSubscription(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 TenantPropertyDto 直接字段,需由 TenantProperty + 其 Order + 正在生效的 plan 推導。可參考 GetCurrServiceInfo(CurrServiceInfoDto.ActiveBPlans、EffectiveEndTime、QCountLeft 等)。keyword 拉全部可續費租戶,在服務端按 TenantAdminEmail===email 過濾(⚠️ 全量拉取、性能與越權風險,僅臨時)。正解:後端新增「按管理員郵箱查可續費租戶」端點。currentSubscription(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 TenantPropertyDto 直接字段,需由 TenantProperty + 其 Order/OrderDetail 推導。可參考 GetCurrServiceInfo(OrderService.cs:398,CurrServiceInfoDto)。referrer 新增:前端續費頁展示「註冊時填寫的推薦人」。TenantPropertyDto 已有 AgentorCode/AgentorName ⇒ 映射為 referrer:{code:AgentorCode, name:AgentorName}。tenantId | TargetTenantID | |
tenantName | TenantName | |
editionId/editionName | EditionId/EditionName | |
referrer | {AgentorCode, AgentorName} | 新增映射 |
currentSubscription.expiryDate | EffectiveEndTime | 過期判定(前端 daysUntil) |
currentSubscription.startDate | EffectiveStartTime | 未過期續費 → 默認接續此日 |
currentSubscription.qCount/usedQuota | QCountPurchase / QCountPurchase−QCountLeft | 已用 = 購買 − 剩餘 |
currentSubscription.userCountLimit | UserCountLimit | |
currentSubscription.addons.kyc | EnableKYC | topup 時鎖「已購買」 |
currentSubscription.planId/name*/price | 需經 Order/OrderDetail 反查 | 推導,續費續價(沿用上期 price)依賴此 |
currentSubscription.addons.users/jquotaPackageId | 需經訂單明細反查 | 推導 |
currentSubscription.planId/name*/price/periodMonths | 經 Order/OrderDetail 反查 | 推導,續費續價(沿用上期 price)依賴此 |
currentSubscription.addons.users/jquotaPackageId | 經訂單明細反查 | 推導 |
建議後端改動:新增端點 queryRenewableTenantByEmail(email[, tenantName]),直接返回 plans-plus 所需的「租戶 + currentSubscription(含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查。
建議後端改動:新增 queryRenewableTenantByEmail(email),直接返回 plans-plus 所需的「租戶 + currentSubscription(含 planId/上期價/已購加值項)+ referrer」聚合結構,避免在 BFF 拼裝易錯的訂單反查。
plans-plus 新增第 4 種流程 type='single':無需訂閱、填「結果接收郵箱 + 查詢主體 + 勾選查詢項目」→ 付款 → 結果郵件發送。前端契約見 3.1/3.3。全庫 grep 無任何按次查詢 / pay-per-use 端點或實體。
GET /single-query/options:返回查詢項目目錄 operations:[{id, price, name*, desc*}](制裁篩查、PEP、負面新聞、公司查冊、身份核實…)。POST /single-query:建按次查詢訂單,返回 orderId,本擬走與訂閱相同的支付鏈(本輪支付除外)。落地選項:① 短期保持 mock(PLANS_PLUS_MOCK=true 或單獨掛 mock/plans-plus.js 的這兩條),其餘端點走真實後端;② 後端新增「按次查詢」下單 + 計費 + 結果投遞(涉及檢測引擎 iCS,工作量大)。本輪建議選 ①:single-query 端點繼續 mock,優先打通訂閱三態(new/renew/topup)的真實對接。
BFF 依 body.type 分流到不同後端端點,並把前端的「人類友好」payload 翻成後端 DTO;核心是組裝 PlanList(基礎方案 + 各加值項,每項補 planDetailId)。
BFF 依 body.type 分流;核心是組裝 PlanList(基礎方案 + 各加值項,每項補 planDetailId)。
| CreateOrderParam | 前端 payload | 備註 |
|---|---|---|
PlanList[] | plan + addons | 見下「PlanList 組裝」 |
TenantName | company | 必填;後端校驗重名 |
TenantAdminEmail | email | 必填;後端再校驗未註冊 |
Jurisdiction | jurisdiction | 必填 |
OrganizationReference | edition(Guid) | 必填;校驗 edition 存在 |
OrganizationBR / OrganizationCI | br / — | 已改 後端已去除「BR 或 CI 至少一個」必填校驗(OrderService.cs:154,190 注釋),可不填;唯一性校驗保留,僅在實際填寫時生效 |
TenantName | company | 必填;後端校驗重名(:183) |
TenantAdminEmail | email | 必填;後端 CheckEmailExists 校驗未註冊(:159) |
Jurisdiction | jurisdiction | 必填(:153) |
OrganizationReference | edition(Guid) | 必填;校驗 edition 存在(:200) |
OrganizationBR / OrganizationCI | br / —(前端不收 CI) | 阻塞 後端仍要求 BR 或 CI 至少一個(見下) |
EffectiveStartTime | startDate | |
ContactPerson | contact | |
CompanyAddress | address | 可選 |
ContactPerson | contact | 個人主體 = company |
CompanyAddress | address | 可選(個人主體為空) |
Phone | phoneCode + phone | 可選;BFF 拼接 |
AgentorId | 由 agentCode 解析的 Guid | 見 5.4 |
AdminPassword | — | 忽略(後端用默認密碼,發重置郵件) |
IsOfflinePayment/IsAgentBehalf | — | 固定 false |
AgentorId | 由 agentCode 解析的 Guid | 見 5.4;空則後端指派頂級 salesAdmin(:283-287) |
AdminPassword | — | 忽略(後端用默認密碼,發激活郵件) |
IsOfflinePayment/IsAgentBehalf | — | 在線下單固定 false |
BR/CI 已放開(2026-06):後端 CreateOrder 原有兩處「BR 或 CI 至少一個」必填校驗(OrderService.cs:154 與 :190)已注釋去除,plans-plus 可不填 BR/CI 直接下單。保留緊隨其後的唯一性校驗 ExistsByOrganizationBRCI(OrderService.cs:191)——兩者皆空時返回 false 放行,填寫時仍攔重複。DB 列可空(nvarchar(max)),無需遷移。
影響:① 改動為全局,旧站共用 CreateOrder 同樣放開(旧站前端仍自填 BR,行為不變);② 空 BR 不再參與去重,原「同公司只能註冊一次」對空 BR 失效;③ 下游 TenantInfo(OrderService.cs:791)、偵測報告(ReportService.cs:276)僅透傳展示、可容忍空值,但合規/STR 報告該欄會留白,需業務確認可接受。續費/加購走 TenantRenewal,BR 繼承自上期訂單,不受影響。
BR/CI 阻塞(本次更正):CreateOrder 兩處校驗 當前源碼仍生效:OrderService.cs:154 與 :190——if (BR 空 && CI 空) return "OrganizationBR or OrganizationCI is required"。緊隨的唯一性校驗 ExistsByOrganizationBRCI(:191)兩者皆空時返回 false,但走不到那步就先被必填校驗擋下。⇒ plans-plus:
+ · corp 主體:BR 標「可選」,但空 BR 會下單失敗;
+ · individual 主體:br 恒為空、無 CI ⇒ 必然失敗。
+ 三種對策(擇一):(a) 後端真正去除該必填校驗(若業務允許空 BR/CI);(b) 前端把 corp 的 BR 改為必填、且暫不放行 individual 新購;(c) BFF 為空 BR/CI 注入占位值(不推薦,污染合規欄)。需業務決策。
TargetTenantIDtenantIdPlanList[]TenantAdminEmailemailAgentorIdTenantAdminEmailemail(後端校驗與租戶 admin 郵箱一致 :2239)AgentorId:2321-2323)IsOfflinePayment/IsAgentBehalffalse續費續價:後端按 planDetail 的 Price 計;前端的「續費續價(沿用上期 price)」屬展示層優惠,後端不認前端傳入的 price。若要真正續價,需後端支持自定義價 / 專屬續費 PlanDetail——本輪按目錄價下單,前端續費折扣僅為展示(待業務確認)。
純加購(無基礎方案,只買 增加用戶/KYC/jQuota)。後端無直接端點。候選方案:
-TenantRenewal,PlanList 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。TenantTopup 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC,不延長有效期」。UpdateTenantProperty(admin)邏輯可借鑒(它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。純加購(無基礎方案,只買 增加用戶/KYC/jQuota)。後端無專屬端點。候選 A(推薦):復用 TenantRenewal,PlanList 只含加值項(不含 B 類)。源碼上 TenantRenewal 只對 Tag1Code==='B' 的行設服務起止期(:2300-2314),非 B 行只疊加配額/用戶 ⇒ 理論上「只疊配額不延期」語義成立,但需確認 ProcessTenantEventQueue 下游對「無 B 行的續費單」處理正確。候選 B:後端新增 TenantTopup(可借鑒 admin 的 UpdateTenantProperty :2389,它就是疊 PlanList 重算配額)。
PlanList = []
@@ -426,9 +496,9 @@ if (type!=='topup') PlanList.push({ PlanId: plan.planId,
// 增加用戶:PCS = addons.users
if (addons.users>0) PlanList.push({ PlanId: AdlU.planId,
PlanDetailId: AdlU.planDetailId, PCS: addons.users })
-// KYC:PCS = 1
+// KYC(設備月租):PCS = kycRentalMonths(把 KYC planDetail.price 當月租單價;見 5.2)
if (addons.kyc) PlanList.push({ PlanId: KYC.planId,
- PlanDetailId: KYC.planDetailId, PCS: 1 })
+ PlanDetailId: KYC.planDetailId, PCS: addons.kycRentalMonths })
// jQuota:選中的配套 ×1
if (addons.jquotaPackageId)
PlanList.push({ PlanId: jqPack.planId,
@@ -437,112 +507,192 @@ if (addons.jquotaPackageId)
planDetailId 從哪來:前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 /plans/catalog 的服務端版本(含 planDetailId),按 plan.planId 與加值項類型反查補齊。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),/subscribe 時直接查表。
- 後端返回(OperationDto):下單成功返回含 orderCode、planName、paymentStatus、txamt 等(旧站 plan4.component 用其拼 QFPay URL)。BFF 應把其中 orderId/orderCode 回給前端的 data.orderId,並暫存 orderCode/amount 供下一步支付用。
+ 後端返回(OrderDto):下單成功返回 OperationDto.Success(OrderDto),含 orderCode、goodsName、orderStatus、planPrice 等。BFF 應把 orderId/orderCode 回給前端的 data.orderId。免費/0 元訂單後端直接進 PendingActive 並發激活郵件(:296-303);在線非 0 元走 MockupPayment(本地佔位,見 5.9)。
-
- 5.7 POST /payments/create(支付) 缺口大
- POST /api/payments/create → QFPay 收銀台(服務端拼簽名 URL)
- 旧站沒有獨立的 create-payment 後端端點:plan4.component.payFn() 在前端直接用 orderCode/planName/txamt 拼 QFPay checkstand URL 並 sha256 簽名後 location.href 跳轉。plans-plus 改成調 /payments/create 拿 redirectUrl ⇒ 這段簽名邏輯應移到 BFF 服務端(API key 不可暴露前端)。
- BFF 實現要點(對標旧站 QFPay 參數)
+
+ 5.8 聯絡我們/推薦人(發送訂單摘要 · 不創建訂單) 復用現有 contact API
+ POST /api/contact → POST /api/amlPortal/customer/CreateFeedback
+ 訂單摘要下方的「聯絡我們 / 聯絡推薦人」按鈕與付款按鈕同樣的有效性門檻(由 updateSubmitEnabled() 控制),但不創建任何訂單/租戶/線索——只是把用戶當前已填寫的訂單摘要(collectPayload() 產出的 type/行業/方案/加值項/期間/總價與聯絡資料)整理成一段留言,經既有 contact us API 發出,讓管理員(或推薦人)主動跟進。成功後前端顯示「已收到資料,將盡快聯絡」(showOfflineSubmitted())。
+
+ 好消息——復用已實現端點:站內已有 server/routes/contact.js(POST /api/contact → 後端 customer/CreateFeedback,免 token,見 contact.js:40,143),plans-plus 的「聯絡我們」直接復用它即可,無需新增後端 lead 表/端點。BFF 只需接受一段摘要 message(+ 收件人路由參數)並轉為 CreateFeedback。
+
+ 收件人路由:管理員 vs 推薦人
+
+ - 未填推薦人(或
type≠'new'):發給平台管理員——即現有 CreateFeedback 的既定收件邏輯(後端反饋 + ADMIN_EMAIL 通知,contact.js:17)。
+ - 已填有效推薦人代碼(
state.agent 已解析;前端 hasReferrer() 僅在 new 流程為真):改發給該推薦人。推薦人 email/phone 已由 /agents/:code 帶出(見 5.4),前端可隨摘要一併提交。
+
- QFPay 參數 來源
+ contact 端點入參 plans-plus 來源 對應 CreateFeedback
- appcode環境配置(.env)
- goods_name下單返回 planName / 前端 planName
- out_trade_no下單返回 orderCode
- txamtapis.test ? 10 : amount*100(單位:分)
- txcurrcdHKD
- return_url/failed_urlV2 的 pay-return / 失敗頁
- notify_url{API_BASE_URL}/api/amlPortal/Order/portal/PaymentWebhook(端點 10,QFPay 服務端回調後端)
- signsha256(排序參數串 + api_key),服務端計算
+ companypayload.company(新購=公司/個人名;續費/加購=租戶名;單次=查詢主體)companyName
+ namepayload.contact / 公司名 / 主體名customerName
+ emailpayload.email(客戶郵箱)customerEmail(供回覆)
+ subject固定文案「訂閱諮詢 · {type}」(可帶方案名) typeOfQuery
+ message 未組裝訂單摘要文本:type/行業/方案+價/加值項(用戶·KYC月租·jQuota)/期間/小計/總價/推薦人碼 —— 現網尚未產出此文本(見下方缺口) Message(需把 collectPayload() 序列化為可讀文字;後端限 ≤1000 字)
+ agentUserId 擬新增state.agent 的後端 Guid(/agents/:code 帶出,見 5.4)→ 擬新增的 CreateFeedback.AssignedAgentUserId:收件人路由,後端據此解析推薦人郵箱(見下)
- BFF 返回 { success, data:{ redirectUrl:<QFPay checkstand URL> } },前端 location.href 跳轉。支付結果由 QFPay 異步回調後端 PaymentWebhook 落單(權威來源);前端返回頁可輪詢訂單狀態。
-
- mock 對照:當前 mock 用 /payments/create + /payments/:pid(輪詢)+ /payments/:pid/webhook 模擬「下單→收銀台→webhook→返回頁輪詢」全鏈路。真實環境收銀台與 webhook 由 QFPay 提供,BFF 只需:① 拼簽名 URL;② 提供查單(透傳後端訂單狀態)給返回頁輪詢。
+
+ 關鍵缺口——摘要 message 目前「未組裝」:現網 submitContact()(plans-plus.js:1243)送的是 collectPayload() 的結構化對象(type/edition/plan/addons/total…)+ channel + referrer,並無任何 message 文本;renderSummary() 只把摘要畫進右側 DOM 面板(#ppSum* 的 textContent),不產出可提交字符串。⇒ 要把「訂單摘要」當作 contact us 的 message,必須新增一步序列化:
+ · 推薦:BFF 端拼裝——前端照舊送結構化字段,/api/contact 路由把 type/行業/方案+價/加值項/期間/小計/總價/推薦人碼格式化為可讀多行文本填入 message(順帶多語言與脫敏);
+ · 或前端拼裝(複用 renderSummary() 的同源計算:currentPlan()/computeSubtotal()/各加值項)。
+ 長度上限:後端 CreateFeedback 限 Message ≤ 1000 字(CustomerService.cs:507),BFF contact.js 亦要求 message 非空 ⇒ 序列化摘要需精簡到 1000 字內。
- 免支付分支:旧站當訂單 paymentStatus===200 或 txamt===0(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(redirectUrl 直接給成功頁)。
+ 與現網代碼的差異(需前端小改):當前 plans-plus.js 的 submitContact() 仍 POST /subscribe-offline(帶 channel:'offline' + referrer、且無 message),語義是「線下對接線索」。按本次決策應改為:組裝訂單摘要文本 + POST /api/contact(帶 message 摘要 + agentUserId 路由),不再創建 lead/訂單。BFF 側可保留 /subscribe-offline 作向後兼容別名(轉調同一 contact 處理),或直接改前端調用點與 mock。
+
+ 「送推薦人」路由 —— 已定方案②(後端加收件人字段),待實現:方案細節(供實作參照,尚未落碼):
+ ① CreateFeedbackDto 與 Feedback 實體各加可空 AssignedAgentUserId(Guid?,CreateMap<CreateFeedbackDto,Feedback> 同名自動映射,無需改 profile);
+ ② CustomerService.CreateFeedback(CustomerService.cs:496)於其有值時,用 ICustomIdentityUserRepository.GetUsersByIDs 解析該推薦人郵箱,作為 SendEmailOnPortalFeedback 第 6 參數 salesEmail 的收件人(推薦人無郵箱時回退平台銷售 _appConfig.Portal.SalesEmailAddress);「給客戶本人」的確認郵件不變;
+ ③ EF 遷移為 AMLPortal_Feedbacks 增一列 uniqueidentifier NULL(SQL Server 遷移工程;MySQL 遷移工程停在 2021 Initial、未並行維護,如啟用該 provider 需補同名列)。
+ BFF 對接:/api/contact 把 agentUserId(/agents/:code 帶出的後端 Guid,見 5.4)透傳為 AssignedAgentUserId,推薦人郵箱由後端解析、前端/BFF 不必自行取。
+ 為何不走「BFF 抄送、零改後端」(原備選①已否決):曾設想 BFF 拿 agentEmail 自行把摘要抄送推薦人、不動後端。核對後不成立:① contact API 的 CreateFeedbackDto 無 cc/bcc 字段;② 底層 CreateEmailQueue(subject, to, cc, bcc, …) 雖有 cc/bcc(EmailMessageService.cs:44),但 SendEmailOnPortalFeedback 調用時均傳 null,且該方法對外調不到,唯一可發任意郵件的 EmailQueueController.CreateEmailQueue 端點已被注釋(EmailQueueController.cs:51);③ V2 BFF 自身無發信能力(無 SMTP/nodemailer,只轉調後端)。⇒「零改後端」需給 BFF 加 SMTP 或解開後端發信端點(皆非零改動),故以方案②(加 AssignedAgentUserId)為準。
+
+
+
+ 5.9 POST /payments/create(支付) 本輪除外
+ POST /api/payments/create → 暫緩
+ 當前 plans-plus.js 的 submit() 已臨時移除支付鏈:/subscribe 成功後直接 showSubmitted() 顯示「已提交成功」,不調 /payments/create、不跳收銀台。⇒ 本輪不實現支付端點。
+ 後端側:在線非 0 元訂單 CreateOrder/TenantRenewal 目前走 MockupPayment(newOrder)(本地佔位、非真實支付方),配合 PaymentWebhook(端點 10)。真實支付(QFPay 收銀台簽名 URL + webhook)留待後續階段;屆時 QFPay 簽名邏輯應在 BFF 服務端完成(API key 不可暴露前端),mock 的 pay-gateway/pay-return//payments/:id 輪詢鏈可作參照。
- 6. plans-plus 相對旧站的新增需求
- 以下是 plans-plus 單頁流程相對旧站 4 步嚮導新增或改變的點,逐一標注對接影響。
+ 6. 本地開發環境對接(docker-compose-local-dev)
+ 本地棧(AML_Backend/docker-compose-local-dev/)已把後端 iCON.Abp.FX.HttpApi.Host 起在 http://localhost:44331(Swagger 同址),SQL Server 在 localhost,11433。要把 V2 的 plans-plus BFF 指到它,需三件事:配 .env、對齊 Portal 訪客憑證、補種子數據。
+
+ 6.1 V2 側配置(把 BFF 指向本地後端)
+ # 便捷腳本(已加入 package.json):APP_ENV=dev + 本地後端 + plans-plus mock 兜底 + PORT 8090
+npm run start:local
+# 等價於:
+cross-env APP_ENV=dev API_BASE_URL=http://localhost:44331 PLANS_PLUS_MOCK=true PORT=8090 node server/index.js
+
+ 實測結論(免 token):plans-plus 依賴的門戶端點 GetPlanList / GetEditionList / getCategoryByTypes / CreateOrder / TenantRenewal / queryRenewableTenant 均為 [AbpAutoAuth("Portal")]——後端 AbpAutoAuthMiddleware 在服務端注入門戶訪客 token 並覆蓋調用方 Authorization;SearchUserByCodeAndType 為 [AllowAnonymous]。⇒ BFF 無需自帶 token,直接匿名 POST 即可。因此本地不必配 AUTH_*(且本地 Portal 租戶未種子化,connect/token?__tenant=Portal 反而會報 Tenant not found)。新增的 5 個真實路由與已改的 countries.js 均按此免 token 實現。
+
+
+ 6.2 種子數據缺口(對接前必補)
+ 本地庫當前(實測):
- # 新增/變更 對接影響 狀態
+ 表 本地現狀 plans-plus 需要
- 1 topup 加購類型(無基礎方案,只買加值項) 後端無端點,需確認映射 TenantRenewal 或新增端點 需後端
- 2 按郵箱查租戶 + 一郵箱多租戶選擇 後端 queryRenewableTenant 按名字搜,需新增按郵箱端點 需後端
- 3 jQuota 改為選配套(package,非按量) BFF 把 jQ plan 映成 packages 即可 BFF
- 4 KYC「已購買」鎖定(topup 已有則禁買) 依賴租戶 EnableKYC,BFF 帶出即可 BFF
- 5 續費續價(沿用上期 price,顯示續費折扣) 依賴 currentSubscription.price,需訂單反查 需後端
- 6 去掉管理員密碼步驟 後端已忽略 AdminPassword,發重置郵件 無影響
- 7 bestValue / 賣點 note(方案卡片標記與文案) 後端 Plan 無此字段,BFF 配置或後端補 BFF/後端
- 8 edition 多語言名稱(中/日) 後端只有 displayName,BFF 補映射 BFF
- 9 BR 標為可選(UI) 後端已去除 BR/CI 必填校驗(OrderService.cs:154,190),可不填即下單;唯一性校驗保留、僅在填寫時生效 已改
- 10 支付 create 端點化(前端拿 redirectUrl) QFPay 簽名邏輯移到 BFF 服務端 BFF
+ AMLPortal_Plans0 條 B(Std/P2G/Pre/CPA) + jQ/j + AdlU + KYC 各若干
+ AMLPortal_PlanDetails0 條 每 Plan 至少 1 條(含 price/periodMonths/qCount/userCountLimit)
+ SaasEditions1 條 Standard(3A220FC7-…) 多行業(VASP/TCSP/MSO/CPA…);至少 1 個 CPA edition 對齊 jQSeparatedEditions
+ AMLPortal_AgentUserPlans0 條 (可選)測 agent tiers 過濾時需要
+ 配置 Portal.jQSeparatedEditions.EditionIds 3A1A2969-…(庫中不存在)對齊到實際 CPA edition 的 GUID
+
+ 補種子的兩條路:① 直接寫 SQL 往 SaasEditions/AMLPortal_Plans/AMLPortal_PlanDetails 插測試數據(快,適合本地聯調;注意 AMLPortal_Plans 需正確的 Tag1Code/Tag2Code/Enabled=1/IsActive=1,並讓 PlanDetail.EffectTime/ExpireTime 覆蓋當前);② 加後端 DataSeedContributor(AMLPortalDataSeedContributor 目前只 seed TenantProperty)補 Plan/Edition 種子,跑 db-migrator 幂等注入(更可復現,改動後端代碼)。本地聯調建議 ①,並把 CPA edition 的 GUID 同步進 appsettings.local.json 後重啟 host。
+
+
+ 6.3 本輪落地狀態(已實現 · 2026-07 實測通過)
+
+ 項 狀態 說明
+
+ Edition + Plan/PlanDetail 種子 已補 docker-compose-local-dev/seed-plans-plus.sql(幂等,鏡像 mock 目錄;CPA edition GUID 對齊 jQSeparatedEditions,down -v 後重跑)
+ 後端 BR/CI 必填放開 已改 OrderService.cs:154,190 兩處必填校驗已注釋;唯一性校驗保留(rebuild 已生效)
+ BFF 真實路由 已寫 新增 routes/editions.js · plans.js · agents.js · tenants.js · subscribe.js + services/catalog.js;countries.js 改為免 token;index.js 掛載於 mock/代理之前
+ 訂閱三態 new 已驗 renew/topup 待數據 new(corp/個人/CPA/加值項)實測返回 orderId、PlanPrice 正確(含 KYC 月租 PCS=月數);renew/topup 路由已寫,但本地無可續費租戶數據,待後端「按郵箱查」或先建租戶
+ single-query / payments mock 按決策暫留 mock(PLANS_PLUS_MOCK=true 兜底);支付本輪除外
+ 聯絡我們(不下單) 改接 contact 復用 routes/contact.js→CreateFeedback;前端調用點由 /subscribe-offline 改 /contact,推薦人路由由 BFF 補
+
+
+ 端到端驗證(npm run start:local + 本地 docker 後端):/editions(10 行業 + CPA 切換)· /plans/catalog(standard/cpa/addons 全對)· /countries(249)· /agents/:code(無數據 found:false)· /subscribe(new 各變體均返回 orderId、訂單入庫)。頁面 GET /plans-plus HTTP 200。
- 7. 實施建議與分期
- 7.1 新增 / 改動文件清單(justsolutionsWebV2/server)
+ 7. plans-plus 相對旧站的新增需求
- 文件 職責 後端依賴
+ # 新增/變更 對接影響 狀態
- routes/editions.js(新)GET /editions + 多語言/過濾整理GetEditionList
- routes/plans.js(新)GET /plans/catalog + filterPlan 拆分 + 緩存 planDetailIdGetPlanList
- routes/countries.js已存在,直接掛載 getCategoryByTypes
- routes/agents.js(新)GET /agents/:code → {code,name,agentorId}SearchUserByCodeAndType
- routes/tenants.js(新)GET /tenants/lookup(依賴後端新端點)queryRenewableTenant(ByEmail)
- routes/subscribe.js(新)POST /subscribe 分流 + PlanList 組裝CreateOrder / TenantRenewal / (Topup)
- routes/payments.js(新)POST /payments/create 拼 QFPay 簽名 URL + 查單PaymentWebhook(回調)
- index.js在真實環境段 app.use(apiPrefix, createXxxRouter(config)) 追加各路由 —
- .env.* / config.js補 QFPay appcode/api_key、edition/plan 文案映射等 —
-
-
-
- 7.2 可能的後端改動(建議與後端團隊確認)
-
- - 按郵箱查可續費租戶端點,返回聚合 currentSubscription(含 planId / 上期 price / 已購加值項)。
- - topup 語義:明確「無基礎方案的加購」如何下單(TenantRenewal 是否允許 / 新增 Topup 端點)。
- - (可選)Plan 增
bestValue 與多語言 note;edition 增多語言名稱。
-
-
- 7.3 建議分期
-
- 階段 內容 可獨立交付
-
- P1 · 只讀目錄 editions + plans/catalog + countries(已就緒)+ agents 頁面可正常渲染方案/行業/國家,校驗推薦人
- P2 · 新購下單 subscribe(new) + payments/create(QFPay) 新租戶完整下單支付閉環
- P3 · 續費 tenants/lookup(後端新端點)+ subscribe(renew) 續費閉環,含續價
- P4 · 加購 topup(後端確認後) 加購閉環
+ 1 單次查詢 single-query(按次付費,第 4 種流程) 後端零支持,需新增或保留 mock 需後端/mock
+ 2 聯絡我們/推薦人(發訂單摘要 · 不創建訂單) 復用既有 contact/CreateFeedback;「送推薦人」需後端加 AssignedAgentUserId 收件人字段(小改)+ BFF 拼裝 message 復用+後端小改
+ 3 agent tiers(推薦人可售級別過濾方案) SearchUser 不含 tiers,需經 GetPlanList(agentUserId) 推導 BFF
+ 4 agent email/phone(聯絡推薦人) AppUserDto 已含 Email/PhoneNumber,BFF 帶出 BFF
+ 5 主體類型 corp/individual(新租戶) 個人主體無 BR/CI ⇒ 撞後端 BR/CI 必填 阻塞
+ 6 BR/CI 仍必填(更正舊版「已放開」結論) OrderService.cs:154,190 校驗仍生效需業務決策
+ 7 KYC 改設備月租(monthlyPrice × 月數) 後端 KYC 單價 ⇒ 下單 PCS=月數(見 5.2) BFF/後端確認
+ 8 jQuota 改選配套(package,非按量) 後端 jQ plan 天然離散,BFF 映 packages BFF
+ 9 按郵箱查租戶 + 郵箱唯一單命中 後端按名字搜,需按郵箱端點;不再多租戶消歧 需後端
+ 10 續費頁展示 referrer TenantPropertyDto.AgentorCode/Name 映射 BFF
+ 11 續費續價 / 顯示舊方案(Req 9) 續價後端不認前端 price;舊方案卡屬前端渲染 後端確認
+ 12 過期加購阻斷(須先續費) 純前端門檻,無對接影響 無影響
+ 13 去掉管理員密碼步驟 後端忽略 AdminPassword,發激活郵件 無影響
+ 14 bestValue / note / edition&plan 多語言 後端無字段,BFF 配置或後端補 BFF/後端
+ 15 支付端點化(本輪除外) submit 暫不跳支付,直接顯示已提交 暫緩
- 8. 待確認問題清單
+ 8. 實施建議與分期
+ 8.1 新增 / 改動文件清單(justsolutionsWebV2/server)
+
+ 文件 職責 後端依賴
+
+ routes/editions.js(新)GET /editions + 多語言/過濾整理 + jQSeparated 鍵名轉換GetEditionList
+ routes/plans.js(新)GET /plans/catalog + filterPlan 拆分 + KYC 月租/jQ 配套 + 緩存 planDetailIdGetPlanList
+ routes/countries.js已存在,直接掛載 getCategoryByTypes
+ routes/agents.js(新)GET /agents/:code → {code,name,tiers,email,phone,agentorId}SearchUserByCodeAndType (+GetPlanList)
+ routes/tenants.js(新)GET /tenants/lookup?email= + currentSubscription 重建 + referrerqueryRenewableTenant(ByEmail)
+ routes/subscribe.js(新)POST /subscribe 分流 + PlanList 組裝(含 KYC PCS=月數)CreateOrder / TenantRenewal / (Topup)
+ routes/contact.js已存在;「聯絡我們」復用之——把結構化 payload 序列化為 message 摘要 + 透傳 agentUserId,轉 CreateFeedback(收件人路由由後端 AssignedAgentUserId 承接) CreateFeedback
+ mock/plans-plus.jssingle-query / payments 暫留 mock;subscribe-offline 改由 contact 承接(可保留為兼容別名) —
+ index.js代理之前追加各真實路由 app.use(apiPrefix, createXxxRouter(config)) —
+ services/auth.js / .env.*門戶訪客憑證 + __tenant=Portal;edition/plan 文案映射 —
+
+
+
+ 8.2 可能的後端改動(與後端團隊確認)
+
+ - BR/CI 必填:是否按 plans-plus 需求去除
CreateOrder 的 BR/CI 必填校驗(或僅對個人主體放開)。
+ - 按郵箱查可續費租戶端點,返回聚合 currentSubscription(含 planId / 上期 price / 已購加值項)+ referrer。
+ - topup 語義:確認
TenantRenewal(PlanList 僅加值項)是否「只疊配額不延期」,或新增 TenantTopup。
+ - KYC 計費口徑:確認 KYC plan 是否可按
PCS=月數 計月租。
+ - single-query:是否新增後端按次查詢端點,還是短期 mock。
+ - 聯絡我們送推薦人:待實現 已定方案——後端
CreateFeedback 增 AssignedAgentUserId 收件人字段(DTO + 實體 + 服務解析郵箱 + EF 遷移):有值發推薦人、否則發平台銷售。
+ - (可選)Plan 增
bestValue/多語言 note/nameJP;edition 增多語言名稱。
+
+
+ 8.3 建議分期
+
+ 階段 內容 可獨立交付
+
+ P0 · 本地種子 補 Edition + Plan/PlanDetail 種子,對齊配置(第 6 章) 後端返回非空目錄,可對接
+ P1 · 只讀目錄 editions + plans/catalog + countries(已就緒)+ agents 頁面渲染方案/行業/國家,校驗推薦人
+ P2 · 新購下單 subscribe(new)(先解 BR/CI 阻塞) 新租戶下單至「已提交成功」(支付除外)
+ P3 · 續費/加購 tenants/lookup(後端新端點)+ subscribe(renew/topup) 續費/加購閉環(支付除外)
+ P4 · 單次查詢/聯絡/支付 single-query + 聯絡我們(接 contact API + 推薦人路由)+ payments(後端就緒後) 完整流程 + 在線支付
+
+
+
+
+
+
+ 9. 待確認問題清單
- - 租戶查詢:後端能否新增「按管理員郵箱查可續費租戶」?返回是否能直接帶 currentSubscription(planId / 上期價 / 已購加值項)?
- - topup:純加購走 TenantRenewal(PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?還是需新端點?
- - BR/CI:已解決 後端已去除「BR 或 CI 至少一個」必填校驗(
OrderService.cs:154,190 注釋,2026-06),plans-plus 可不填 BR/CI 直接下單;唯一性(去重)校驗保留,僅在填寫時生效。待業務確認:① 改動為全局,旧站共用 CreateOrder 是否接受同樣放開;② 空 BR 不再去重、合規/STR 報告該欄留白是否可接受。
- - bestValue / note / edition 多語言:由 BFF 配置維護,還是後端在數據上補字段?
- - QFPay:V2 是否沿用旧站同一
appcode/api_key/收銀台域名?簽名算法是否一致(sha256 排序串 + key)?
- - 0 元 / P2G:免支付分支的判定(
paymentStatus===200 或 txamt===0)是否照搬?
- - agentorId:
/agents/:code 響應是否可附帶 agent 的 Guid id,避免下單時二次查詢?
- - 鑑權憑證:V2 BFF 用的門戶訪客 OAuth 帳號(
.env)權限是否覆蓋 CreateOrder / queryRenewableTenant(均 [AbpAutoAuth("Portal")],應可)?
+ - BR/CI 必填:關鍵 後端
CreateOrder 仍要求 BR 或 CI 至少一個(OrderService.cs:154,190)。plans-plus 新購(含個人主體)需不填也能下單——是否去除該校驗?還是前端把 corp BR 改必填、個人主體暫不開放?
+ - 單次查詢:後端是否新增按次查詢(options + 下單 + 計費 + 結果投遞)?本輪是否先保留 mock?
+ - 聯絡我們(不下單):已定方案 復用
contact/CreateFeedback 發送訂單摘要(不創建訂單/線索);「送推薦人」擬由後端 CreateFeedback.AssignedAgentUserId 承接(待實現,見 5.8)。待辦:後端加字段 + 服務路由 + EF 遷移;前端調用點由 /subscribe-offline 改 POST /api/contact 並透傳 agentUserId;BFF 映為 AssignedAgentUserId。
+ - 租戶查詢:能否新增「按管理員郵箱查可續費租戶」,直接帶 currentSubscription(planId/上期價/已購加值項)+ referrer?
+ - topup:純加購走
TenantRenewal(PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?
+ - KYC 月租:KYC plan 是否可按
PCS=租賃月數 計費(planDetail.price 當月租單價)?
+ - 續費續價:後端
TenantRenewal 是否支持沿用上期價/自定義價?否則前端續費折扣僅展示、實際按目錄價。
+ - agent tiers:BFF 經
GetPlanList(agentUserId) 推導 tiers,還是改為服務端直接按 agent 過濾 catalog?
+ - agentorId:
/agents/:code 響應附帶 agent Guid id,避免下單時二次查詢?
+ - 鑑權憑證:V2 BFF 門戶訪客帳號(
Portal@iconsz.com,__tenant=Portal)權限是否覆蓋 CreateOrder / queryRenewableTenant / SearchUserByCodeAndType?
+ - 本地種子:Plan/Edition 種子走臨時 SQL 還是入
AMLPortalDataSeedContributor?CPA edition GUID 是否同步進 appsettings.local.json?
+ - bestValue / note / 多語言:由 BFF 配置維護,還是後端在數據上補字段?
- 本分析基於源碼靜態閱讀:plans-plus.js / server/* / AMLPortal Controllers 與 DTO / 旧站 PlanService。涉及後端行為(續費續價、topup 語義)以實際接口為準。
+ 本分析基於源碼靜態閱讀 + 本地 docker 庫實測(2026-07):plans-plus.js / server/*(含 mock 契約)/ AMLPortal Controllers 與 Service / CustomIdentityUserController / 旧站 PlanService / appsettings.local.json 與本地數據庫。涉及後端行為(BR/CI 校驗、續費續價、topup / KYC 計費語義)以實際接口 + 後端確認為準。