From 4519399cb7bd1a1b668cf16cacd281bba08adf69 Mon Sep 17 00:00:00 2001 From: Million_MacMini <> Date: Wed, 24 Jun 2026 15:58:03 +0800 Subject: [PATCH] docs(justsolutions): add API analysis for plans-plus real backend integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docs/justsolutionsWebV2/plan-plus-api分析.html mapping each plans-plus mock endpoint to the real AMLPortal backend (the same endpoints the old justsolutionsWeb PlanService uses), via the V2 BFF pattern. Covers per-endpoint field mappings, transformation notes, and the gaps: - tenant lookup is by email but queryRenewableTenant searches by name - the new 'topup' type has no backend endpoint - CreateOrder/TenantRenewal PlanList needs PlanId+PlanDetailId back-fill - CreateOrder ignores AdminPassword; BR-or-CI required; no bestValue/note fields - QFPay payment signing must move server-side Also includes the justsolutionsWeb API-call inventory (api-calls.md/html) that this analysis builds on. --- docs/justsolutionsWeb/api-calls.html | 427 ++++++++++++++ docs/justsolutionsWeb/api-calls.md | 151 +++++ .../justsolutionsWebV2/plan-plus-api分析.html | 546 ++++++++++++++++++ 3 files changed, 1124 insertions(+) create mode 100644 docs/justsolutionsWeb/api-calls.html create mode 100644 docs/justsolutionsWeb/api-calls.md create mode 100644 docs/justsolutionsWebV2/plan-plus-api分析.html diff --git a/docs/justsolutionsWeb/api-calls.html b/docs/justsolutionsWeb/api-calls.html new file mode 100644 index 0000000..128247c --- /dev/null +++ b/docs/justsolutionsWeb/api-calls.html @@ -0,0 +1,427 @@ + + +
+ + +前端:justsolutions-web v1.2.0 · Angular 16 · Angular CLI 16.2.14
基于源码静态分析(src/app 下所有 *.ts)整理。共 14 个后端端点。
所有业务 API 调用都收口在统一的 HttpService(src/app/services/http.service.ts):
environment.apis.url 之后,即 environment.apis.url + request.url。localStorage 或 sessionStorage 读取 aml_access_token,存在时加上请求头 Authorization: Bearer <token>。Http.Request<T>(src/app/models/http.ts),字段含 method / url / body / params / headers / responseType 等。例外:支付跳转直接 location.href 重定向到 QFPay 外部网关(见第 6 节),不经过 HttpService。
| 环境文件 | apis.url(API 基地址) | apis.amlUrl(AML 门户来源) | apis.test |
|---|---|---|---|
environment.ts(本地默认) | https://api-aml.iconsz.com | https://aml.iconsz.com | true |
environment.dev.ts | https://api-aml.iconsz.com | https://aml.iconsz.com | true |
environment.prod.ts | https://api.justsolutions.ai | https://aml.justsolutions.ai | false |
apis.amlUrl:仅用于 plans 页面与父窗口(AML 门户 iframe 宿主)之间的 postMessage 通信,并非 HTTP 端点。apis.test:为 true 时支付金额固定写死为 10(分),用于测试支付(见第 6 节)。| # | 方法 | 端点(相对 apis.url) | 用途 | 前端方法 | 所在文件 |
|---|---|---|---|---|---|
| 1 | POST | +/connect/token?__tenant=2C |
+ OAuth 取 access token | +getToken() |
+ aml-query.service.ts |
+
| 2 | GET | +/api/aml/ConsumerPortal/GetPrice/{inputCode} |
+ 查询消费者查询价格 | +getPrice() |
+ aml-query.service.ts |
+
| 3 | POST | +/api/amlPortal/customer/CreateFeedback |
+ 提交联系表单反馈 | +sendContactEmailAndReturnFeedback() |
+ contact.service.ts |
+
| 4 | POST | +/api/aml/emailQueue/CreateEmailQueue |
+ 发送通知邮件(管理员 + 用户回执两次调用) | +sendContactEmail() / sendContactByReturnEmail() |
+ contact.service.ts |
+
| 5 | POST | +/api/amlPortal/plan/portal/GetPlanList |
+ 获取套餐列表(基础/jQ/加人/KYC) | +getAllPlanList() |
+ plan.service.ts |
+
| 6 | POST | +/api/amlPortal/Order/portal/GetEditionList |
+ 获取系统定义的 edition(所属行业)列表 | +getEditionList() |
+ plan.service.ts |
+
| 7 | POST | +/api/amlPortal/Order/getCategoryByTypes |
+ 获取国家/地区列表(typeCodes: ['COUNTRY']) |
+ getCountryList() |
+ plan.service.ts |
+
| 8 | GET | +/api/identity/users/SearchUserByCodeAndType/{userCode}/true |
+ 校验优惠码/代理商是否存在 | +getAgentor() |
+ plan.service.ts |
+
| 9 | POST | +/api/amlPortal/Order/portal/queryRenewableTenant |
+ 查询可续费租户 | +queryTenant() |
+ plan.service.ts |
+
| 10 | POST | +/api/amlPortal/Order/portal/ExistsByOrganizationBRCI |
+ 校验商业登记号(BR)/公司编号(CI)是否已存在 | +checkBRCI() |
+ plan.service.ts |
+
| 11 | POST | +/api/amlPortal/customer/portal/CheckEmailExists/{email} |
+ 校验管理员邮箱是否已存在 | +checkEmailExist() |
+ plan.service.ts |
+
| 12 | POST | +/api/amlPortal/Order/portal/CreateOrder |
+ 新租户下单 | +createOrder() |
+ plan.service.ts |
+
| 13 | POST | +/api/amlPortal/Order/portal/TenantRenewal |
+ 租户续费下单 | +tenantRenewal() |
+ plan.service.ts |
+
| 14 | POST | +/api/amlPortal/Order/portal/PaymentWebhook |
+ 支付完成回调(作为 notify_url 传给 QFPay,由其服务端回调,非前端直接调用) |
+ — | +plan4.component.ts |
+
端点 1 — POST /connect/token?__tenant=2C
AmlQueryService。在该服务构造函数中自动触发:当 localStorage 无 aml_access_token 时,调用 getToken() 并把返回的 access_token 写入 localStorage。Content-Type: application/x-www-form-urlencodedBody(x-www-form-urlencoded,硬编码凭证):
| 字段 | 值 |
|---|---|
grant_type | password |
response_type | token |
username | customer1 |
password | 1qaz@WSX |
scope | FX |
client_id | customer1_client |
client_secret | 1qaz@WSX |
⚠️ 用户名/密码/client secret 以明文硬编码在前端源码中(aml-query.service.ts:47-51),属于公开网站内置的固定访客凭证。
端点 2 — GET /api/aml/ConsumerPortal/GetPrice/{inputCode}
aml-query-layout.component.ts(ngOnInit 时以 getPrice('E') 拉取价格)。AmlQueryService.calculatePrice()(本地计算,不走接口)。aml-query/pages/result/result.component.ts(评估报告页)当前使用本地 mock 数据 / Math.random() 生成风险评估结果,PDF 下载指向 assets/sample-report.pdf,apiIntegration() 弹窗里的 api.credit-risk.example.com 等仅为示例文案,不是真实后端调用。
调用方:contact/pages/contact/contact.component.ts 提交表单时触发。
POST /api/amlPortal/customer/CreateFeedback:保存反馈记录。
+ CreateFeedbackParam):companyName、customerName、customerEmail、typeOfQuery、message、userHost、destEmail、emailSubject、emailBody。
+ POST /api/aml/emailQueue/CreateEmailQueue:进入邮件发送队列,被调用两次:
+ sendContactEmail() → 通知管理员(收件人 environment.email.contact)。sendContactByReturnEmail() → 给提交者发送回执邮件(收件人为用户填写邮箱)。CreateEmailQueueParam):subject、to、body(HTML)、businessType: 2100、businessId: ''。
+ PlanService 集中了套餐查询、校验与下单接口。各端点的页面调用关系:
| 端点 | 调用页面 |
|---|---|
getAllPlanList()端点 5 | plans.component.ts(进入套餐页 / 收到父窗口 postMessage 后) |
getEditionList()端点 6 | plans.component.ts、plan3.component.ts |
getCountryList()端点 7 | plans.component.ts、plan3.component.ts |
getAgentor()端点 8 | plan3.component.ts(输入优惠码校验) |
queryTenant()端点 9 | plan1.component.ts、plan3.component.ts(续费选租户) |
checkBRCI()端点 10 | plan3.component.ts |
checkEmailExist()端点 11 | plan1.component.ts、plan3.component.ts |
createOrder()端点 12 | plan4.component.ts(type === 'new') |
tenantRenewal()端点 13 | plan4.component.ts(type === 'renew') |
getAllPlanList(GetPlanListParam):pageIndex: 0、pageSize: 9999、tag1List: ['B','jQ','j','AdlU','KYC']、tag2List/tag3List(由调用方传入)、agentUserId。getCountryList:{ typeCodes: ['COUNTRY'] }。checkBRCI:通过 params 传 br / ci(query string)。createOrder(CreateOrderParam):planList[](planId/planDetailId/pcs)、tenantName、tenantAdminEmail、jurisdiction、organizationBR、organizationCI、organizationReference、contactPerson、companyAddress、phone、effectiveStartTime、agentorId、isOfflinePayment: false、isAgentBehalf。tenantRenewal(TenantRenewalParam):targetTenantID、planList[]、agentorId、tenantAdminEmail、isOfflinePayment: false、isAgentBehalf。位置:plan4.component.ts 的 payFn()。下单成功(createOrder/tenantRenewal 返回 code === 0)后:
paymentStatus === 200 或 txamt === 0 → 直接跳转 plans-plus/payment-success。location.href 重定向到 QFPay 收银台:https://openapi-hk.qfapi.com/checkstand/#/?<参数>&sign=<sha256>主要参数:
+| 参数 | 值 |
|---|---|
appcode | environment.qfpay.appcode(固定值) |
goods_name | 下单返回的 planName |
out_trade_no | 下单返回的 orderCode |
paysource | remotepay_checkout |
return_url | {baseUrl}/plans-plus/payment-success |
failed_url | {baseUrl}/plans-plus/payment-failure |
notify_url | {apis.url}/api/amlPortal/Order/portal/PaymentWebhook(端点 14,QFPay 服务端回调后端) |
txamt | apis.test ? 10 : planPrice * 100(单位:分) |
txcurrcd | HKD |
sign | sha256(<排序后的参数串> + api_key) |
| 前缀 | 含义 | 涉及端点 |
|---|---|---|
/connect/* | OpenIddict/IdentityServer OAuth | 端点 1 |
/api/aml/* | AML 主模块(消费者查询、邮件队列) | 端点 2、4 |
/api/amlPortal/* | AML 门户模块(客户、套餐、订单) | 端点 3、5、6、7、9、10、11、12、13、14 |
/api/identity/* | ABP Identity(用户) | 端点 8 |
AmlQueryService 构造 → 端点 1 取 token。把 public/plans-plus.html 的單頁訂閱流程,從本地 mock 切換到 AML 後端(iCON.Abp.AMLPortal)真實 API。
plans-plus 前端共依賴 7 個業務端點(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 justsolutionsWeb 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 justsolutionsWebV2/server/routes/ 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 countries.js / industries.js 完全同一範式)。
| 前端端點 | 對應後端 | 復用程度 | 主要缺口 |
|---|---|---|---|
GET /editions | Order/portal/GetEditionList | 改造 | 多語言 edition 名稱(後端只有 displayName) |
GET /plans/catalog | plan/portal/GetPlanList | 改造 | 需復刻旧站 filterPlan 拆分;bestValue/note 後端無 |
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 回調 |
好消息:後端 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)還是新增端點。
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。關鍵差異(與旧站):旧站 Angular 直接從瀏覽器調 https://api-aml.iconsz.com/api/amlPortal/*,token 存 localStorage。V2 改為瀏覽器不直接接觸後端,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。
以下是 plans-plus.js 實際讀寫的字段(即 BFF 必須產出/接受的契約,目前由 mock/routes.js 滿足)。真實對接時 BFF 輸出必須與此逐字段一致,否則前端渲染/計價會出錯。
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 }
+
+ 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 } } }
+
+ submit() 先 POST /subscribe 拿 orderId,再 POST /payments/create 拿 redirectUrl 跳轉支付。
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/TenantRenewal 的 PlanList 每項都必須同時帶 PlanId+PlanDetailId(OrderService.cs:176 聯合校驗)。⇒ BFF 需在服務端依 /plans/catalog 結果反查補齊 planDetailId(見 5.6)。
以下端點均在 AML_Backend/modules/iCON.Abp.AMLPortal,並已被旧站 justsolutionsWeb/PlanService 使用驗證過。前綴 /api/amlPortal/*,鑑權走 [AbpAutoAuth("Portal")](門戶訪客 token)。
| # | 方法 / 路由 | 用途 | 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 |
| 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 |
DTO 位置:Application.Contracts/PlanAppLayer/*(GetPlanListParam、PlanDto、PlanDetailDto)、Application.Contracts/OrderAppLayer/*(CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto)。
每節:前端契約 → 對應後端 → 字段映射 → BFF 轉換要點 → 差異/缺口。新增的 BFF 文件建議放 server/routes/,與 countries.js 同範式(createAuthConfig() + axios + 字段映射 + {success,data} 包裝)。
後端返回 { code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }。前端要 editionList:[{id, displayName, nameCN, nameJP}] 與 jQSeparatedEditions.editionIds。
| 前端字段 | 後端來源 | 說明 |
|---|---|---|
id | editionList[].id(Guid) | 直接映射;後續作 OrganizationReference |
displayName | editionList[].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。
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 }
+ | 前端 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 |
periodMonths | planDetails[0].periodMonths | |
price/originalPrice | planDetails[0].price/originalPrice | 劃線價用 originalPrice |
qCount | planDetails[0].qCount | -1 表無限 |
userCountLimit | planDetails[0].userCountLimit | |
bestValue | 後端無 | 缺口,見下 |
noteCN/EN/JP | item.description?(無多語言) | 缺口,見下 |
standard ← tag1Code==='B' && tag2Code!=='CPA';cpa ← tag1Code==='B' && tag2Code==='CPA'addons.user.unitPrice ← tag1Code==='AdlU' 的 planDetails[0].price(並記其 planId/planDetailId 備下單)addons.kyc.unitPrice ← tag1Code==='KYC' 同上addons.jquota.packages[] ← tag1Code==='jQ'||'j':每項 { id:planId, jq:qCount, price:planDetails[0].price, name* },並各自記 planDetailId缺口:bestValue(最超值標記)、note*(賣點文案)後端 PlanDto/PlanDetailDto 無對應字段。建議:① BFF 配置(按 systemCode 標 bestValue / 文案);或 ② 後端在 Plan 增 bestValue 與多語言 note。短期可在 BFF 寫死映射,不阻塞。
提示:plans-plus 的 jQuota 改為「選配套」(jquotaPackageId),不再是旧站的「按數量」。後端 jQ 計劃本就是離散 plan,天然契合——每個 jQ plan 即一個 package。
server/routes/countries.js 已實現並可直接用:取 typeCodes:['COUNTRY'],從 categoryTranslations 提 zh-hk/zh-cn/ja/en-us,輸出 {code, name, nameTC, nameSC, nameJP, phoneCode},正好滿足 plans-plus 的 countryDisplay() 與 phoneCode 自動填充。無需改動。
前端要 { 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.name | name / surname / userName |
(下單用)agentorId | 用戶 id(Guid)→ 提交時作 CreateOrderParam.AgentorId |
關鍵:前端只展示 name,但下單需要 agent 的 Guid id(CreateOrderParam.AgentorId)。BFF 應在 /agents/:code 響應內順帶緩存 code→id(或前端 data 內含 id),否則 /subscribe 階段需再查一次。建議 data 增隱藏字段 agentorId。
備註:AMLPortal 另有 Agentor/portal/GetAgentorByCode,但已標【廢棄】;旧站用的是 Identity 的 SearchUserByCodeAndType,沿用之。
前端按郵箱查租戶,支持「none / unique / multiple」三態,命中後帶出 currentSubscription 預填續費表單。但後端 queryRenewableTenant 只接受 {keyword} 且按 TenantName.Contains(keyword) 搜索(OrderService.cs:2363),返回 List<TenantPropertyDto>(內部已把 admin 郵箱填入 TenantAdminEmail)。
TenantAdminEmail===email 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。TenantPropertyDto 結構)。currentSubscription(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 TenantPropertyDto 直接字段,需由 TenantProperty + 其 Order + 正在生效的 plan 推導。可參考 GetCurrServiceInfo(CurrServiceInfoDto.ActiveBPlans、EffectiveEndTime、QCountLeft 等)。| 前端 | TenantPropertyDto | 備註 |
|---|---|---|
tenantId | TargetTenantID | |
tenantName | TenantName | |
editionId/editionName | EditionId/EditionName | |
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 | 需經訂單明細反查 | 推導 |
建議後端改動:新增端點 queryRenewableTenantByEmail(email[, tenantName]),直接返回 plans-plus 所需的「租戶 + currentSubscription(含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查。
BFF 依 body.type 分流到不同後端端點,並把前端的「人類友好」payload 翻成後端 DTO;核心是組裝 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);plans-plus 僅收 BR 且標可選 ⇒ 缺口 |
EffectiveStartTime | startDate | |
ContactPerson | contact | |
CompanyAddress | address | 可選 |
Phone | phoneCode + phone | 可選;BFF 拼接 |
AgentorId | 由 agentCode 解析的 Guid | 見 5.4 |
AdminPassword | — | 忽略(後端用默認密碼,發重置郵件) |
IsOfflinePayment/IsAgentBehalf | — | 固定 false |
| TenantRenewalParam | 前端 payload |
|---|---|
TargetTenantID | tenantId |
PlanList[] | plan + addons(同下組裝) |
TenantAdminEmail | email |
AgentorId | 空 → 延續原代理 |
IsOfflinePayment/IsAgentBehalf | 固定 false |
純加購(無基礎方案,只買 增加用戶/KYC/jQuota)。後端無直接端點。候選方案:
+TenantRenewal,PlanList 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。TenantTopup 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC,不延長有效期」。UpdateTenantProperty(admin)邏輯可借鑒(它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。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):下單成功返回含 orderCode、planName、paymentStatus、txamt 等(旧站 plan4.component 用其拼 QFPay URL)。BFF 應把其中 orderId/orderCode 回給前端的 data.orderId,並暫存 orderCode/amount 供下一步支付用。
旧站沒有獨立的 create-payment 後端端點:plan4.component.payFn() 在前端直接用 orderCode/planName/txamt 拼 QFPay checkstand URL 並 sha256 簽名後 location.href 跳轉。plans-plus 改成調 /payments/create 拿 redirectUrl ⇒ 這段簽名邏輯應移到 BFF 服務端(API key 不可暴露前端)。
| QFPay 參數 | 來源 |
|---|---|
appcode | 環境配置(.env) |
goods_name | 下單返回 planName / 前端 planName |
out_trade_no | 下單返回 orderCode |
txamt | apis.test ? 10 : amount*100(單位:分) |
txcurrcd | HKD |
return_url/failed_url | V2 的 pay-return / 失敗頁 |
notify_url | {API_BASE_URL}/api/amlPortal/Order/portal/PaymentWebhook(端點 10,QFPay 服務端回調後端) |
sign | sha256(排序參數串 + 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===200 或 txamt===0(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(redirectUrl 直接給成功頁)。
以下是 plans-plus 單頁流程相對旧站 4 步嚮導新增或改變的點,逐一標注對接影響。
+| # | 新增/變更 | 對接影響 | 狀態 |
|---|---|---|---|
| 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 至少一個;需 UI 補 CI 或設必填 | 需對齊 |
| 10 | 支付 create 端點化(前端拿 redirectUrl) | QFPay 簽名邏輯移到 BFF 服務端 | BFF |
| 文件 | 職責 | 後端依賴 |
|---|---|---|
routes/editions.js(新) | GET /editions + 多語言/過濾整理 | GetEditionList |
routes/plans.js(新) | GET /plans/catalog + filterPlan 拆分 + 緩存 planDetailId | GetPlanList |
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 文案映射等 | — |
bestValue 與多語言 note;edition 增多語言名稱。| 階段 | 內容 | 可獨立交付 |
|---|---|---|
| P1 · 只讀目錄 | editions + plans/catalog + countries(已就緒)+ agents | 頁面可正常渲染方案/行業/國家,校驗推薦人 |
| P2 · 新購下單 | subscribe(new) + payments/create(QFPay) | 新租戶完整下單支付閉環 |
| P3 · 續費 | tenants/lookup(後端新端點)+ subscribe(renew) | 續費閉環,含續價 |
| P4 · 加購 | topup(後端確認後) | 加購閉環 |
appcode/api_key/收銀台域名?簽名算法是否一致(sha256 排序串 + key)?paymentStatus===200 或 txamt===0)是否照搬?/agents/:code 響應是否可附帶 agent 的 Guid id,避免下單時二次查詢?.env)權限是否覆蓋 CreateOrder / queryRenewableTenant(均 [AbpAutoAuth("Portal")],應可)?
+ 本分析基於源碼靜態閱讀:plans-plus.js / server/* / AMLPortal Controllers 與 DTO / 旧站 PlanService。涉及後端行為(續費續價、topup 語義)以實際接口為準。
+