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 @@ + + + + + +justsolutionsWeb — 后端 API 调用清单 + + + +
+ +
+

justsolutionsWeb — 后端 API 调用清单

+

前端:justsolutions-web v1.2.0 · Angular 16 · Angular CLI 16.2.14

+

基于源码静态分析(src/app 下所有 *.ts)整理。共 14 个后端端点。

+
+ + + +
+

1. 调用架构概览

+

所有业务 API 调用都收口在统一的 HttpServicesrc/app/services/http.service.ts):

+ +
+

例外:支付跳转直接 location.href 重定向到 QFPay 外部网关(见第 6 节),不经过 HttpService

+
+ +

1.1 环境与基地址配置

+ + + + + + + + + +
环境文件apis.url(API 基地址)apis.amlUrl(AML 门户来源)apis.test
environment.ts(本地默认)https://api-aml.iconsz.comhttps://aml.iconsz.comtrue
environment.dev.tshttps://api-aml.iconsz.comhttps://aml.iconsz.comtrue
environment.prod.tshttps://api.justsolutions.aihttps://aml.justsolutions.aifalse
+ +
+ +
+

2. 调用清单(按端点汇总)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#方法端点(相对 apis.url用途前端方法所在文件
1POST/connect/token?__tenant=2COAuth 取 access tokengetToken()aml-query.service.ts
2GET/api/aml/ConsumerPortal/GetPrice/{inputCode}查询消费者查询价格getPrice()aml-query.service.ts
3POST/api/amlPortal/customer/CreateFeedback提交联系表单反馈sendContactEmailAndReturnFeedback()contact.service.ts
4POST/api/aml/emailQueue/CreateEmailQueue发送通知邮件(管理员 + 用户回执两次调用)sendContactEmail() / sendContactByReturnEmail()contact.service.ts
5POST/api/amlPortal/plan/portal/GetPlanList获取套餐列表(基础/jQ/加人/KYC)getAllPlanList()plan.service.ts
6POST/api/amlPortal/Order/portal/GetEditionList获取系统定义的 edition(所属行业)列表getEditionList()plan.service.ts
7POST/api/amlPortal/Order/getCategoryByTypes获取国家/地区列表(typeCodes: ['COUNTRY']getCountryList()plan.service.ts
8GET/api/identity/users/SearchUserByCodeAndType/{userCode}/true校验优惠码/代理商是否存在getAgentor()plan.service.ts
9POST/api/amlPortal/Order/portal/queryRenewableTenant查询可续费租户queryTenant()plan.service.ts
10POST/api/amlPortal/Order/portal/ExistsByOrganizationBRCI校验商业登记号(BR)/公司编号(CI)是否已存在checkBRCI()plan.service.ts
11POST/api/amlPortal/customer/portal/CheckEmailExists/{email}校验管理员邮箱是否已存在checkEmailExist()plan.service.ts
12POST/api/amlPortal/Order/portal/CreateOrder新租户下单createOrder()plan.service.ts
13POST/api/amlPortal/Order/portal/TenantRenewal租户续费下单tenantRenewal()plan.service.ts
14POST/api/amlPortal/Order/portal/PaymentWebhook支付完成回调(作为 notify_url 传给 QFPay,由其服务端回调,非前端直接调用plan4.component.ts
+
+ +
+

3. 鉴权(OAuth Token)

+

端点 1POST /connect/token?__tenant=2C

+ +

Body(x-www-form-urlencoded,硬编码凭证):

+ + + + + + + + + + + +
字段
grant_typepassword
response_typetoken
usernamecustomer1
password1qaz@WSX
scopeFX
client_idcustomer1_client
client_secret1qaz@WSX
+
+

⚠️ 用户名/密码/client secret 以明文硬编码在前端源码中(aml-query.service.ts:47-51),属于公开网站内置的固定访客凭证。

+
+
+ +
+

4. AML 查询模块(aml-query)

+

端点 2GET /api/aml/ConsumerPortal/GetPrice/{inputCode}

+ +
+

aml-query/pages/result/result.component.ts(评估报告页)当前使用本地 mock 数据 / Math.random() 生成风险评估结果,PDF 下载指向 assets/sample-report.pdfapiIntegration() 弹窗里的 api.credit-risk.example.com 等仅为示例文案,不是真实后端调用

+
+
+ +
+

5. 联系我们模块(contact)

+

调用方:contact/pages/contact/contact.component.ts 提交表单时触发。

+ +
+ +
+

6. 套餐与下单模块(plans / Order)

+

PlanService 集中了套餐查询、校验与下单接口。各端点的页面调用关系:

+ + + + + + + + + + + + + +
端点调用页面
getAllPlanList()端点 5plans.component.ts(进入套餐页 / 收到父窗口 postMessage 后)
getEditionList()端点 6plans.component.tsplan3.component.ts
getCountryList()端点 7plans.component.tsplan3.component.ts
getAgentor()端点 8plan3.component.ts(输入优惠码校验)
queryTenant()端点 9plan1.component.tsplan3.component.ts(续费选租户)
checkBRCI()端点 10plan3.component.ts
checkEmailExist()端点 11plan1.component.tsplan3.component.ts
createOrder()端点 12plan4.component.tstype === 'new'
tenantRenewal()端点 13plan4.component.tstype === 'renew'
+ +

关键请求体

+ + +

6.1 支付跳转(QFPay 外部网关,不经过 HttpService)

+

位置:plan4.component.tspayFn()。下单成功(createOrder/tenantRenewal 返回 code === 0)后:

+ +

主要参数:

+ + + + + + + + + + + + + + +
参数
appcodeenvironment.qfpay.appcode(固定值)
goods_name下单返回的 planName
out_trade_no下单返回的 orderCode
paysourceremotepay_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 服务端回调后端)
txamtapis.test ? 10 : planPrice * 100(单位:分)
txcurrcdHKD
signsha256(<排序后的参数串> + api_key)
+
+ +
+

7. 端点前缀分布

+ + + + + + + + +
前缀含义涉及端点
/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
+
+ +
+

8. 速查 · 典型流程(按调用次序)

+ +
+ + + +
+ + diff --git a/docs/justsolutionsWeb/api-calls.md b/docs/justsolutionsWeb/api-calls.md new file mode 100644 index 0000000..7a6d8ef --- /dev/null +++ b/docs/justsolutionsWeb/api-calls.md @@ -0,0 +1,151 @@ +# justsolutionsWeb — 后端 API 调用清单 + +> 本文档整理 `justsolutionsWeb`(Angular 16,`justsolutions-web` v1.2.0)前端对后端 API 的全部调用。 +> 统计基于源码静态分析(`src/app` 下所有 `*.ts`)。 + +## 1. 调用架构概览 + +所有业务 API 调用都收口在统一的 **`HttpService`**(`src/app/services/http.service.ts`): + +- **基地址**:请求时把相对路径拼到 `environment.apis.url` 之后,即 `environment.apis.url + request.url`。 +- **鉴权**:自动从 `localStorage` 或 `sessionStorage` 读取 `aml_access_token`,存在时加上请求头 `Authorization: Bearer `。 +- **请求模型**:统一使用 `Http.Request`(`src/app/models/http.ts`),字段含 `method` / `url` / `body` / `params` / `headers` / `responseType` 等。 + +> 例外:支付跳转直接 `location.href` 重定向到 QFPay 外部网关(见第 6 节),不经过 `HttpService`。 + +### 1.1 环境与基地址配置 + +| 环境文件 | `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 节)。 + +## 2. 调用清单(按端点汇总) + +| # | 方法 | 端点(相对 `apis.url`) | 用途 | 前端方法 | 所在文件 | +|---|------|--------------------------|------|----------|----------| +| 1 | POST | `/connect/token?__tenant=2C` | OAuth 取 access token | `getToken()` | `modules/aml-query/services/aml-query.service.ts` | +| 2 | GET | `/api/aml/ConsumerPortal/GetPrice/{inputCode}` | 查询消费者查询价格 | `getPrice()` | `modules/aml-query/services/aml-query.service.ts` | +| 3 | POST | `/api/amlPortal/customer/CreateFeedback` | 提交联系表单反馈 | `sendContactEmailAndReturnFeedback()` | `modules/contact/services/contact.service.ts` | +| 4 | POST | `/api/aml/emailQueue/CreateEmailQueue` | 发送通知邮件(管理员 + 用户回执两次调用) | `sendContactEmail()` / `sendContactByReturnEmail()` | `modules/contact/services/contact.service.ts` | +| 5 | POST | `/api/amlPortal/plan/portal/GetPlanList` | 获取套餐列表(基础/jQ/加人/KYC) | `getAllPlanList()` | `modules/plans/services/plan.service.ts` | +| 6 | POST | `/api/amlPortal/Order/portal/GetEditionList` | 获取系统定义的 edition(所属行业)列表 | `getEditionList()` | `modules/plans/services/plan.service.ts` | +| 7 | POST | `/api/amlPortal/Order/getCategoryByTypes` | 获取国家/地区列表(`typeCodes: ['COUNTRY']`) | `getCountryList()` | `modules/plans/services/plan.service.ts` | +| 8 | GET | `/api/identity/users/SearchUserByCodeAndType/{userCode}/true` | 校验优惠码/代理商是否存在 | `getAgentor()` | `modules/plans/services/plan.service.ts` | +| 9 | POST | `/api/amlPortal/Order/portal/queryRenewableTenant` | 查询可续费租户 | `queryTenant()` | `modules/plans/services/plan.service.ts` | +| 10 | POST | `/api/amlPortal/Order/portal/ExistsByOrganizationBRCI` | 校验商业登记号(BR)/公司编号(CI)是否已存在 | `checkBRCI()` | `modules/plans/services/plan.service.ts` | +| 11 | POST | `/api/amlPortal/customer/portal/CheckEmailExists/{email}` | 校验管理员邮箱是否已存在 | `checkEmailExist()` | `modules/plans/services/plan.service.ts` | +| 12 | POST | `/api/amlPortal/Order/portal/CreateOrder` | 新租户下单 | `createOrder()` | `modules/plans/services/plan.service.ts` | +| 13 | POST | `/api/amlPortal/Order/portal/TenantRenewal` | 租户续费下单 | `tenantRenewal()` | `modules/plans/services/plan.service.ts` | +| 14 | POST | `/api/amlPortal/Order/portal/PaymentWebhook` | 支付完成回调(作为 `notify_url` 传给 QFPay,由 QFPay 服务端回调,**非前端直接调用**) | — | `modules/plans/pages/tabs/plan4/plan4.component.ts` | + +## 3. 鉴权(OAuth Token) + +**端点 1** — `POST /connect/token?__tenant=2C` + +- 位置:`AmlQueryService`。在该服务 **构造函数中自动触发**:当 `localStorage` 无 `aml_access_token` 时,调用 `getToken()` 并把返回的 `access_token` 写入 `localStorage`。 +- 请求头:`Content-Type: application/x-www-form-urlencoded` +- Body(`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`),属于公开网站内置的固定访客凭证。 + +## 4. AML 查询模块(aml-query) + +**端点 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` 等仅为示例文案,**不是真实后端调用**。 + +## 5. 联系我们模块(contact) + +调用方:`contact/pages/contact/contact.component.ts` 提交表单时触发。 + +- **端点 3** `POST /api/amlPortal/customer/CreateFeedback`:保存反馈记录。 + - Body(`CreateFeedbackParam`):`companyName`、`customerName`、`customerEmail`、`typeOfQuery`、`message`、`userHost`、`destEmail`、`emailSubject`、`emailBody`。 +- **端点 4** `POST /api/aml/emailQueue/CreateEmailQueue`:进入邮件发送队列,被调用 **两次**: + - `sendContactEmail()` → 通知管理员(收件人 `environment.email.contact`)。 + - `sendContactByReturnEmail()` → 给提交者发送回执邮件(收件人为用户填写邮箱)。 + - Body(`CreateEmailQueueParam`):`subject`、`to`、`body`(HTML)、`businessType: 2100`、`businessId: ''`。 + +## 6. 套餐与下单模块(plans / Order) + +`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`。 + +### 6.1 支付跳转(QFPay 外部网关,不经过 HttpService) + +位置:`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=` + + 主要参数: + + | 参数 | 值 | + |------|-----| + | `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)` | + +## 7. 端点前缀分布 + +| 前缀 | 含义 | 涉及端点 | +|------|------|----------| +| `/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 | + +## 8. 速查(按调用次序的典型流程) + +- **应用启动**:`AmlQueryService` 构造 → 端点 1 取 token。 +- **AML 查询页加载**:端点 2 取价格。 +- **联系页提交**:端点 3 + 端点 4(×2)。 +- **套餐购买流程**: + 1. 进入套餐页:端点 5、6、7。 + 2. 填写租户/续费信息:端点 8(优惠码)、9(租户)、10(BR/CI)、11(邮箱)。 + 3. 确认下单:端点 12(新购)或 13(续费)。 + 4. 跳转 QFPay 支付 → 支付完成后 QFPay 回调端点 14。 diff --git a/docs/justsolutionsWebV2/plan-plus-api分析.html b/docs/justsolutionsWebV2/plan-plus-api分析.html new file mode 100644 index 0000000..0334689 --- /dev/null +++ b/docs/justsolutionsWebV2/plan-plus-api分析.html @@ -0,0 +1,546 @@ + + + + + +justsolutionsWebV2 · plans-plus 真實 API 對接分析 + + + +
+ +
+

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. 結論速覽

+

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 後端
+
    +
  • 前端封裝 public/js/api.jsapi.get('/editions') 實際請求 /api/editions。所有 plans-plus 端點均走此封裝。
  • +
  • 環境切換 server/config.jsmockEnabled = (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→後端映射」兩層。

+
+
+ + +
+

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)

+
    +
  • standardtag1Code==='B' && tag2Code!=='CPA'cpatag1Code==='B' && tag2Code==='CPA'
  • +
  • addons.user.unitPricetag1Code==='AdlU'planDetails[0].price(並記其 planId/planDetailId 備下單)
  • +
  • addons.kyc.unitPricetag1Code==='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。

+
+ + +

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. +
  3. currentSubscription 需重建:前端的 currentSubscription(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 TenantPropertyDto 直接字段,需由 TenantProperty + 其 Order + 正在生效的 plan 推導。可參考 GetCurrServiceInfoCurrServiceInfoDto.ActiveBPlans、EffectiveEndTime、QCountLeft 等)。
  4. +
+

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)。後端無直接端點。候選方案:

+
    +
  • A(推薦):復用 TenantRenewalPlanList 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。
  • +
  • B:後端新增 TenantTopup 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC,不延長有效期」。UpdateTenantProperty(admin)邏輯可借鑒(它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。
  • +
+ +

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

+
    +
  • 按郵箱查可續費租戶端點,返回聚合 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(後端確認後)加購閉環
+
+ + +
+

8. 待確認問題清單

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

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

+ +
+ +