AML/docs/justsolutionsWebV2/plan-plus-api分析.html

547 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>justsolutionsWebV2 · plans-plus 真實 API 對接分析</title>
<style>
:root {
--bg: #f6f8fa;
--card: #ffffff;
--text: #24292f;
--muted: #57606a;
--border: #d0d7de;
--accent: #0969da;
--accent-soft: #ddf4ff;
--code-bg: #f0f3f6;
--th-bg: #f0f3f6;
--row-alt: #fafbfc;
--warn-bg: #fff8c5;
--warn-border: #d4a72c;
--note-bg: #ddf4ff;
--note-border: #54aeff;
--gap-bg: #ffebe9;
--gap-border: #ff8182;
--ok-bg: #dafbe1;
--ok-border: #4ac26b;
--get: #1a7f37;
--post: #9a6700;
--new: #8250df;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "PingFang HK", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.65;
font-size: 15px;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 24px 80px; }
header.page {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 28px 32px; margin-bottom: 24px;
}
header.page h1 { margin: 0 0 10px; font-size: 26px; }
header.page p { margin: 4px 0; color: var(--muted); }
header.page .meta { font-size: 13px; }
.toc {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 18px 28px; margin-bottom: 28px;
}
.toc h2 { font-size: 13px; margin: 0 0 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.toc ol { margin: 0; padding-left: 20px; columns: 2; column-gap: 36px; }
.toc li { margin: 4px 0; break-inside: avoid; }
.toc a { color: var(--accent); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
section { margin-bottom: 36px; }
h2.sec {
font-size: 21px; border-bottom: 2px solid var(--border);
padding-bottom: 8px; margin: 0 0 18px; scroll-margin-top: 16px;
}
h3 { font-size: 17px; margin: 24px 0 10px; scroll-margin-top: 16px; }
h4 { font-size: 15px; margin: 16px 0 6px; color: var(--muted); }
p { margin: 10px 0; }
table {
border-collapse: collapse; width: 100%; margin: 14px 0;
font-size: 14px; background: var(--card);
border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
}
th, td { border: 1px solid var(--border); padding: 8px 11px; text-align: left; vertical-align: top; }
th { background: var(--th-bg); font-weight: 600; }
tr:nth-child(even) td { background: var(--row-alt); }
code {
background: var(--code-bg); padding: 1.5px 6px; border-radius: 5px;
font-family: "SF Mono", "JetBrains Mono", "Fira Code", Consolas, monospace; font-size: 12.5px;
}
pre {
background: #0d1117; color: #e6edf3; padding: 16px 18px;
border-radius: 8px; overflow-x: auto; font-size: 12.5px; line-height: 1.55;
}
pre code { background: none; padding: 0; color: inherit; font-size: 12.5px; }
.method { font-weight: 700; font-size: 11.5px; padding: 1px 7px; border-radius: 5px; color: #fff; display: inline-block; }
.method.get { background: var(--get); }
.method.post { background: var(--post); }
.method.new { background: var(--new); }
.tag { font-size: 11px; padding: 1px 7px; border-radius: 20px; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }
.callout { border-radius: 8px; padding: 12px 16px; margin: 14px 0; border: 1px solid; }
.callout p { margin: 4px 0; }
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
.callout.note { background: var(--note-bg); border-color: var(--note-border); }
.callout.gap { background: var(--gap-bg); border-color: var(--gap-border); }
.callout.ok { background: var(--ok-bg); border-color: var(--ok-border); }
.callout .lbl { font-weight: 700; }
.pill { display:inline-block; font-size:11px; font-weight:700; padding:1px 8px; border-radius:20px; }
.pill.done { background: var(--ok-bg); color:#1a7f37; border:1px solid var(--ok-border);}
.pill.partial { background: var(--warn-bg); color:#7a5c00; border:1px solid var(--warn-border);}
.pill.todo { background: var(--gap-bg); color:#cf222e; border:1px solid var(--gap-border);}
.pill.newreq { background:#fbefff; color:#8250df; border:1px solid #d8b9ff;}
.endpoint-head {
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
background: var(--code-bg); border:1px solid var(--border); border-radius:8px;
padding:10px 14px; margin: 6px 0 12px; font-family:"SF Mono",Consolas,monospace; font-size:13.5px;
}
.small { font-size: 13px; color: var(--muted); }
ul.tight { margin: 8px 0; padding-left: 22px; }
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; }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1>justsolutionsWebV2 · plans-plus 真實 API 對接分析</h1>
<p><code>public/plans-plus.html</code> 的單頁訂閱流程,從本地 mock 切換到 AML 後端(<code>iCON.Abp.AMLPortal</code>)真實 API。</p>
<p class="meta">範圍:<code>justsolutionsWebV2/public/js/plans-plus.js</code>(前端契約) · <code>justsolutionsWebV2/server/</code>BFF 代理層) · <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>(後端)</p>
<p class="meta">參照:旧站 <code>justsolutionsWeb</code><code>PlanService</code>(同一套後端端點) · 见 <code>docs/justsolutionsWeb/api-calls.md</code></p>
</header>
<div class="toc">
<h2>目錄</h2>
<ol>
<li><a href="#s1">1. 結論速覽</a></li>
<li><a href="#s2">2. 總體架構與調用鏈</a></li>
<li><a href="#s3">3. plans-plus 前端依賴的 API 契約</a></li>
<li><a href="#s4">4. 後端真實端點清單AMLPortal</a></li>
<li><a href="#s5">5. 端點逐一映射與落地方案</a></li>
<li><a href="#s5-1">5.1 GET /editions所屬行業</a></li>
<li><a href="#s5-2">5.2 GET /plans/catalog方案目錄</a></li>
<li><a href="#s5-3">5.3 GET /countries註冊地</a></li>
<li><a href="#s5-4">5.4 GET /agents/:code推薦人</a></li>
<li><a href="#s5-5">5.5 GET /tenants/lookup續費查租戶</a></li>
<li><a href="#s5-6">5.6 POST /subscribe下單</a></li>
<li><a href="#s5-7">5.7 POST /payments/create支付</a></li>
<li><a href="#s6">6. plans-plus 相對旧站的新增需求</a></li>
<li><a href="#s7">7. 實施建議與分期</a></li>
<li><a href="#s8">8. 待確認問題清單</a></li>
</ol>
</div>
<!-- ───────────── 1 ───────────── -->
<section id="s1">
<h2 class="sec">1. 結論速覽</h2>
<p>plans-plus 前端共依賴 <b>7 個業務端點</b>(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 <code>justsolutionsWeb</code> 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 <code>justsolutionsWebV2/server/routes/</code> 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 <code>countries.js</code> / <code>industries.js</code> 完全同一範式)。</p>
<table>
<thead><tr><th>前端端點</th><th>對應後端</th><th>復用程度</th><th>主要缺口</th></tr></thead>
<tbody>
<tr><td><code>GET /editions</code></td><td><code>Order/portal/GetEditionList</code></td><td><span class="pill partial">改造</span></td><td>多語言 edition 名稱(後端只有 displayName</td></tr>
<tr><td><code>GET /plans/catalog</code></td><td><code>plan/portal/GetPlanList</code></td><td><span class="pill partial">改造</span></td><td>需復刻旧站 filterPlan 拆分;<code>bestValue</code>/<code>note</code> 後端無</td></tr>
<tr><td><code>GET /countries</code></td><td><code>Order/getCategoryByTypes</code></td><td><span class="pill done">已實現</span></td><td>—(<code>routes/countries.js</code> 已可用)</td></tr>
<tr><td><code>GET /agents/:code</code></td><td><code>identity/users/SearchUserByCodeAndType</code></td><td><span class="pill partial">改造</span></td><td>響應 → <code>{code,name}</code> 轉換</td></tr>
<tr><td><code>GET /tenants/lookup</code></td><td><code>Order/portal/queryRenewableTenant</code></td><td><span class="pill todo">缺口大</span></td><td>後端按<b>租戶名</b>搜,前端按<b>郵箱</b>查;<code>currentSubscription</code> 需重建</td></tr>
<tr><td><code>POST /subscribe</code></td><td><code>CreateOrder</code> / <code>TenantRenewal</code></td><td><span class="pill partial">改造</span></td><td>需補 <code>planDetailId</code><code>topup</code> 類型無對應端點</td></tr>
<tr><td><code>POST /payments/create</code></td><td>QFPay 收銀台(旧站前端拼 URL</td><td><span class="pill todo">缺口大</span></td><td>需服務端簽名 + <code>PaymentWebhook</code> 回調</td></tr>
</tbody>
</table>
<div class="callout ok">
<p><span class="lbl">好消息:</span>後端 <code>CreateOrder</code> 已忽略前端傳入的 <code>AdminPassword</code>,改用 <code>appConfig.General.TenantAdminDefaultPassword</code><code>OrderService.cs:215,236</code>)。因此 plans-plus 去掉「管理員密碼」步驟<b>不構成阻塞</b>——後端會發重置密碼郵件給租戶管理員。</p>
</div>
<div class="callout gap">
<p><span class="lbl">兩個真正的硬缺口:</span></p>
<p><b>按郵箱查租戶</b><code>queryRenewableTenant</code> 只支持 <code>keyword</code>(按 <code>TenantName.Contains</code>。plans-plus 用郵箱查、且要支持「一郵箱多租戶」選擇——需新增後端端點,或在 BFF 全量拉取後按郵箱過濾(不可取)。</p>
<p><b>topup加購流程</b>plans-plus 新增的純加購(不含基礎方案、只買 增加用戶/KYC/jQuota旧站沒有後端亦無直接端點。需確認映射到 <code>TenantRenewal</code>(僅加值項 planList還是新增端點。</p>
</div>
</section>
<!-- ───────────── 2 ───────────── -->
<section id="s2">
<h2 class="sec">2. 總體架構與調用鏈</h2>
<p>justsolutionsWebV2 採「<b>BFFBackend-for-Frontend</b>」式:瀏覽器只調用本站 <code>/api/*</code>,由 Node/Express 依 <code>APP_ENV</code> 決定走 mock 還是真實後端,<b>前端代碼零改動</b>即可切換環境。</p>
<div class="flow">瀏覽器 plans-plus.js → window.api.get/post('/api/*') → Express server →
&nbsp;&nbsp;├─ APP_ENV=test ………… server/mock/routes.js內存假數據
&nbsp;&nbsp;└─ dev/stag/prod …… server/routes/*.jsBFF→ AuthService 取 token → AMLPortal 後端</div>
<ul class="tight">
<li><b>前端封裝</b> <code>public/js/api.js</code><code>api.get('/editions')</code> 實際請求 <code>/api/editions</code>。所有 plans-plus 端點均走此封裝。</li>
<li><b>環境切換</b> <code>server/config.js</code><code>mockEnabled = (APP_ENV==='test')</code>;真實環境需配 <code>API_BASE_URL</code> 與 OAuth 憑證(<code>.env.*</code>)。</li>
<li><b>掛載順序</b> <code>server/index.js</code>:真實環境先掛 <code>routes/*.js</code> 業務路由,再掛 proxy 兜底mock 環境掛 <code>mock/routes.js</code><b>新增 plans-plus 的真實路由就照此追加 <code>app.use(apiPrefix, createXxxRouter(config))</code></b></li>
<li><b>鑑權</b> <code>server/services/auth.js</code>OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的 <code>customer1</code> 訪客憑證,現改為 <code>.env</code> 配置。BFF 每次調後端用 <code>createAuthConfig()</code><code>Authorization: Bearer</code></li>
</ul>
<div class="callout note">
<p><span class="lbl">關鍵差異(與旧站):</span>旧站 Angular 直接從瀏覽器調 <code>https://api-aml.iconsz.com/api/amlPortal/*</code>token 存 localStorage。V2 改為<b>瀏覽器不直接接觸後端</b>,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。</p>
</div>
</section>
<!-- ───────────── 3 ───────────── -->
<section id="s3">
<h2 class="sec">3. plans-plus 前端依賴的 API 契約</h2>
<p>以下是 <code>plans-plus.js</code> 實際讀寫的字段(即 BFF <b>必須產出/接受</b>的契約,目前由 <code>mock/routes.js</code> 滿足)。真實對接時 BFF 輸出必須與此<b>逐字段一致</b>,否則前端渲染/計價會出錯。</p>
<h3>3.1 載入期(頁面初始化並發拉取)</h3>
<p><code>loadAll()</code> 並發請求 <code>/editions</code><code>/plans/catalog</code><code>/countries</code>,三者均以 <code>{ success:true, data:… }</code> 為成功標誌。</p>
<pre><code>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 }</code></pre>
<h3>3.2 交互期(按需)</h3>
<pre><code>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 } } }</code></pre>
<h3>3.3 提交期(兩步串聯)</h3>
<p><code>submit()</code><code>POST /subscribe</code><code>orderId</code>,再 <code>POST /payments/create</code><code>redirectUrl</code> 跳轉支付。</p>
<pre><code>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 } }</code></pre>
<div class="callout warn">
<p><span class="lbl">注意:</span><code>/subscribe</code> 的 body 不含 <code>planDetailId</code>,也不含各加值項(增加用戶/KYC/jQuota對應的 <code>planId</code>/<code>planDetailId</code>。而後端 <code>CreateOrder</code>/<code>TenantRenewal</code><code>PlanList</code> 每項都<b>必須</b>同時帶 <code>PlanId</code>+<code>PlanDetailId</code><code>OrderService.cs:176</code> 聯合校驗)。⇒ BFF 需在服務端依 <code>/plans/catalog</code> 結果<b>反查補齊</b> planDetailId見 5.6)。</p>
</div>
</section>
<!-- ───────────── 4 ───────────── -->
<section id="s4">
<h2 class="sec">4. 後端真實端點清單AMLPortal</h2>
<p>以下端點均在 <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>,並已被旧站 <code>justsolutionsWeb/PlanService</code> 使用驗證過。前綴 <code>/api/amlPortal/*</code>,鑑權走 <code>[AbpAutoAuth("Portal")]</code>(門戶訪客 token</p>
<table>
<thead><tr><th>#</th><th>方法 / 路由</th><th>用途</th><th>Controller</th></tr></thead>
<tbody>
<tr><td>1</td><td><span class="method post">POST</span> <code>/api/amlPortal/plan/portal/GetPlanList</code></td><td>取方案列表B/jQ/j/AdlU/KYC</td><td>PlanController:37</td></tr>
<tr><td>2</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/GetEditionList</code></td><td>取 edition所屬行業+ jQSeparatedEditions</td><td>OrderController:136</td></tr>
<tr><td>3</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/getCategoryByTypes</code></td><td>取國家列表(<code>typeCodes:['COUNTRY']</code></td><td>OrderController:184</td></tr>
<tr><td>4</td><td><span class="method get">GET</span> <code>/api/identity/users/SearchUserByCodeAndType/{code}/true</code></td><td>校驗推薦人/代理 code</td><td>Identity非 AMLPortal</td></tr>
<tr><td>5</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/queryRenewableTenant</code></td><td>查可續費租戶(按 <code>keyword</code>=租戶名)</td><td>OrderController:308</td></tr>
<tr><td>6</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/ExistsByOrganizationBRCI</code></td><td>校驗 BR/CI 是否已存在</td><td>OrderController:150</td></tr>
<tr><td>7</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/portal/CheckEmailExists/{email}</code></td><td>校驗管理員郵箱是否已用</td><td>CustomerController:121</td></tr>
<tr><td>8</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/CreateOrder</code></td><td>新租戶下單</td><td>OrderController:53</td></tr>
<tr><td>9</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/TenantRenewal</code></td><td>租戶續費下單</td><td>OrderController:321</td></tr>
<tr><td>10</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/PaymentWebhook</code></td><td>支付回調QFPay 服務端調,非前端)</td><td>OrderController:163</td></tr>
</tbody>
</table>
<p class="small">DTO 位置:<code>Application.Contracts/PlanAppLayer/*</code>GetPlanListParam、PlanDto、PlanDetailDto<code>Application.Contracts/OrderAppLayer/*</code>CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto</p>
</section>
<!-- ───────────── 5 ───────────── -->
<section id="s5">
<h2 class="sec">5. 端點逐一映射與落地方案</h2>
<p class="small">每節:前端契約 → 對應後端 → 字段映射 → BFF 轉換要點 → 差異/缺口。新增的 BFF 文件建議放 <code>server/routes/</code>,與 <code>countries.js</code> 同範式(<code>createAuthConfig()</code> + axios + 字段映射 + <code>{success,data}</code> 包裝)。</p>
<!-- 5.1 -->
<h3 id="s5-1">5.1 GET /editions所屬行業 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/editions &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/GetEditionList</div>
<p>後端返回 <code>{ code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }</code>。前端要 <code>editionList:[{id, displayName, nameCN, nameJP}]</code><code>jQSeparatedEditions.editionIds</code></p>
<table>
<thead><tr><th>前端字段</th><th>後端來源</th><th>說明</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td><code>editionList[].id</code>Guid</td><td>直接映射;後續作 <code>OrganizationReference</code></td></tr>
<tr><td><code>displayName</code></td><td><code>editionList[].displayName</code></td><td>英文名</td></tr>
<tr><td><code>nameCN</code> / <code>nameJP</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr>
<tr><td><code>jQSeparatedEditions.editionIds</code></td><td>同名字段</td><td>驅動 CPA/標準方案集切換(<code>state.isCpa</code></td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">缺口:</span>edition 多語言名稱。後端 <code>GetEditionList</code> 只有 <code>displayName</code>,旧站也僅用 displayName。plans-plus 的 <code>editionName()</code> 在 tc/jp 下會優先取 <code>nameCN</code>/<code>nameJP</code>,缺失時已能 fallback 回 displayName ⇒ <b>不阻塞,但中日文會顯示英文</b></p>
<p><span class="lbl">建議:</span>BFF 維護一份 <code>editionId/displayName → {nameCN,nameJP}</code> 映射表(與 <code>industries.js</code> 現有靜態行業翻譯同思路),或後端在 edition 上補多語言字段。</p>
</div>
<div class="callout note">
<p><span class="lbl">旧站行為(可選沿用):</span><code>getEditionList()</code><b>過濾掉 <code>Standard</code></b>、並把 <code>Others</code> 排到列表末尾,同時把 editionIds <code>toLowerCase()</code>。plans-plus 目前未做此整理;若要與旧站一致,這段邏輯應放 BFF。</p>
</div>
<!-- 5.2 -->
<h3 id="s5-2">5.2 GET /plans/catalog方案目錄 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/plans/catalog &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/plan/portal/GetPlanList</div>
<p>BFF 以旧站同款請求體調 <code>GetPlanList</code>,再復刻旧站 <code>filterPlan()</code> 把扁平 <code>items</code><code>tag1Code</code> 拆成 5 組,組裝成 <code>{standard, cpa, addons}</code></p>
<h4>請求體(沿用 PlanService.getAllPlanList</h4>
<pre><code>{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false,
tag1List:['B','jQ','j','AdlU','KYC'], tag2List, tag3List, agentUserId }</code></pre>
<h4>後端 items → 前端 Plan 字段映射</h4>
<table>
<thead><tr><th>前端 Plan</th><th>後端來源item / planDetails[0]</th><th>備註</th></tr></thead>
<tbody>
<tr><td><code>planId</code></td><td><code>item.id</code></td><td></td></tr>
<tr><td><code>planDetailId</code> ⚠️</td><td><code>item.planDetails[0].id</code></td><td><b>前端契約現缺此字段</b>,但下單必需 ⇒ BFF 須補進 Plan提交時回填見 5.6</td></tr>
<tr><td><code>tag2Code</code></td><td><code>item.tag2Code</code></td><td>Std/P2G/CPA/Pre…CPA 判定用</td></tr>
<tr><td><code>nameCN</code>/<code>nameEN</code></td><td><code>item.nameCN</code>/<code>nameEN</code></td><td>旧站會截掉「(…」後綴,可沿用</td></tr>
<tr><td><code>nameJP</code></td><td><b>後端無</b></td><td>缺口fallback EN</td></tr>
<tr><td><code>periodMonths</code></td><td><code>planDetails[0].periodMonths</code></td><td></td></tr>
<tr><td><code>price</code>/<code>originalPrice</code></td><td><code>planDetails[0].price</code>/<code>originalPrice</code></td><td>劃線價用 originalPrice</td></tr>
<tr><td><code>qCount</code></td><td><code>planDetails[0].qCount</code></td><td><code>-1</code> 表無限</td></tr>
<tr><td><code>userCountLimit</code></td><td><code>planDetails[0].userCountLimit</code></td><td></td></tr>
<tr><td><code>bestValue</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr>
<tr><td><code>noteCN/EN/JP</code></td><td><code>item.description</code>?(無多語言)</td><td>缺口,見下</td></tr>
</tbody>
</table>
<h4>addons 拆分(按 tag1Code</h4>
<ul class="tight">
<li><code>standard</code><code>tag1Code==='B' && tag2Code!=='CPA'</code><code>cpa</code><code>tag1Code==='B' && tag2Code==='CPA'</code></li>
<li><code>addons.user.unitPrice</code><code>tag1Code==='AdlU'</code><code>planDetails[0].price</code>(並記其 planId/planDetailId 備下單)</li>
<li><code>addons.kyc.unitPrice</code><code>tag1Code==='KYC'</code> 同上</li>
<li><code>addons.jquota.packages[]</code><code>tag1Code==='jQ'||'j'</code>:每項 <code>{ id:planId, jq:qCount, price:planDetails[0].price, name* }</code>,並各自記 planDetailId</li>
</ul>
<div class="callout gap">
<p><span class="lbl">缺口:</span><code>bestValue</code>(最超值標記)、<code>note*</code>(賣點文案)後端 <code>PlanDto</code>/<code>PlanDetailDto</code> 無對應字段。建議:① BFF 配置(按 systemCode 標 bestValue / 文案);或 ② 後端在 Plan 增 <code>bestValue</code> 與多語言 note。短期可在 BFF 寫死映射,不阻塞。</p>
</div>
<div class="callout note">
<p><span class="lbl">提示:</span>plans-plus 的 jQuota 改為「<b>選配套</b>」(<code>jquotaPackageId</code>),不再是旧站的「按數量」。後端 jQ 計劃本就是離散 plan天然契合——每個 jQ plan 即一個 package。</p>
</div>
<!-- 5.3 -->
<h3 id="s5-3">5.3 GET /countries註冊地 <span class="pill done">已實現</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/countries &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/getCategoryByTypes</div>
<p><code>server/routes/countries.js</code> <b>已實現並可直接用</b>:取 <code>typeCodes:['COUNTRY']</code>,從 <code>categoryTranslations</code> 提 zh-hk/zh-cn/ja/en-us輸出 <code>{code, name, nameTC, nameSC, nameJP, phoneCode}</code>,正好滿足 plans-plus 的 <code>countryDisplay()</code><code>phoneCode</code> 自動填充。<b>無需改動。</b></p>
<!-- 5.4 -->
<h3 id="s5-4">5.4 GET /agents/:code推薦人代理 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/agents/:code &nbsp;&nbsp; <span class="method get">GET</span> /api/identity/users/SearchUserByCodeAndType/{code}/true</div>
<p>前端要 <code>{ success, found, data:{code, name} }</code>。後端Identity返回匹配到的用戶含 name/userName/extraProperties.code 等。BFF 取首條 → <code>found:true, data:{code, name}</code>;無匹配 → <code>found:false, data:null</code></p>
<table>
<thead><tr><th>前端</th><th>後端用戶字段</th></tr></thead>
<tbody>
<tr><td><code>data.code</code></td><td>用戶 codeagent code</td></tr>
<tr><td><code>data.name</code></td><td><code>name</code> / <code>surname</code> / <code>userName</code></td></tr>
<tr><td>(下單用)<code>agentorId</code></td><td>用戶 <code>id</code>Guid→ 提交時作 <code>CreateOrderParam.AgentorId</code></td></tr>
</tbody>
</table>
<div class="callout warn">
<p><span class="lbl">關鍵:</span>前端只展示 <code>name</code>,但下單需要 agent 的 <b>Guid id</b><code>CreateOrderParam.AgentorId</code>。BFF 應在 <code>/agents/:code</code> 響應內<b>順帶緩存 code→id</b>(或前端 <code>data</code> 內含 id否則 <code>/subscribe</code> 階段需再查一次。建議 <code>data</code> 增隱藏字段 <code>agentorId</code></p>
<p>備註AMLPortal 另有 <code>Agentor/portal/GetAgentorByCode</code>,但已標【廢棄】;旧站用的是 Identity 的 <code>SearchUserByCodeAndType</code>,沿用之。</p>
</div>
<!-- 5.5 -->
<h3 id="s5-5">5.5 GET /tenants/lookup續費查租戶 <span class="pill todo">缺口大</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/tenants/lookup?email=&amp;name= &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenant</div>
<p>前端按<b>郵箱</b>查租戶支持「none / unique / multiple」三態命中後帶出 <code>currentSubscription</code> 預填續費表單。但後端 <code>queryRenewableTenant</code> 只接受 <code>{keyword}</code> 且按 <b><code>TenantName.Contains(keyword)</code></b> 搜索(<code>OrderService.cs:2363</code>),返回 <code>List&lt;TenantPropertyDto&gt;</code>(內部已把 admin 郵箱填入 <code>TenantAdminEmail</code>)。</p>
<h4>兩個不匹配點</h4>
<ol class="tight" style="padding-left:20px">
<li><b>查詢維度不符</b>:前端給郵箱,後端按名字搜。<br>
短期 BFF 變通:以空/寬 keyword 拉取後在服務端按 <code>TenantAdminEmail===email</code> 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。<br>
正解:<b>後端新增「按管理員郵箱查可續費租戶」端點</b>(返回同 <code>TenantPropertyDto</code> 結構)。</li>
<li><b>currentSubscription 需重建</b>:前端的 <code>currentSubscription</code>planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons並非 <code>TenantPropertyDto</code> 直接字段,需由 TenantProperty + 其 <code>Order</code> + 正在生效的 plan 推導。可參考 <code>GetCurrServiceInfo</code><code>CurrServiceInfoDto.ActiveBPlans</code>、EffectiveEndTime、QCountLeft 等)。</li>
</ol>
<h4>TenantPropertyDto → 前端 Tenant 映射(可得部分)</h4>
<table>
<thead><tr><th>前端</th><th>TenantPropertyDto</th><th>備註</th></tr></thead>
<tbody>
<tr><td><code>tenantId</code></td><td><code>TargetTenantID</code></td><td></td></tr>
<tr><td><code>tenantName</code></td><td><code>TenantName</code></td><td></td></tr>
<tr><td><code>editionId</code>/<code>editionName</code></td><td><code>EditionId</code>/<code>EditionName</code></td><td></td></tr>
<tr><td><code>currentSubscription.expiryDate</code></td><td><code>EffectiveEndTime</code></td><td>過期判定(前端 daysUntil</td></tr>
<tr><td><code>currentSubscription.startDate</code></td><td><code>EffectiveStartTime</code></td><td>未過期續費 → 默認接續此日</td></tr>
<tr><td><code>currentSubscription.qCount</code>/<code>usedQuota</code></td><td><code>QCountPurchase</code> / <code>QCountPurchaseQCountLeft</code></td><td>已用 = 購買 剩餘</td></tr>
<tr><td><code>currentSubscription.userCountLimit</code></td><td><code>UserCountLimit</code></td><td></td></tr>
<tr><td><code>currentSubscription.addons.kyc</code></td><td><code>EnableKYC</code></td><td>topup 時鎖「已購買」</td></tr>
<tr><td><code>currentSubscription.planId</code>/<code>name*</code>/<code>price</code></td><td>需經 <code>Order</code>/<code>OrderDetail</code> 反查</td><td><b>推導</b>,續費續價(沿用上期 price依賴此</td></tr>
<tr><td><code>currentSubscription.addons.users</code>/<code>jquotaPackageId</code></td><td>需經訂單明細反查</td><td><b>推導</b></td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">建議後端改動:</span>新增端點 <code>queryRenewableTenantByEmail(email[, tenantName])</code>,直接返回 plans-plus 所需的「租戶 + currentSubscription含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查。</p>
</div>
<!-- 5.6 -->
<h3 id="s5-6">5.6 POST /subscribe下單 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/subscribe &nbsp;&nbsp; 按 type 分流</div>
<p>BFF 依 <code>body.type</code> 分流到不同後端端點並把前端的「人類友好」payload 翻成後端 DTO核心是<b>組裝 <code>PlanList</code></b>(基礎方案 + 各加值項,每項補 <code>planDetailId</code>)。</p>
<h4>① type='new' → CreateOrder</h4>
<table>
<thead><tr><th>CreateOrderParam</th><th>前端 payload</th><th>備註</th></tr></thead>
<tbody>
<tr><td><code>PlanList[]</code></td><td>plan + addons</td><td>見下「PlanList 組裝」</td></tr>
<tr><td><code>TenantName</code></td><td><code>company</code></td><td>必填;後端校驗重名</td></tr>
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td><td>必填;後端再校驗未註冊</td></tr>
<tr><td><code>Jurisdiction</code></td><td><code>jurisdiction</code></td><td>必填</td></tr>
<tr><td><code>OrganizationReference</code></td><td><code>edition</code>Guid</td><td>必填;校驗 edition 存在</td></tr>
<tr><td><code>OrganizationBR</code> / <code>OrganizationCI</code></td><td><code>br</code> / —</td><td><b>BR 或 CI 至少一個</b>OrderService.cs:154plans-plus 僅收 BR 且標可選 ⇒ 缺口</td></tr>
<tr><td><code>EffectiveStartTime</code></td><td><code>startDate</code></td><td></td></tr>
<tr><td><code>ContactPerson</code></td><td><code>contact</code></td><td></td></tr>
<tr><td><code>CompanyAddress</code></td><td><code>address</code></td><td>可選</td></tr>
<tr><td><code>Phone</code></td><td><code>phoneCode + phone</code></td><td>可選BFF 拼接</td></tr>
<tr><td><code>AgentorId</code></td><td><code>agentCode</code> 解析的 Guid</td><td>見 5.4</td></tr>
<tr><td><code>AdminPassword</code></td><td></td><td><b>忽略</b>(後端用默認密碼,發重置郵件)</td></tr>
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td></td><td>固定 <code>false</code></td></tr>
</tbody>
</table>
<h4>② type='renew' → TenantRenewal</h4>
<table>
<thead><tr><th>TenantRenewalParam</th><th>前端 payload</th></tr></thead>
<tbody>
<tr><td><code>TargetTenantID</code></td><td><code>tenantId</code></td></tr>
<tr><td><code>PlanList[]</code></td><td>plan + addons同下組裝</td></tr>
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td></tr>
<tr><td><code>AgentorId</code></td><td>空 → 延續原代理</td></tr>
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>固定 <code>false</code></td></tr>
</tbody>
</table>
<h4>③ type='topup' → <span class="pill newreq">新增需求 · 無對應端點</span></h4>
<p>純加購(無基礎方案,只買 增加用戶/KYC/jQuota。後端無直接端點。候選方案</p>
<ul class="tight">
<li><b>A推薦</b>:復用 <code>TenantRenewal</code><code>PlanList</code> 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。</li>
<li><b>B</b>:後端新增 <code>TenantTopup</code> 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC不延長有效期」。<code>UpdateTenantProperty</code>admin邏輯可借鑒它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。</li>
</ul>
<h4>PlanList 組裝(三種 type 共用,核心難點)</h4>
<pre><code>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 })
// KYCPCS = 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 })</code></pre>
<div class="callout warn">
<p><span class="lbl">planDetailId 從哪來:</span>前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 <code>/plans/catalog</code> 的服務端版本(含 planDetailId<code>plan.planId</code> 與加值項類型<b>反查補齊</b>。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),<code>/subscribe</code> 時直接查表。</p>
</div>
<div class="callout note">
<p><span class="lbl">後端返回OperationDto</span>下單成功返回含 <code>orderCode</code><code>planName</code><code>paymentStatus</code><code>txamt</code> 等(旧站 <code>plan4.component</code> 用其拼 QFPay URL。BFF 應把其中 <code>orderId/orderCode</code> 回給前端的 <code>data.orderId</code>,並<b>暫存 orderCode/amount</b> 供下一步支付用。</p>
</div>
<!-- 5.7 -->
<h3 id="s5-7">5.7 POST /payments/create支付 <span class="pill todo">缺口大</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/payments/create &nbsp;&nbsp; QFPay 收銀台(服務端拼簽名 URL</div>
<p>旧站<b>沒有</b>獨立的 create-payment 後端端點:<code>plan4.component.payFn()</code> 在前端直接用 <code>orderCode</code>/<code>planName</code>/<code>txamt</code> 拼 QFPay checkstand URL 並 <code>sha256</code> 簽名後 <code>location.href</code> 跳轉。plans-plus 改成調 <code>/payments/create</code><code>redirectUrl</code><b>這段簽名邏輯應移到 BFF 服務端</b>API key 不可暴露前端)。</p>
<h4>BFF 實現要點(對標旧站 QFPay 參數)</h4>
<table>
<thead><tr><th>QFPay 參數</th><th>來源</th></tr></thead>
<tbody>
<tr><td><code>appcode</code></td><td>環境配置(<code>.env</code></td></tr>
<tr><td><code>goods_name</code></td><td>下單返回 <code>planName</code> / 前端 <code>planName</code></td></tr>
<tr><td><code>out_trade_no</code></td><td>下單返回 <code>orderCode</code></td></tr>
<tr><td><code>txamt</code></td><td><code>apis.test ? 10 : amount*100</code>(單位:分)</td></tr>
<tr><td><code>txcurrcd</code></td><td><code>HKD</code></td></tr>
<tr><td><code>return_url</code>/<code>failed_url</code></td><td>V2 的 <code>pay-return</code> / 失敗頁</td></tr>
<tr><td><code>notify_url</code></td><td><code>{API_BASE_URL}/api/amlPortal/Order/portal/PaymentWebhook</code>(端點 10QFPay 服務端回調後端)</td></tr>
<tr><td><code>sign</code></td><td><code>sha256(排序參數串 + api_key)</code><b>服務端計算</b></td></tr>
</tbody>
</table>
<p>BFF 返回 <code>{ success, data:{ redirectUrl:&lt;QFPay checkstand URL&gt; } }</code>,前端 <code>location.href</code> 跳轉。支付結果由 QFPay 異步回調後端 <code>PaymentWebhook</code> 落單(權威來源);前端返回頁可輪詢訂單狀態。</p>
<div class="callout note">
<p><span class="lbl">mock 對照:</span>當前 mock 用 <code>/payments/create</code> + <code>/payments/:pid</code>(輪詢)+ <code>/payments/:pid/webhook</code> 模擬「下單→收銀台→webhook→返回頁輪詢」全鏈路。真實環境收銀台與 webhook 由 QFPay 提供BFF 只需:① 拼簽名 URL② 提供查單(透傳後端訂單狀態)給返回頁輪詢。</p>
</div>
<div class="callout warn">
<p><span class="lbl">免支付分支:</span>旧站當訂單 <code>paymentStatus===200</code><code>txamt===0</code>(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(<code>redirectUrl</code> 直接給成功頁)。</p>
</div>
</section>
<!-- ───────────── 6 ───────────── -->
<section id="s6">
<h2 class="sec">6. plans-plus 相對旧站的新增需求</h2>
<p>以下是 plans-plus 單頁流程相對旧站 4 步嚮導<b>新增或改變</b>的點,逐一標注對接影響。</p>
<table>
<thead><tr><th>#</th><th>新增/變更</th><th>對接影響</th><th>狀態</th></tr></thead>
<tbody>
<tr><td>1</td><td><b>topup 加購類型</b>(無基礎方案,只買加值項)</td><td>後端無端點,需確認映射 TenantRenewal 或新增端點</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td>2</td><td><b>按郵箱查租戶</b> + 一郵箱多租戶選擇</td><td>後端 queryRenewableTenant 按名字搜,需新增按郵箱端點</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td>3</td><td><b>jQuota 改為選配套</b>package非按量</td><td>BFF 把 jQ plan 映成 packages 即可</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>4</td><td><b>KYC「已購買」鎖定</b>topup 已有則禁買)</td><td>依賴租戶 <code>EnableKYC</code>BFF 帶出即可</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>5</td><td><b>續費續價</b>(沿用上期 price顯示續費折扣</td><td>依賴 currentSubscription.price需訂單反查</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td>6</td><td><b>去掉管理員密碼步驟</b></td><td>後端已忽略 AdminPassword發重置郵件</td><td><span class="pill done">無影響</span></td></tr>
<tr><td>7</td><td><b>bestValue / 賣點 note</b>(方案卡片標記與文案)</td><td>後端 Plan 無此字段BFF 配置或後端補</td><td><span class="pill partial">BFF/後端</span></td></tr>
<tr><td>8</td><td><b>edition 多語言名稱</b>(中/日)</td><td>後端只有 displayNameBFF 補映射</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>9</td><td><b>BR 標為可選</b>UI</td><td>後端要求 BR 或 CI 至少一個;需 UI 補 CI 或設必填</td><td><span class="pill todo">需對齊</span></td></tr>
<tr><td>10</td><td><b>支付 create 端點化</b>(前端拿 redirectUrl</td><td>QFPay 簽名邏輯移到 BFF 服務端</td><td><span class="pill todo">BFF</span></td></tr>
</tbody>
</table>
</section>
<!-- ───────────── 7 ───────────── -->
<section id="s7">
<h2 class="sec">7. 實施建議與分期</h2>
<h3>7.1 新增 / 改動文件清單justsolutionsWebV2/server</h3>
<table>
<thead><tr><th>文件</th><th>職責</th><th>後端依賴</th></tr></thead>
<tbody>
<tr><td><code>routes/editions.js</code>(新)</td><td><code>GET /editions</code> + 多語言/過濾整理</td><td>GetEditionList</td></tr>
<tr><td><code>routes/plans.js</code>(新)</td><td><code>GET /plans/catalog</code> + filterPlan 拆分 + 緩存 planDetailId</td><td>GetPlanList</td></tr>
<tr><td><code>routes/countries.js</code></td><td>已存在,直接掛載</td><td>getCategoryByTypes</td></tr>
<tr><td><code>routes/agents.js</code>(新)</td><td><code>GET /agents/:code</code> → {code,name,agentorId}</td><td>SearchUserByCodeAndType</td></tr>
<tr><td><code>routes/tenants.js</code>(新)</td><td><code>GET /tenants/lookup</code>(依賴後端新端點)</td><td>queryRenewableTenant(ByEmail)</td></tr>
<tr><td><code>routes/subscribe.js</code>(新)</td><td><code>POST /subscribe</code> 分流 + PlanList 組裝</td><td>CreateOrder / TenantRenewal / (Topup)</td></tr>
<tr><td><code>routes/payments.js</code>(新)</td><td><code>POST /payments/create</code> 拼 QFPay 簽名 URL + 查單</td><td>PaymentWebhook回調</td></tr>
<tr><td><code>index.js</code></td><td>在真實環境段 <code>app.use(apiPrefix, createXxxRouter(config))</code> 追加各路由</td><td></td></tr>
<tr><td><code>.env.*</code> / <code>config.js</code></td><td>補 QFPay <code>appcode/api_key</code>、edition/plan 文案映射等</td><td></td></tr>
</tbody>
</table>
<h3>7.2 可能的後端改動(建議與後端團隊確認)</h3>
<ul class="tight">
<li><b>按郵箱查可續費租戶</b>端點,返回聚合 currentSubscription含 planId / 上期 price / 已購加值項)。</li>
<li><b>topup 語義</b>明確「無基礎方案的加購」如何下單TenantRenewal 是否允許 / 新增 Topup 端點)。</li>
<li>可選Plan 增 <code>bestValue</code> 與多語言 <code>note</code>edition 增多語言名稱。</li>
</ul>
<h3>7.3 建議分期</h3>
<table>
<thead><tr><th>階段</th><th>內容</th><th>可獨立交付</th></tr></thead>
<tbody>
<tr><td>P1 · 只讀目錄</td><td>editions + plans/catalog + countries已就緒+ agents</td><td>頁面可正常渲染方案/行業/國家,校驗推薦人</td></tr>
<tr><td>P2 · 新購下單</td><td>subscribe(new) + payments/createQFPay</td><td>新租戶完整下單支付閉環</td></tr>
<tr><td>P3 · 續費</td><td>tenants/lookup後端新端點+ subscribe(renew)</td><td>續費閉環,含續價</td></tr>
<tr><td>P4 · 加購</td><td>topup後端確認後</td><td>加購閉環</td></tr>
</tbody>
</table>
</section>
<!-- ───────────── 8 ───────────── -->
<section id="s8">
<h2 class="sec">8. 待確認問題清單</h2>
<ol class="tight" style="padding-left:20px">
<li><b>租戶查詢:</b>後端能否新增「按管理員郵箱查可續費租戶」?返回是否能直接帶 currentSubscriptionplanId / 上期價 / 已購加值項)?</li>
<li><b>topup</b>純加購走 TenantRenewalPlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?還是需新端點?</li>
<li><b>BR/CI</b>plans-plus 把 BR 標為可選,但後端要求 BR 或 CI 至少一個——UI 是否補 CI 字段,或把 BR 設為必填?</li>
<li><b>bestValue / note / edition 多語言:</b>由 BFF 配置維護,還是後端在數據上補字段?</li>
<li><b>QFPay</b>V2 是否沿用旧站同一 <code>appcode</code>/<code>api_key</code>/收銀台域名簽名算法是否一致sha256 排序串 + key</li>
<li><b>0 元 / P2G</b>免支付分支的判定(<code>paymentStatus===200</code><code>txamt===0</code>)是否照搬?</li>
<li><b>agentorId</b><code>/agents/:code</code> 響應是否可附帶 agent 的 Guid id避免下單時二次查詢</li>
<li><b>鑑權憑證:</b>V2 BFF 用的門戶訪客 OAuth 帳號(<code>.env</code>)權限是否覆蓋 CreateOrder / queryRenewableTenant<code>[AbpAutoAuth("Portal")]</code>,應可)?</li>
</ol>
</section>
<p class="small" style="text-align:center;margin-top:48px;color:var(--muted)">
本分析基於源碼靜態閱讀:<code>plans-plus.js</code> / <code>server/*</code> / <code>AMLPortal</code> Controllers 與 DTO / 旧站 <code>PlanService</code>。涉及後端行為續費續價、topup 語義)以實際接口為準。
</p>
</div>
</body>
</html>