justsolutionsWebV2 · plans-plus 真實 API 對接分析

public/plans-plus.html 的單頁訂閱流程,從本地 mock 切換到 AML 後端(iCON.Abp.AMLPortal)真實 API。

範圍:justsolutionsWebV2/public/js/plans-plus.js(前端契約) · justsolutionsWebV2/server/(BFF 代理層) · AML_Backend/modules/iCON.Abp.AMLPortal(後端)

參照:旧站 justsolutionsWebPlanService(同一套後端端點) · 见 docs/justsolutionsWeb/api-calls.md

目錄

  1. 1. 結論速覽
  2. 2. 總體架構與調用鏈
  3. 3. plans-plus 前端依賴的 API 契約
  4. 4. 後端真實端點清單(AMLPortal)
  5. 5. 端點逐一映射與落地方案
  6. 5.1 GET /editions(所屬行業)
  7. 5.2 GET /plans/catalog(方案目錄)
  8. 5.3 GET /countries(註冊地)
  9. 5.4 GET /agents/:code(推薦人)
  10. 5.5 GET /tenants/lookup(續費查租戶)
  11. 5.6 POST /subscribe(下單)
  12. 5.7 POST /payments/create(支付)
  13. 6. plans-plus 相對旧站的新增需求
  14. 7. 實施建議與分期
  15. 8. 待確認問題清單

1. 結論速覽

plans-plus 前端共依賴 7 個業務端點(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 justsolutionsWeb 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 justsolutionsWebV2/server/routes/ 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 countries.js / industries.js 完全同一範式)。

前端端點對應後端復用程度主要缺口
GET /editionsOrder/portal/GetEditionList改造多語言 edition 名稱(後端只有 displayName)
GET /plans/catalogplan/portal/GetPlanList改造需復刻旧站 filterPlan 拆分;bestValue/note 後端無
GET /countriesOrder/getCategoryByTypes已實現—(routes/countries.js 已可用)
GET /agents/:codeidentity/users/SearchUserByCodeAndType改造響應 → {code,name} 轉換
GET /tenants/lookupOrder/portal/queryRenewableTenant缺口大後端按租戶名搜,前端按郵箱查;currentSubscription 需重建
POST /subscribeCreateOrder / TenantRenewal改造需補 planDetailIdtopup 類型無對應端點
POST /payments/createQFPay 收銀台(旧站前端拼 URL)缺口大需服務端簽名 + PaymentWebhook 回調

好消息:後端 CreateOrder 已忽略前端傳入的 AdminPassword,改用 appConfig.General.TenantAdminDefaultPasswordOrderService.cs:215,236)。因此 plans-plus 去掉「管理員密碼」步驟不構成阻塞——後端會發重置密碼郵件給租戶管理員。

兩個真正的硬缺口:

按郵箱查租戶queryRenewableTenant 只支持 keyword(按 TenantName.Contains)。plans-plus 用郵箱查、且要支持「一郵箱多租戶」選擇——需新增後端端點,或在 BFF 全量拉取後按郵箱過濾(不可取)。

topup(加購)流程:plans-plus 新增的純加購(不含基礎方案、只買 增加用戶/KYC/jQuota)旧站沒有,後端亦無直接端點。需確認映射到 TenantRenewal(僅加值項 planList)還是新增端點。

2. 總體架構與調用鏈

justsolutionsWebV2 採「BFF(Backend-for-Frontend)」式:瀏覽器只調用本站 /api/*,由 Node/Express 依 APP_ENV 決定走 mock 還是真實後端,前端代碼零改動即可切換環境。

瀏覽器 plans-plus.js → window.api.get/post('/api/*') → Express server →   ├─ APP_ENV=test ………… server/mock/routes.js(內存假數據)   └─ dev/stag/prod …… server/routes/*.js(BFF)→ AuthService 取 token → AMLPortal 後端

關鍵差異(與旧站):旧站 Angular 直接從瀏覽器調 https://api-aml.iconsz.com/api/amlPortal/*,token 存 localStorage。V2 改為瀏覽器不直接接觸後端,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。

3. plans-plus 前端依賴的 API 契約

以下是 plans-plus.js 實際讀寫的字段(即 BFF 必須產出/接受的契約,目前由 mock/routes.js 滿足)。真實對接時 BFF 輸出必須與此逐字段一致,否則前端渲染/計價會出錯。

3.1 載入期(頁面初始化並發拉取)

loadAll() 並發請求 /editions/plans/catalog/countries,三者均以 { 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}] } } } }
GET /countries    → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] }

Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths,
         price, originalPrice, qCount(-1=無限), userCountLimit,
         bestValue, noteCN, noteEN, noteJP }

3.2 交互期(按需)

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 }

Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci,
           currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price,
             qCount, userCountLimit, startDate, expiryDate, usedQuota,
             addons:{ users:int, kyc:bool, jquotaPackageId:string } } }

3.3 提交期(兩步串聯)

submit()POST /subscribeorderId,再 POST /payments/createredirectUrl 跳轉支付。

POST /subscribe  body = {
   type:'new'|'renew'|'topup', edition, isCpa, startDate,
   plan:Plan, planPrice, isRenewalRate,
   addons:{ users, userUnitPrice, kyc, kycUnitPrice,
            jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice },
   agentCode, subtotal, total,
   // type=new 追加: company, jurisdiction, br, contact, phoneCode, phone, email, 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 } }

注意:/subscribe 的 body 不含 planDetailId,也不含各加值項(增加用戶/KYC/jQuota)對應的 planId/planDetailId。而後端 CreateOrder/TenantRenewalPlanList 每項都必須同時帶 PlanId+PlanDetailIdOrderService.cs:176 聯合校驗)。⇒ BFF 需在服務端依 /plans/catalog 結果反查補齊 planDetailId(見 5.6)。

4. 後端真實端點清單(AMLPortal)

以下端點均在 AML_Backend/modules/iCON.Abp.AMLPortal,並已被旧站 justsolutionsWeb/PlanService 使用驗證過。前綴 /api/amlPortal/*,鑑權走 [AbpAutoAuth("Portal")](門戶訪客 token)。

#方法 / 路由用途Controller
1POST /api/amlPortal/plan/portal/GetPlanList取方案列表(B/jQ/j/AdlU/KYC)PlanController:37
2POST /api/amlPortal/Order/portal/GetEditionList取 edition(所屬行業)+ jQSeparatedEditionsOrderController:136
3POST /api/amlPortal/Order/getCategoryByTypes取國家列表(typeCodes:['COUNTRY']OrderController:184
4GET /api/identity/users/SearchUserByCodeAndType/{code}/true校驗推薦人/代理 codeIdentity(非 AMLPortal)
5POST /api/amlPortal/Order/portal/queryRenewableTenant查可續費租戶(按 keyword=租戶名)OrderController:308
6POST /api/amlPortal/Order/portal/ExistsByOrganizationBRCI校驗 BR/CI 是否已存在OrderController:150
7POST /api/amlPortal/customer/portal/CheckEmailExists/{email}校驗管理員郵箱是否已用CustomerController:121
8POST /api/amlPortal/Order/portal/CreateOrder新租戶下單OrderController:53
9POST /api/amlPortal/Order/portal/TenantRenewal租戶續費下單OrderController:321
10POST /api/amlPortal/Order/portal/PaymentWebhook支付回調(QFPay 服務端調,非前端)OrderController:163

DTO 位置:Application.Contracts/PlanAppLayer/*(GetPlanListParam、PlanDto、PlanDetailDto)、Application.Contracts/OrderAppLayer/*(CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto)。

5. 端點逐一映射與落地方案

每節:前端契約 → 對應後端 → 字段映射 → BFF 轉換要點 → 差異/缺口。新增的 BFF 文件建議放 server/routes/,與 countries.js 同範式(createAuthConfig() + axios + 字段映射 + {success,data} 包裝)。

5.1 GET /editions(所屬行業) 改造

GET /api/editions  →  POST /api/amlPortal/Order/portal/GetEditionList

後端返回 { code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }。前端要 editionList:[{id, displayName, nameCN, nameJP}]jQSeparatedEditions.editionIds

前端字段後端來源說明
ideditionList[].id(Guid)直接映射;後續作 OrganizationReference
displayNameeditionList[].displayName英文名
nameCN / nameJP後端無缺口,見下
jQSeparatedEditions.editionIds同名字段驅動 CPA/標準方案集切換(state.isCpa

缺口:edition 多語言名稱。後端 GetEditionList 只有 displayName,旧站也僅用 displayName。plans-plus 的 editionName() 在 tc/jp 下會優先取 nameCN/nameJP,缺失時已能 fallback 回 displayName ⇒ 不阻塞,但中日文會顯示英文

建議:BFF 維護一份 editionId/displayName → {nameCN,nameJP} 映射表(與 industries.js 現有靜態行業翻譯同思路),或後端在 edition 上補多語言字段。

旧站行為(可選沿用):getEditionList()過濾掉 Standard、並把 Others 排到列表末尾,同時把 editionIds toLowerCase()。plans-plus 目前未做此整理;若要與旧站一致,這段邏輯應放 BFF。

5.2 GET /plans/catalog(方案目錄) 改造

GET /api/plans/catalog  →  POST /api/amlPortal/plan/portal/GetPlanList

BFF 以旧站同款請求體調 GetPlanList,再復刻旧站 filterPlan() 把扁平 itemstag1Code 拆成 5 組,組裝成 {standard, cpa, addons}

請求體(沿用 PlanService.getAllPlanList)

{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false,
  tag1List:['B','jQ','j','AdlU','KYC'], tag2List, tag3List, agentUserId }

後端 items → 前端 Plan 字段映射

前端 Plan後端來源(item / planDetails[0])備註
planIditem.id
planDetailId ⚠️item.planDetails[0].id前端契約現缺此字段,但下單必需 ⇒ BFF 須補進 Plan,提交時回填(見 5.6)
tag2Codeitem.tag2CodeStd/P2G/CPA/Pre…,CPA 判定用
nameCN/nameENitem.nameCN/nameEN旧站會截掉「(…」後綴,可沿用
nameJP後端無缺口,fallback EN
periodMonthsplanDetails[0].periodMonths
price/originalPriceplanDetails[0].price/originalPrice劃線價用 originalPrice
qCountplanDetails[0].qCount-1 表無限
userCountLimitplanDetails[0].userCountLimit
bestValue後端無缺口,見下
noteCN/EN/JPitem.description?(無多語言)缺口,見下

addons 拆分(按 tag1Code)

缺口:bestValue(最超值標記)、note*(賣點文案)後端 PlanDto/PlanDetailDto 無對應字段。建議:① BFF 配置(按 systemCode 標 bestValue / 文案);或 ② 後端在 Plan 增 bestValue 與多語言 note。短期可在 BFF 寫死映射,不阻塞。

提示:plans-plus 的 jQuota 改為「選配套」(jquotaPackageId),不再是旧站的「按數量」。後端 jQ 計劃本就是離散 plan,天然契合——每個 jQ plan 即一個 package。

5.3 GET /countries(註冊地) 已實現

GET /api/countries  →  POST /api/amlPortal/Order/getCategoryByTypes

server/routes/countries.js 已實現並可直接用:取 typeCodes:['COUNTRY'],從 categoryTranslations 提 zh-hk/zh-cn/ja/en-us,輸出 {code, name, nameTC, nameSC, nameJP, phoneCode},正好滿足 plans-plus 的 countryDisplay()phoneCode 自動填充。無需改動。

5.4 GET /agents/:code(推薦人代理) 改造

GET /api/agents/:code  →  GET /api/identity/users/SearchUserByCodeAndType/{code}/true

前端要 { success, found, data:{code, name} }。後端(Identity)返回匹配到的用戶(含 name/userName/extraProperties.code 等)。BFF 取首條 → found:true, data:{code, name};無匹配 → found:false, data:null

前端後端用戶字段
data.code用戶 code(agent code)
data.namename / surname / userName
(下單用)agentorId用戶 id(Guid)→ 提交時作 CreateOrderParam.AgentorId

關鍵:前端只展示 name,但下單需要 agent 的 Guid idCreateOrderParam.AgentorId)。BFF 應在 /agents/:code 響應內順帶緩存 code→id(或前端 data 內含 id),否則 /subscribe 階段需再查一次。建議 data 增隱藏字段 agentorId

備註:AMLPortal 另有 Agentor/portal/GetAgentorByCode,但已標【廢棄】;旧站用的是 Identity 的 SearchUserByCodeAndType,沿用之。

5.5 GET /tenants/lookup(續費查租戶) 缺口大

GET /api/tenants/lookup?email=&name=  →  POST /api/amlPortal/Order/portal/queryRenewableTenant

前端按郵箱查租戶,支持「none / unique / multiple」三態,命中後帶出 currentSubscription 預填續費表單。但後端 queryRenewableTenant 只接受 {keyword} 且按 TenantName.Contains(keyword) 搜索(OrderService.cs:2363),返回 List<TenantPropertyDto>(內部已把 admin 郵箱填入 TenantAdminEmail)。

兩個不匹配點

  1. 查詢維度不符:前端給郵箱,後端按名字搜。
    短期 BFF 變通:以空/寬 keyword 拉取後在服務端按 TenantAdminEmail===email 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。
    正解:後端新增「按管理員郵箱查可續費租戶」端點(返回同 TenantPropertyDto 結構)。
  2. currentSubscription 需重建:前端的 currentSubscription(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 TenantPropertyDto 直接字段,需由 TenantProperty + 其 Order + 正在生效的 plan 推導。可參考 GetCurrServiceInfoCurrServiceInfoDto.ActiveBPlans、EffectiveEndTime、QCountLeft 等)。

TenantPropertyDto → 前端 Tenant 映射(可得部分)

前端TenantPropertyDto備註
tenantIdTargetTenantID
tenantNameTenantName
editionId/editionNameEditionId/EditionName
currentSubscription.expiryDateEffectiveEndTime過期判定(前端 daysUntil)
currentSubscription.startDateEffectiveStartTime未過期續費 → 默認接續此日
currentSubscription.qCount/usedQuotaQCountPurchase / QCountPurchase−QCountLeft已用 = 購買 − 剩餘
currentSubscription.userCountLimitUserCountLimit
currentSubscription.addons.kycEnableKYCtopup 時鎖「已購買」
currentSubscription.planId/name*/price需經 Order/OrderDetail 反查推導,續費續價(沿用上期 price)依賴此
currentSubscription.addons.users/jquotaPackageId需經訂單明細反查推導

建議後端改動:新增端點 queryRenewableTenantByEmail(email[, tenantName]),直接返回 plans-plus 所需的「租戶 + currentSubscription(含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查。

5.6 POST /subscribe(下單) 改造

POST /api/subscribe  →  按 type 分流

BFF 依 body.type 分流到不同後端端點,並把前端的「人類友好」payload 翻成後端 DTO;核心是組裝 PlanList(基礎方案 + 各加值項,每項補 planDetailId)。

① type='new' → CreateOrder

CreateOrderParam前端 payload備註
PlanList[]plan + addons見下「PlanList 組裝」
TenantNamecompany必填;後端校驗重名
TenantAdminEmailemail必填;後端再校驗未註冊
Jurisdictionjurisdiction必填
OrganizationReferenceedition(Guid)必填;校驗 edition 存在
OrganizationBR / OrganizationCIbr / —BR 或 CI 至少一個(OrderService.cs:154);plans-plus 僅收 BR 且標可選 ⇒ 缺口
EffectiveStartTimestartDate
ContactPersoncontact
CompanyAddressaddress可選
PhonephoneCode + phone可選;BFF 拼接
AgentorIdagentCode 解析的 Guid見 5.4
AdminPassword忽略(後端用默認密碼,發重置郵件)
IsOfflinePayment/IsAgentBehalf固定 false

② type='renew' → TenantRenewal

TenantRenewalParam前端 payload
TargetTenantIDtenantId
PlanList[]plan + addons(同下組裝)
TenantAdminEmailemail
AgentorId空 → 延續原代理
IsOfflinePayment/IsAgentBehalf固定 false

③ type='topup' → 新增需求 · 無對應端點

純加購(無基礎方案,只買 增加用戶/KYC/jQuota)。後端無直接端點。候選方案:

PlanList 組裝(三種 type 共用,核心難點)

PlanList = []
// 基礎方案(topup 不加)
if (type!=='topup') PlanList.push({ PlanId: plan.planId,
                                    PlanDetailId: plan.planDetailId, PCS: 1 })
// 增加用戶:PCS = addons.users
if (addons.users>0) PlanList.push({ PlanId: AdlU.planId,
                                    PlanDetailId: AdlU.planDetailId, PCS: addons.users })
// KYC:PCS = 1
if (addons.kyc)     PlanList.push({ PlanId: KYC.planId,
                                    PlanDetailId: KYC.planDetailId, PCS: 1 })
// jQuota:選中的配套 ×1
if (addons.jquotaPackageId)
                    PlanList.push({ PlanId: jqPack.planId,
                                    PlanDetailId: jqPack.planDetailId, PCS: 1 })

planDetailId 從哪來:前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 /plans/catalog 的服務端版本(含 planDetailId),按 plan.planId 與加值項類型反查補齊。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),/subscribe 時直接查表。

後端返回(OperationDto):下單成功返回含 orderCodeplanNamepaymentStatustxamt 等(旧站 plan4.component 用其拼 QFPay URL)。BFF 應把其中 orderId/orderCode 回給前端的 data.orderId,並暫存 orderCode/amount 供下一步支付用。

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/createredirectUrl這段簽名邏輯應移到 BFF 服務端(API key 不可暴露前端)。

BFF 實現要點(對標旧站 QFPay 參數)

QFPay 參數來源
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)服務端計算

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;② 提供查單(透傳後端訂單狀態)給返回頁輪詢。

免支付分支:旧站當訂單 paymentStatus===200txamt===0(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(redirectUrl 直接給成功頁)。

6. plans-plus 相對旧站的新增需求

以下是 plans-plus 單頁流程相對旧站 4 步嚮導新增或改變的點,逐一標注對接影響。

#新增/變更對接影響狀態
1topup 加購類型(無基礎方案,只買加值項)後端無端點,需確認映射 TenantRenewal 或新增端點需後端
2按郵箱查租戶 + 一郵箱多租戶選擇後端 queryRenewableTenant 按名字搜,需新增按郵箱端點需後端
3jQuota 改為選配套(package,非按量)BFF 把 jQ plan 映成 packages 即可BFF
4KYC「已購買」鎖定(topup 已有則禁買)依賴租戶 EnableKYC,BFF 帶出即可BFF
5續費續價(沿用上期 price,顯示續費折扣)依賴 currentSubscription.price,需訂單反查需後端
6去掉管理員密碼步驟後端已忽略 AdminPassword,發重置郵件無影響
7bestValue / 賣點 note(方案卡片標記與文案)後端 Plan 無此字段,BFF 配置或後端補BFF/後端
8edition 多語言名稱(中/日)後端只有 displayName,BFF 補映射BFF
9BR 標為可選(UI)後端要求 BR 或 CI 至少一個;需 UI 補 CI 或設必填需對齊
10支付 create 端點化(前端拿 redirectUrl)QFPay 簽名邏輯移到 BFF 服務端BFF

7. 實施建議與分期

7.1 新增 / 改動文件清單(justsolutionsWebV2/server)

文件職責後端依賴
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 可能的後端改動(建議與後端團隊確認)

7.3 建議分期

階段內容可獨立交付
P1 · 只讀目錄editions + plans/catalog + countries(已就緒)+ agents頁面可正常渲染方案/行業/國家,校驗推薦人
P2 · 新購下單subscribe(new) + payments/create(QFPay)新租戶完整下單支付閉環
P3 · 續費tenants/lookup(後端新端點)+ subscribe(renew)續費閉環,含續價
P4 · 加購topup(後端確認後)加購閉環

8. 待確認問題清單

  1. 租戶查詢:後端能否新增「按管理員郵箱查可續費租戶」?返回是否能直接帶 currentSubscription(planId / 上期價 / 已購加值項)?
  2. topup:純加購走 TenantRenewal(PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?還是需新端點?
  3. BR/CI:plans-plus 把 BR 標為可選,但後端要求 BR 或 CI 至少一個——UI 是否補 CI 字段,或把 BR 設為必填?
  4. bestValue / note / edition 多語言:由 BFF 配置維護,還是後端在數據上補字段?
  5. QFPay:V2 是否沿用旧站同一 appcode/api_key/收銀台域名?簽名算法是否一致(sha256 排序串 + key)?
  6. 0 元 / P2G:免支付分支的判定(paymentStatus===200txamt===0)是否照搬?
  7. agentorId:/agents/:code 響應是否可附帶 agent 的 Guid id,避免下單時二次查詢?
  8. 鑑權憑證:V2 BFF 用的門戶訪客 OAuth 帳號(.env)權限是否覆蓋 CreateOrder / queryRenewableTenant(均 [AbpAutoAuth("Portal")],應可)?

本分析基於源碼靜態閱讀:plans-plus.js / server/* / AMLPortal Controllers 與 DTO / 旧站 PlanService。涉及後端行為(續費續價、topup 語義)以實際接口為準。