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

701 lines
77 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; }
.updated { font-size:12px; color:#8250df; font-weight:700; }
</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並配合本地 <code>docker-compose-local-dev</code> 環境落地(<b>支付除外</b>)。</p>
<p class="meta">範圍:<code>justsolutionsWebV2/public/js/plans-plus.js</code>(前端契約) · <code>justsolutionsWebV2/server/</code>BFF 代理層 + mock · <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>(後端) · <code>AML_Backend/docker-compose-local-dev</code>(本地棧)</p>
<p class="meta">參照:旧站 <code>justsolutionsWeb</code><code>PlanService</code>(同一套後端端點) · mock 契約權威來源 <code>server/mock/plans-plus.js</code></p>
<p class="meta updated">本次更新2026-07修正 BR/CI 校驗現狀、補入 <b>單次查詢</b>/<b>線下對接</b>/<b>agent tiers</b>/<b>KYC 租賃</b>/<b>主體類型</b> 等前端新契約,並新增「本地開發環境對接」與「本地種子數據缺口」章節。</p>
<p class="meta updated">補充更新2026-07釐清「<b>聯絡我們/聯絡推薦人</b>」語義——<b>不創建任何訂單/租戶/線索</b>,而是把當前<b>訂單摘要</b><b>既有 contact us API<code>POST /api/contact</code> → 後端 <code>customer/CreateFeedback</code></b>發送給平台管理員若已填有效推薦人代碼則改送該推薦人其郵箱。原「線下對接下單」定位5.8)已據此改寫。</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推薦人 + tiers</a></li>
<li><a href="#s5-5">5.5 GET /tenants/lookup按郵箱查租戶</a></li>
<li><a href="#s5-6">5.6 單次查詢 single-query</a></li>
<li><a href="#s5-7">5.7 POST /subscribe下單</a></li>
<li><a href="#s5-8">5.8 聯絡我們/推薦人(發訂單摘要·不下單)</a></li>
<li><a href="#s5-9">5.9 POST /payments/create支付·暫緩</a></li>
<li><a href="#s6">6. 本地開發環境對接docker-compose-local-dev</a></li>
<li><a href="#s7">7. plans-plus 相對旧站的新增需求</a></li>
<li><a href="#s8">8. 實施建議與分期</a></li>
<li><a href="#s9">9. 待確認問題清單</a></li>
</ol>
</div>
<!-- ───────────── 1 ───────────── -->
<section id="s1">
<h2 class="sec">1. 結論速覽(含本次更正)</h2>
<p>plans-plus 前端目前依賴 <b>3 條「只讀目錄」端點</b>editions / plans/catalog / countries<b>2 條「交互查詢」端點</b>agents / tenants/lookup<b>1 組單次查詢端點</b>single-query<b>2 條「下單提交」端點</b>subscribe / payments、以及 <b>1 條「聯絡我們」通知端點</b>contact——<b>不創建訂單</b>,僅發送訂單摘要),共 4 種訂閱流程:<code>new</code>(新購)、<code>renew</code>(續費)、<code>topup</code>(加購)、<code>single</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本地庫僅有 1 個 <code>Standard</code></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<b>KYC 改月租</b>、jQuota 改配套;<code>bestValue</code>/<code>note</code>/<code>nameJP</code> 後端無;<b>本地庫 0 條 Plan</b></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>SearchUserByCodeAndType</code> + <code>GetPlanList(agentUserId)</code></td><td><span class="pill partial">改造</span></td><td><b>新增 <code>tiers</code></b>(可售方案級別)、<code>email</code>/<code>phone</code>、隱藏 <code>agentorId</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> + <code>referrer</code></td></tr>
<tr><td><code>GET /single-query/options</code><br><code>POST /single-query</code></td><td><span class="pill newreq">無對應後端</span></td><td><span class="pill todo">缺口大</span></td><td><b>全新按次查詢流程,後端完全無端點</b>(見 5.6</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><b>BR/CI 仍必填(更正)</b><code>topup</code>/個人主體待定</td></tr>
<tr><td><code>POST /contact</code><br><span class="small">(聯絡我們/推薦人)</span></td><td><code>customer/CreateFeedback</code></td><td><span class="pill done">復用現有</span></td><td><b>不創建訂單</b>:訂單摘要經 contact us API 發管理員;填了推薦人則發推薦人(見 5.8</td></tr>
<tr><td><code>POST /payments/create</code></td><td></td><td><span class="tag">暫緩</span></td><td><b>本輪支付除外</b>:前端提交後直接顯示「已提交成功」(見 5.9</td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">本次三個最重要的更正 / 硬缺口:</span></p>
<p><b>BR/CI 必填(舊版結論已過時,本輪已放開)</b><code>CreateOrder</code> 兩處「BR 或 CI 至少一個」校驗(<code>OrderService.cs:154</code><code>:190</code>)原本仍生效(舊版文檔稱 2026-06 已放開與現狀不符)。<span class="pill done">已改</span> 本輪按決策<b>已注釋這兩處必填校驗</b>(保留唯一性校驗),並 rebuild 生效——個人主體/空 BR 已可下單。<b>注意:此為全局改動,旧站共用 <code>CreateOrder</code>,其「空 BR 不再攔截」影響待業務確認(見第 9 章 Q1</b></p>
<p><b>單次查詢single-query後端零支持</b>:全庫 grep 無任何按次查詢 / pay-per-use 概念。<code>/single-query/options</code><code>/single-query</code> 需新增後端,或本地繼續走 mock。</p>
<p><b>本地種子數據缺口</b>:本地 docker 庫 <code>AMLPortal_Plans</code> / <code>AMLPortal_PlanDetails</code> 均為 <b>0 條</b><code>SaasEditions</code> 僅 1 條 <code>Standard</code><code>AMLPortal_AgentUserPlans</code> 為 0。⇒ 直接對接會拿到空目錄,<b>必須先補種子數據</b>才能跑通(見第 6 章)。</p>
</div>
<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>——後端會給租戶管理員發激活/重置密碼郵件。續費 <code>TenantRenewal</code> 亦繼承上期 <code>AdminPassword</code><code>:2262</code>)。</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 + mock/plans-plus.js內存假數據
&nbsp;&nbsp;├─ dev/stag/prod + PLANS_PLUS_MOCK=true … 先掛 mock/plans-plus.js其餘 /api/* 才透傳後端
&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>plansPlusMock</code><code>PLANS_PLUS_MOCK</code>)開關——在真實環境下<b>仍讓 plans-plus 這組端點走 mock</b>,其餘 <code>/api/*</code> 透傳後端。真實對接時把它置 <code>false</code>,並在 <code>server/routes/</code> 補齊業務路由。</li>
<li><b>掛載順序</b> <code>server/index.js</code>:真實環境依次掛 <code>contact / trial-application / industries / countries</code> 路由,再按 <code>plansPlusMock</code> 決定是否掛 mock最後掛 proxy 兜底。<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>.env</code> 配置(<code>AUTH_USERNAME/PASSWORD/CLIENT_ID/...</code>。BFF 每次調後端用 <code>createAuthConfig()</code><code>Authorization: Bearer</code></li>
</ul>
<div class="callout note">
<p><span class="lbl">關鍵差異(與旧站):</span>旧站 Angular 直接從瀏覽器調 <code>/api/amlPortal/*</code>token 存 localStorage。V2 改為<b>瀏覽器不直接接觸後端</b>,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。</p>
</div>
<div class="callout warn">
<p><span class="lbl">支付本輪除外:</span>當前 <code>plans-plus.js</code><code>submit()</code> 已臨時改為——<code>POST /subscribe</code> 成功後<b>直接顯示「已提交成功」</b><code>showSubmitted()</code><b>不再</b>調 <code>/payments/create</code>、不跳收銀台(源碼注釋標明「臨時改動…還原方法」)。<code>submitSingle()</code> 仍保留支付鏈,但單次查詢後端未就緒。⇒ 本輪落地只需打通到 <code>/subscribe</code> 為止;支付見 5.9 暫緩。</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/plans-plus.js</code> 滿足)。真實對接時 BFF 輸出必須與此<b>逐字段一致</b>,否則前端渲染/計價會出錯。</p>
<h3>3.1 載入期(頁面初始化並發拉取 <code>loadAll()</code></h3>
<p>並發請求 <code>/editions</code><code>/plans/catalog</code><code>/countries</code><code>/single-query/options</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, name* },
kyc:{ monthlyPrice, name* }, // ← 改為「月租」
jquota:{ packages:[{id, jq, price, name*}] } } } }
GET /countries → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] }
GET /single-query/options → { success, data:{ operations:[
{id, price, nameCN/EN/JP, descCN/EN/JP}] } } // ← 新
Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths,
price, originalPrice, qCount(-1=無限), userCountLimit,
bestValue, noteCN, noteEN, noteJP }</code></pre>
<div class="callout note">
<p><span class="lbl">KYC 改為「設備月租」:</span>catalog 的 <code>addons.kyc</code> 不再是 <code>unitPrice</code>,而是 <code>monthlyPrice</code>。前端 <code>kycUnit()</code> = <code>monthlyPrice × 所選方案 periodMonths</code>(隨方案期數自動變動,一次付清、無套餐優惠價,見 <code>plans-plus.js</code> <code>kycPriceForMonths()</code>。jQuota 亦由「按量」改為「選配套」(<code>jquotaPackageId</code>)。</p>
</div>
<h3>3.2 交互期(按需)</h3>
<pre><code>GET /tenants/lookup?email= // ← 只按郵箱;郵箱全局唯一 → 至多命中 1 個租戶
→ { success, match:'none'|'unique', tenant:Tenant|null }
// 註:前端已不再做「一郵箱多租戶」消歧;舊契約的 match:'multiple' / candidates[] 已廢棄
GET /agents/:code
→ { success, found:bool, data:{ code, name, tiers:[tag2Code…], email, phone }|null }
// tiers該推薦人可售的方案級別P2G/Std/Pre/CPA前端據此再過濾行業方案集
Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci,
referrer:{code,name}|null, // ← 註冊時填寫的推薦人,續費頁展示
currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price,
qCount, userCountLimit, startDate, expiryDate, usedQuota,
addons:{ users:int, kyc:bool, jquotaPackageId:string } } }</code></pre>
<h3>3.3 提交期</h3>
<p><code>submit()</code>(在線下單)與 <code>submitContact()</code>(聯絡我們/推薦人)共用 <code>collectPayload()</code> 收集當前表單。本輪支付除外——<code>submit()</code><code>POST /subscribe</code>,成功即顯示「已提交成功」;<code>submitContact()</code> <b>不創建任何訂單</b>,把訂單摘要 <code>POST</code> 到聯絡端點(見 5.8),成功後顯示「已收到資料,將盡快聯絡」。<span class="small">(現網代碼仍 <code>POST /subscribe-offline</code>;本次語義調整後應改走 contact us API見 5.8「與現網代碼的差異」。)</span></p>
<pre><code>POST /subscribe body = {
type:'new'|'renew'|'topup', edition, isCpa, startDate,
plan:Plan, planPrice, isRenewalRate,
addons:{ users, userUnitPrice,
kyc, kycUnitPrice, kycRentalMonths, // ← 含租賃月數
jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice },
agentCode, subtotal, total,
// type=new 追加: subjectType('corp'|'individual'), company, jurisdiction, br,
// contact, phoneCode, phone, email, address
// 個人主體contact=companybr/address 恒為空)
// type=renew/topup 追加: tenantId, company, email, currentSubscription
} → { success, data:{ orderId, status, createdAt } }
POST /contact body = { // ← 聯絡我們/推薦人:不創建訂單,僅發送摘要
name, email, company, // 客戶聯絡人 / 郵箱 / 公司(租戶/主體)名
subject:'訂閱諮詢 · {type}', // 可帶方案名
message: <collectPayload() type//+////>,
agentUserId? // 有推薦人時附帶其後端 Guid → 後端解析郵箱、收件人改為該推薦人
} → { success, data:{ feedbackId } }
POST /single-query body = {
type:'single', subjectType:'individual'|'company', subject, email,
operations:[{id, name, price}], total
} → { success, data:{ orderId, status, createdAt } }</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,2215</code> 聯合校驗)。⇒ BFF 需在服務端依 <code>/plans/catalog</code> 結果<b>反查補齊</b> planDetailId見 5.7)。</p>
</div>
<div class="callout note">
<p><span class="lbl">提交按鈕門檻:</span>前端 <code>computeValidity()</code> 在 terms 勾選 + 各類型必填齊備前禁用「付款」與「聯絡我們」兩個按鈕。<code>new</code> 要求 company/jurisdiction/emailcorp 另需 contact、方案已選、P2G 方案必選 jQuota<code>renew</code> 要求已解析到租戶;<code>topup</code> 要求已解析租戶且至少選一個加值項且未過期;<code>single</code> 要求 subject/email/至少一項查詢。</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>agent 校驗端點在 Identity 模塊 <code>/api/identity/*</code>)。</p>
<table>
<thead><tr><th>#</th><th>方法 / 路由</th><th>用途</th><th>位置</th></tr></thead>
<tbody>
<tr><td>1</td><td><span class="method post">POST</span> <code>/api/amlPortal/plan/portal/GetPlanList</code></td><td>取方案列表Tag1: B/jQ/j/AdlU/KYC</td><td>PlanController:37 · PlanService:54</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 · OrderService:1103</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>按 UserCode 精確查用戶(校驗推薦人/代理)</td><td>CustomIdentityUserController:306</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>=租戶名 <code>Contains</code></td><td>OrderController:308 · OrderService:2353</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 · OrderService:1122</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</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 · OrderService:146</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 · OrderService:2197</td></tr>
<tr><td>10</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/PaymentWebhook</code></td><td>支付回調(支付方服務端調,非前端)</td><td>OrderController:163</td></tr>
<tr><td>11</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/CreateFeedback</code></td><td>聯絡我們/推薦人:接收訂單摘要(<b>不下單</b><b></b><code>AssignedAgentUserId</code> 收件人路由(見 5.8</td><td>CustomerController · <code>routes/contact.js</code> 已封裝</td></tr>
<tr><td></td><td><span class="method new"></span> 單次查詢options + 下單)</td><td>後端<b>無對應端點</b>(見 5.6</td><td></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。agent 返回 DTO<code>iCON.Abp.FX.Users/AppUserDto</code></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:[<b>完整 Edition 實體</b>], jQSeparatedEditions:{ EditionIds:[…], EditionNames:[…] } } }</code><code>OrderService.cs:1103</code> 直接把 <code>SaasEdition</code> 實體與 <code>_appConfig.Portal.jQSeparatedEditions</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>英文名Saas Edition 的 DisplayName</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><code>jQSeparatedEditions.EditionIds</code></td><td>驅動 CPA/標準方案集切換(<code>state.isCpa</code>);注意後端是 <b>PascalCase</b>BFF 需轉小寫鍵名並 <code>toLowerCase()</code></td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">缺口:</span>edition 多語言名稱。後端 <code>GetEditionList</code> 只有 <code>displayName</code>。plans-plus 的 <code>editionName()</code> 在 tc/jp 下優先取 <code>nameCN</code>/<code>nameJP</code>,缺失時 fallback 回 displayName ⇒ <b>不阻塞,但中日文會顯示英文</b>。建議BFF 維護 <code>editionId/displayName → {nameCN,nameJP}</code> 映射表(與 <code>industries.js</code> 現有靜態行業翻譯同思路)。</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>
<div class="callout warn">
<p><span class="lbl">本地現狀:</span>本地庫 <code>SaasEditions</code> <b>只有 1 條 <code>Standard</code></b>Id <code>3A220FC7-9577-660B-729A-4024B1E4BEAA</code>),而 <code>appsettings.local.json</code><code>Portal.jQSeparatedEditions.EditionIds</code>/<code>EditionOUMappings</code> 引用的是另一組 GUID<code>3A1A2969-…</code>/<code>3A0149C4-…</code>),與實庫不符。⇒ 若要在本地看到多行業 + CPA 切換,需<b>先補 Edition 種子並對齊配置</b>(見第 6 章)。</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:null }
// 後端 GetPlanList 對 Tag2List/Tag3List命中或 Tag2Code 為空皆放行OrderService/PlanService:78-80</code></pre>
<p>後端返回 <code>{ code:0, data:{ totalCount, items:[PlanDto{…, planDetails:[PlanDetailDto]}] } }</code><code>PlanService.cs:107</code> <code>PagedResultDto&lt;PlanDto&gt;</code>)。<b>注意</b><code>getAllItems:false</code> 時後端已過濾 <code>Enabled</code> 且僅保留在有效期內的 planDetails<code>PlanService.cs:99-104</code>)。</p>
<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.7</td></tr>
<tr><td><code>tag2Code</code></td><td><code>item.tag2Code</code></td><td>Std/P2G/CPA/Pre…CPA 判定 + agent tiers 過濾用</td></tr>
<tr><td><code>nameCN</code>/<code>nameEN</code></td><td><code>item.nameCN</code>/<code>item.nameEN</code></td><td>旧站會截掉「(…」後綴,可沿用</td></tr>
<tr><td><code>nameJP</code></td><td><b>後端無</b>PlanDto 只有 CN/EN</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.monthlyPrice</code><code>tag1Code==='KYC'</code><code>planDetails[0].price</code><b>當作月租單價</b>,見下方 KYC 映射)</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 warn">
<p><span class="lbl">KYC「月租」映射本次新增難點</span>前端把 KYC 當設備月租:<code>租金 = monthlyPrice × 方案 periodMonths</code>,一次付清。後端 KYC plan 只有<b>單一 Price</b>、旧站按 <code>PCS=1</code> 購買。兩種對法:<br>
<b>把後端 KYC <code>planDetails[0].price</code> 當「月租單價」,下單時 <code>PCS = kycRentalMonths</code></b>(則後端 PaidAmount = price × 月數,與前端計價自洽)——<b>推薦,且無需後端改動</b><br>
② 或後端為 KYC 按周期建多條 PlanDetail / 增月租字段。<br>
⇒ 需與後端確認 KYC 計費口徑BFF 反查 planDetailId 時把 <code>kycRentalMonths</code> 填入該行 <code>PCS</code></p>
</div>
<div class="callout gap">
<p><span class="lbl">缺口:</span><code>bestValue</code><code>note*</code><code>nameJP</code> 後端 <code>PlanDto</code>/<code>PlanDetailDto</code> 無對應字段。短期在 BFF 按 <code>systemCode</code> 寫死映射bestValue / 賣點文案 / 日文名),不阻塞;長期可後端補字段。</p>
</div>
<div class="callout warn">
<p><span class="lbl">本地現狀(阻塞):</span>本地庫 <code>AMLPortal_Plans</code>/<code>AMLPortal_PlanDetails</code><b>0 條</b><code>GetPlanList</code> 會返回空目錄。<b>必須先補 Plan 種子</b>(含 Tag1Code=B/jQ/j/AdlU/KYC、Tag2Code=Std/P2G/CPA/Pre 與對應 PlanDetail才能渲染方案卡見第 6 章)。</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>(種子含 249 個國家)。</p>
<!-- 5.4 -->
<h3 id="s5-4">5.4 GET /agents/:code推薦人代理 + tiers <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 &nbsp;(+&nbsp;GetPlanList)</div>
<p>前端要 <code>{ success, found, data:{ code, name, tiers:[tag2Code…], email, phone } }</code>。後端 <code>SearchUserByCodeAndType</code><code>UserCode</code> 精確匹配,返回 <code>List&lt;AppUserDto&gt;</code><code>{Id, UserName, Email, Name, Surname, PhoneNumber, ExtraProperties}</code>。BFF 取首條做轉換;無匹配 → <code>found:false</code></p>
<table>
<thead><tr><th>前端</th><th>後端來源AppUserDto</th><th>備註</th></tr></thead>
<tbody>
<tr><td><code>data.code</code></td><td><code>ExtraProperties.UserCode</code></td><td>agent codeEF 屬性 UserCode</td></tr>
<tr><td><code>data.name</code></td><td><code>Name</code>(+<code>Surname</code>) / <code>UserName</code></td><td>拼接展示名</td></tr>
<tr><td><code>data.email</code></td><td><code>Email</code></td><td>「聯絡推薦人」用</td></tr>
<tr><td><code>data.phone</code></td><td><code>PhoneNumber</code></td><td>「聯絡推薦人」用</td></tr>
<tr><td>(下單用)<code>agentorId</code></td><td><code>Id</code>Guid</td><td>提交時作 <code>CreateOrderParam.AgentorId</code>;建議 BFF 在 <code>data</code> 內附帶或服務端緩存 code→id</td></tr>
<tr><td><code>data.tiers</code> ⚠️</td><td><b>後端無直接字段</b></td><td>見下「tiers 推導」</td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">tiers 推導(本次新增):</span>plans-plus 用 <code>state.agent.tiers</code>tag2Code 列表)在「所屬行業選定的方案集」上<b>再過濾</b>——只顯示該代理可售級別。後端 agent 的可售方案由 <code>AMLPortal_AgentUserPlans</code> 建模,並由 <code>GetPlanList</code><code>agentUserId</code> 過濾(<code>PlanService.cs:87-92</code>)。⇒ BFF 兩種實現:<br>
<b>推導 tiers</b>:再調一次 <code>GetPlanList{ agentUserId: agent.Id, tag1List:['B'] }</code>,取 distinct <code>tag2Code</code><code>tiers</code>(保持前端過濾邏輯);<br>
<b>或改為服務端過濾</b><code>/plans/catalog</code> 帶上 <code>agentUserId</code> 直接返回該代理可售方案(則前端 tiers 過濾成空操作)。<br>
本地缺 <code>AgentUserPlan</code> 數據,<b>短期可讓 BFF 在 <code>tiers</code> 缺失時返回空數組 = 不過濾</b>(顯示該行業全部方案),與前端 <code>computePlanSet()</code> 對「tiers 為空不過濾」的處理一致。</p>
</div>
<div class="callout note">
<p>備註AMLPortal 另有 <code>Agentor/portal/GetAgentorByCode</code>/<code>GetAgentorByName</code>,旧站與 plans-plus 均用 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= &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenant</div>
<p>前端按<b>郵箱</b>查租戶,返回 <code>match:'none'|'unique'</code> 兩態(<b>不再</b>多租戶消歧——註釋明確「租戶名與郵箱均全局唯一 → 一郵箱至多 1 個租戶」)。命中後帶出 <code>currentSubscription</code> + <code>referrer</code> 預填續費/加購表單。</p>
<p>但後端 <code>queryRenewableTenant</code> 只接受 <code>{keyword}</code> 且按 <b><code>TenantName.Contains(keyword)</code></b> 搜索(<code>OrderService.cs:2362</code>),返回 <code>List&lt;TenantPropertyDto&gt;</code>——<code>TenantAdminEmail</code> 是查完後再逐條用 admin 用戶郵箱回填(<code>:2375-2379</code><b>不能在 DB 層按郵箱過濾</b></p>
<h4>三個不匹配點</h4>
<ol class="tight" style="padding-left:20px">
<li><b>查詢維度不符</b>:前端給郵箱、後端按名字搜。短期 BFF 變通:以空/寬 <code>keyword</code> 拉全部可續費租戶,在服務端按 <code>TenantAdminEmail===email</code> 過濾(⚠️ 全量拉取、性能與越權風險,僅臨時)。正解:<b>後端新增「按管理員郵箱查可續費租戶」端點</b></li>
<li><b>currentSubscription 需重建</b>:前端的 <code>currentSubscription</code>planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons並非 <code>TenantPropertyDto</code> 直接字段,需由 <code>TenantProperty + 其 Order/OrderDetail</code> 推導。可參考 <code>GetCurrServiceInfo</code><code>OrderService.cs:398</code><code>CurrServiceInfoDto</code>)。</li>
<li><b><code>referrer</code> 新增</b>:前端續費頁展示「註冊時填寫的推薦人」。<code>TenantPropertyDto</code> 已有 <code>AgentorCode</code>/<code>AgentorName</code> ⇒ 映射為 <code>referrer:{code:AgentorCode, name:AgentorName}</code></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>referrer</code></td><td><code>{AgentorCode, AgentorName}</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>/<code>periodMonths</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)</code>,直接返回 plans-plus 所需的「租戶 + currentSubscription含 planId/上期價/已購加值項)+ referrer」聚合結構避免在 BFF 拼裝易錯的訂單反查。</p>
</div>
<!-- 5.6 -->
<h3 id="s5-6">5.6 單次查詢 single-query按次付費 <span class="pill newreq">新增需求 · 後端零支持</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/single-query/options &nbsp;·&nbsp; <span class="method post">POST</span> /api/single-query &nbsp;&nbsp; <span class="method new">無對應後端</span></div>
<p>plans-plus 新增第 4 種流程 <code>type='single'</code>:無需訂閱、填「結果接收郵箱 + 查詢主體 + 勾選查詢項目」→ 付款 → 結果郵件發送。前端契約見 3.1/3.3。全庫 grep <b>無任何按次查詢 / pay-per-use 端點或實體</b></p>
<ul class="tight">
<li><code>GET /single-query/options</code>:返回查詢項目目錄 <code>operations:[{id, price, name*, desc*}]</code>制裁篩查、PEP、負面新聞、公司查冊、身份核實…</li>
<li><code>POST /single-query</code>:建按次查詢訂單,返回 <code>orderId</code>,本擬走與訂閱相同的支付鏈(本輪支付除外)。</li>
</ul>
<div class="callout gap">
<p><span class="lbl">落地選項:</span><b>短期保持 mock</b><code>PLANS_PLUS_MOCK=true</code> 或單獨掛 <code>mock/plans-plus.js</code> 的這兩條),其餘端點走真實後端;② 後端新增「按次查詢」下單 + 計費 + 結果投遞(涉及檢測引擎 iCS工作量大<b>本輪建議選 ①</b>single-query 端點繼續 mock優先打通訂閱三態new/renew/topup的真實對接。</p>
</div>
<!-- 5.7 -->
<h3 id="s5-7">5.7 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> 分流;核心是<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>必填;後端校驗重名(<code>:183</code></td></tr>
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td><td>必填;後端 <code>CheckEmailExists</code> 校驗未註冊(<code>:159</code></td></tr>
<tr><td><code>Jurisdiction</code></td><td><code>jurisdiction</code></td><td>必填(<code>:153</code></td></tr>
<tr><td><code>OrganizationReference</code></td><td><code>edition</code>Guid</td><td>必填;校驗 edition 存在(<code>:200</code></td></tr>
<tr><td><code>OrganizationBR</code> / <code>OrganizationCI</code></td><td><code>br</code> / —(前端不收 CI</td><td><span class="pill todo">阻塞</span> <b>後端仍要求 BR 或 CI 至少一個</b>(見下)</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>個人主體 = <code>company</code></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;空則後端指派頂級 salesAdmin<code>:283-287</code></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>
<div class="callout gap">
<p><span class="lbl">BR/CI 阻塞(本次更正):</span><code>CreateOrder</code> 兩處校驗 <b>當前源碼仍生效</b><code>OrderService.cs:154</code><code>:190</code>——<code>if (BR 空 &amp;&amp; CI 空) return "OrganizationBR or OrganizationCI is required"</code>。緊隨的唯一性校驗 <code>ExistsByOrganizationBRCI</code><code>:191</code>)兩者皆空時返回 false<b>走不到那步就先被必填校驗擋下</b>。⇒ plans-plus<br>
· <b>corp 主體</b>BR 標「可選」,但空 BR 會下單失敗;<br>
· <b>individual 主體</b><code>br</code> 恒為空、無 CI ⇒ <b>必然失敗</b><br>
<b>三種對策(擇一):</b>(a) 後端真正去除該必填校驗(若業務允許空 BR/CI(b) 前端把 corp 的 BR 改為必填、且暫不放行 individual 新購;(c) BFF 為空 BR/CI 注入占位值(不推薦,污染合規欄)。<b>需業務決策。</b></p>
</div>
<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>(後端校驗與租戶 admin 郵箱一致 <code>:2239</code></td></tr>
<tr><td><code>AgentorId</code></td><td>空 → 延續原代理(<code>:2321-2323</code></td></tr>
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>固定 <code>false</code></td></tr>
</tbody>
</table>
<p class="small">續費續價:後端按 planDetail 的 <code>Price</code> 計;前端的「續費續價(沿用上期 price」屬展示層優惠<b>後端不認前端傳入的 price</b>。若要真正續價,需後端支持自定義價 / 專屬續費 PlanDetail——本輪按目錄價下單前端續費折扣僅為展示待業務確認</p>
<h4>③ type='topup' → TenantRenewal僅加值項 <span class="pill newreq">需後端確認語義</span></h4>
<p>純加購(無基礎方案,只買 增加用戶/KYC/jQuota。後端無專屬端點。<b>候選 A推薦</b>:復用 <code>TenantRenewal</code><code>PlanList</code> 只含加值項(不含 B 類)。源碼上 <code>TenantRenewal</code> 只對 <code>Tag1Code==='B'</code> 的行設服務起止期(<code>:2300-2314</code>),非 B 行只疊加配額/用戶 ⇒ 理論上「只疊配額不延期」語義成立,但需確認 <code>ProcessTenantEventQueue</code> 下游對「無 B 行的續費單」處理正確。<b>候選 B</b>:後端新增 <code>TenantTopup</code>(可借鑒 admin 的 <code>UpdateTenantProperty</code> <code>:2389</code>,它就是疊 PlanList 重算配額)。</p>
<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 })
// KYC設備月租PCS = kycRentalMonths把 KYC planDetail.price 當月租單價;見 5.2
if (addons.kyc) PlanList.push({ PlanId: KYC.planId,
PlanDetailId: KYC.planDetailId, PCS: addons.kycRentalMonths })
// 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">後端返回OrderDto</span>下單成功返回 <code>OperationDto.Success(OrderDto)</code>,含 <code>orderCode</code><code>goodsName</code><code>orderStatus</code><code>planPrice</code> 等。BFF 應把 <code>orderId/orderCode</code> 回給前端的 <code>data.orderId</code><b>免費/0 元</b>訂單後端直接進 <code>PendingActive</code> 並發激活郵件(<code>:296-303</code><b>在線非 0 元</b><code>MockupPayment</code>(本地佔位,見 5.9)。</p>
</div>
<!-- 5.8 -->
<h3 id="s5-8">5.8 聯絡我們/推薦人(發送訂單摘要 · 不創建訂單) <span class="pill done">復用現有 contact API</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/contact &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/customer/CreateFeedback</div>
<p>訂單摘要下方的「<b>聯絡我們 / 聯絡推薦人</b>」按鈕與付款按鈕<b>同樣的有效性門檻</b>(由 <code>updateSubmitEnabled()</code> 控制),但<b>不創建任何訂單/租戶/線索</b>——只是把用戶當前<b>已填寫的訂單摘要</b><code>collectPayload()</code> 產出的 type/行業/方案/加值項/期間/總價與聯絡資料)整理成一段留言,經<b>既有 contact us API</b> 發出,讓管理員(或推薦人)主動跟進。成功後前端顯示「已收到資料,將盡快聯絡」(<code>showOfflineSubmitted()</code>)。</p>
<div class="callout ok">
<p><span class="lbl">好消息——復用已實現端點:</span>站內已有 <code>server/routes/contact.js</code><code>POST /api/contact</code> → 後端 <code>customer/CreateFeedback</code><b>免 token</b>,見 <code>contact.js:40,143</code>plans-plus 的「聯絡我們」<b>直接復用它即可</b><b>無需新增後端 lead 表/端點</b>。BFF 只需接受一段摘要 <code>message</code>+ 收件人路由參數)並轉為 <code>CreateFeedback</code></p>
</div>
<h4>收件人路由:管理員 vs 推薦人</h4>
<ul class="tight">
<li><b>未填推薦人</b>(或 <code>type≠'new'</code>):發給<b>平台管理員</b>——即現有 <code>CreateFeedback</code> 的既定收件邏輯(後端反饋 + <code>ADMIN_EMAIL</code> 通知,<code>contact.js:17</code>)。</li>
<li><b>已填有效推薦人代碼</b><code>state.agent</code> 已解析;前端 <code>hasReferrer()</code> 僅在 <code>new</code> 流程為真):改發給<b>該推薦人</b>。推薦人 <code>email</code>/<code>phone</code> 已由 <code>/agents/:code</code> 帶出(見 5.4),前端可隨摘要一併提交。</li>
</ul>
<table>
<thead><tr><th>contact 端點入參</th><th>plans-plus 來源</th><th>對應 CreateFeedback</th></tr></thead>
<tbody>
<tr><td><code>company</code></td><td><code>payload.company</code>(新購=公司/個人名;續費/加購=租戶名;單次=查詢主體)</td><td><code>companyName</code></td></tr>
<tr><td><code>name</code></td><td><code>payload.contact</code> / 公司名 / 主體名</td><td><code>customerName</code></td></tr>
<tr><td><code>email</code></td><td><code>payload.email</code>(客戶郵箱)</td><td><code>customerEmail</code>(供回覆)</td></tr>
<tr><td><code>subject</code></td><td>固定文案「訂閱諮詢 · {type}」(可帶方案名)</td><td><code>typeOfQuery</code></td></tr>
<tr><td><code>message</code> <span class="pill todo">未組裝</span></td><td><b>訂單摘要文本</b>type/行業/方案+價/加值項用戶·KYC月租·jQuota/期間/小計/總價/推薦人碼 —— <b>現網尚未產出此文本</b>(見下方缺口)</td><td><code>Message</code>(需把 <code>collectPayload()</code> 序列化為可讀文字;後端限 ≤1000 字)</td></tr>
<tr><td><code>agentUserId</code> <span class="pill newreq">擬新增</span></td><td><code>state.agent</code> 的後端 Guid<code>/agents/:code</code> 帶出,見 5.4</td><td>→ 擬新增的 <code>CreateFeedback.AssignedAgentUserId</code><b>收件人路由</b>,後端據此解析推薦人郵箱(見下)</td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">關鍵缺口——摘要 <code>message</code> 目前「未組裝」:</span>現網 <code>submitContact()</code><code>plans-plus.js:1243</code>)送的是 <code>collectPayload()</code><b>結構化對象</b>type/edition/plan/addons/total…+ <code>channel</code> + <code>referrer</code><b>並無任何 <code>message</code> 文本</b><code>renderSummary()</code> 只把摘要畫進右側 DOM 面板(<code>#ppSum*</code> 的 textContent不產出可提交字符串。⇒ 要把「訂單摘要」當作 contact us 的 <code>message</code><b>必須新增一步序列化</b><br>
· <b>推薦BFF 端拼裝</b>——前端照舊送結構化字段,<code>/api/contact</code> 路由把 type/行業/方案+價/加值項/期間/小計/總價/推薦人碼格式化為可讀多行文本填入 <code>message</code>(順帶多語言與脫敏);<br>
· 或前端拼裝(複用 <code>renderSummary()</code> 的同源計算:<code>currentPlan()</code>/<code>computeSubtotal()</code>/各加值項)。<br>
<b>長度上限</b>:後端 <code>CreateFeedback</code><code>Message ≤ 1000</code> 字(<code>CustomerService.cs:507</code>BFF <code>contact.js</code> 亦要求 <code>message</code> 非空 ⇒ 序列化摘要需精簡到 1000 字內。</p>
</div>
<div class="callout warn">
<p><span class="lbl">與現網代碼的差異(需前端小改):</span>當前 <code>plans-plus.js</code><code>submitContact()</code><code>POST /subscribe-offline</code>(帶 <code>channel:'offline'</code> + <code>referrer</code>、且<b><code>message</code></b>),語義是「線下對接線索」。按本次決策應改為:<b>組裝訂單摘要文本 + <code>POST /api/contact</code></b>(帶 <code>message</code> 摘要 + <code>agentUserId</code> 路由),<b>不再</b>創建 lead/訂單。BFF 側可保留 <code>/subscribe-offline</code> 作向後兼容別名(轉調同一 contact 處理),或直接改前端調用點與 mock。</p>
</div>
<div class="callout note">
<p><span class="lbl">「送推薦人」路由 —— 已定方案②(後端加收件人字段),<span class="pill todo">待實現</span></span>方案細節(供實作參照,尚未落碼):<br>
<code>CreateFeedbackDto</code><code>Feedback</code> 實體各加可空 <code>AssignedAgentUserId</code>Guid?<code>CreateMap&lt;CreateFeedbackDto,Feedback&gt;</code> 同名自動映射,無需改 profile<br>
<code>CustomerService.CreateFeedback</code><code>CustomerService.cs:496</code>)於其有值時,用 <code>ICustomIdentityUserRepository.GetUsersByIDs</code> 解析該推薦人郵箱,作為 <code>SendEmailOnPortalFeedback</code> 第 6 參數 <code>salesEmail</code> 的收件人(推薦人無郵箱時回退平台銷售 <code>_appConfig.Portal.SalesEmailAddress</code>);「給客戶本人」的確認郵件不變;<br>
③ EF 遷移為 <code>AMLPortal_Feedbacks</code> 增一列 <code>uniqueidentifier NULL</code>SQL Server 遷移工程;<b>MySQL 遷移工程停在 2021 <code>Initial</code>、未並行維護</b>,如啟用該 provider 需補同名列)。<br>
<b>BFF 對接</b><code>/api/contact</code><code>agentUserId</code><code>/agents/:code</code> 帶出的後端 Guid見 5.4)透傳為 <code>AssignedAgentUserId</code>,推薦人郵箱由後端解析、前端/BFF 不必自行取。</p>
<p><span class="lbl">為何不走「BFF 抄送、零改後端」(原備選①已否決):</span>曾設想 BFF 拿 <code>agentEmail</code> 自行把摘要<b>抄送</b>推薦人、不動後端。<b>核對後不成立</b>:① contact API 的 <code>CreateFeedbackDto</code> <b>無 cc/bcc 字段</b>;② 底層 <code>CreateEmailQueue(subject, to, cc, bcc, …)</code> 雖有 cc/bcc<code>EmailMessageService.cs:44</code>),但 <code>SendEmailOnPortalFeedback</code> 調用時均傳 <code>null</code>,且該方法<b>對外調不到</b>,唯一可發任意郵件的 <code>EmailQueueController.CreateEmailQueue</code> 端點<b>已被注釋</b><code>EmailQueueController.cs:51</code>);③ V2 BFF 自身<b>無發信能力</b>(無 SMTP/nodemailer只轉調後端。⇒「零改後端」需給 BFF 加 SMTP 或解開後端發信端點(皆非零改動),故以方案②(加 <code>AssignedAgentUserId</code>)為準。</p>
</div>
<!-- 5.9 -->
<h3 id="s5-9">5.9 POST /payments/create支付 <span class="tag">本輪除外</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/payments/create &nbsp;&nbsp; <span class="tag">暫緩</span></div>
<p>當前 <code>plans-plus.js</code><code>submit()</code> 已臨時<b>移除支付鏈</b><code>/subscribe</code> 成功後直接 <code>showSubmitted()</code> 顯示「已提交成功」,不調 <code>/payments/create</code>、不跳收銀台。⇒ <b>本輪不實現支付端點。</b></p>
<p class="small">後端側:在線非 0 元訂單 <code>CreateOrder</code>/<code>TenantRenewal</code> 目前走 <code>MockupPayment(newOrder)</code>(本地佔位、非真實支付方),配合 <code>PaymentWebhook</code>(端點 10。真實支付QFPay 收銀台簽名 URL + webhook留待後續階段屆時 QFPay 簽名邏輯應在 BFF 服務端完成API key 不可暴露前端mock 的 <code>pay-gateway</code>/<code>pay-return</code>/<code>/payments/:id</code> 輪詢鏈可作參照。</p>
</section>
<!-- ───────────── 6 ───────────── -->
<section id="s6">
<h2 class="sec">6. 本地開發環境對接docker-compose-local-dev</h2>
<p>本地棧(<code>AML_Backend/docker-compose-local-dev/</code>)已把後端 <code>iCON.Abp.FX.HttpApi.Host</code> 起在 <code>http://localhost:44331</code>Swagger 同址SQL Server 在 <code>localhost,11433</code>。要把 V2 的 plans-plus BFF 指到它,需三件事:<b>配 .env、對齊 Portal 訪客憑證、補種子數據</b></p>
<h3>6.1 V2 側配置(把 BFF 指向本地後端)</h3>
<pre><code># 便捷腳本(已加入 package.jsonAPP_ENV=dev + 本地後端 + plans-plus mock 兜底 + PORT 8090
npm run start:local
# 等價於:
cross-env APP_ENV=dev API_BASE_URL=http://localhost:44331 PLANS_PLUS_MOCK=true PORT=8090 node server/index.js</code></pre>
<div class="callout ok">
<p><span class="lbl">實測結論(免 token</span>plans-plus 依賴的門戶端點 <code>GetPlanList / GetEditionList / getCategoryByTypes / CreateOrder / TenantRenewal / queryRenewableTenant</code> 均為 <code>[AbpAutoAuth("Portal")]</code>——後端 <code>AbpAutoAuthMiddleware</code><b>服務端</b>注入門戶訪客 token 並覆蓋調用方 <code>Authorization</code><code>SearchUserByCodeAndType</code><code>[AllowAnonymous]</code>。⇒ <b>BFF 無需自帶 token</b>,直接匿名 POST 即可。因此本地不必配 <code>AUTH_*</code>(且本地 <code>Portal</code> 租戶未種子化,<code>connect/token?__tenant=Portal</code> 反而會報 <code>Tenant not found</code>)。新增的 5 個真實路由與已改的 <code>countries.js</code> 均按此免 token 實現。</p>
</div>
<h3>6.2 種子數據缺口(對接前必補)</h3>
<p>本地庫當前(實測):</p>
<table>
<thead><tr><th></th><th>本地現狀</th><th>plans-plus 需要</th></tr></thead>
<tbody>
<tr><td><code>AMLPortal_Plans</code></td><td><b>0 條</b></td><td>B(Std/P2G/Pre/CPA) + jQ/j + AdlU + KYC 各若干</td></tr>
<tr><td><code>AMLPortal_PlanDetails</code></td><td><b>0 條</b></td><td>每 Plan 至少 1 條(含 price/periodMonths/qCount/userCountLimit</td></tr>
<tr><td><code>SaasEditions</code></td><td>1 條 <code>Standard</code><code>3A220FC7-…</code></td><td>多行業VASP/TCSP/MSO/CPA…至少 1 個 CPA edition 對齊 jQSeparatedEditions</td></tr>
<tr><td><code>AMLPortal_AgentUserPlans</code></td><td>0 條</td><td>(可選)測 agent tiers 過濾時需要</td></tr>
<tr><td>配置 <code>Portal.jQSeparatedEditions.EditionIds</code></td><td><code>3A1A2969-…</code>(庫中不存在)</td><td>對齊到實際 CPA edition 的 GUID</td></tr>
</tbody>
</table>
<div class="callout gap">
<p><span class="lbl">補種子的兩條路:</span><b>直接寫 SQL</b><code>SaasEditions</code>/<code>AMLPortal_Plans</code>/<code>AMLPortal_PlanDetails</code> 插測試數據(快,適合本地聯調;注意 <code>AMLPortal_Plans</code> 需正確的 <code>Tag1Code</code>/<code>Tag2Code</code>/<code>Enabled=1</code>/<code>IsActive=1</code>,並讓 <code>PlanDetail.EffectTime/ExpireTime</code> 覆蓋當前);② <b>加後端 DataSeedContributor</b><code>AMLPortalDataSeedContributor</code> 目前只 seed TenantProperty補 Plan/Edition 種子,跑 <code>db-migrator</code> 幂等注入(更可復現,改動後端代碼)。<b>本地聯調建議 ①</b>,並把 CPA edition 的 GUID 同步進 <code>appsettings.local.json</code> 後重啟 host。</p>
</div>
<h3>6.3 本輪落地狀態(已實現 · 2026-07 實測通過)</h3>
<table>
<thead><tr><th></th><th>狀態</th><th>說明</th></tr></thead>
<tbody>
<tr><td>Edition + Plan/PlanDetail 種子</td><td><span class="pill done">已補</span></td><td><code>docker-compose-local-dev/seed-plans-plus.sql</code>(幂等,鏡像 mock 目錄CPA edition GUID 對齊 jQSeparatedEditions<code>down -v</code> 後重跑)</td></tr>
<tr><td>後端 BR/CI 必填放開</td><td><span class="pill done">已改</span></td><td><code>OrderService.cs:154,190</code> 兩處必填校驗已注釋唯一性校驗保留rebuild 已生效)</td></tr>
<tr><td>BFF 真實路由</td><td><span class="pill done">已寫</span></td><td>新增 <code>routes/editions.js · plans.js · agents.js · tenants.js · subscribe.js</code> + <code>services/catalog.js</code><code>countries.js</code> 改為免 token<code>index.js</code> 掛載於 mock/代理之前</td></tr>
<tr><td>訂閱三態</td><td><span class="pill done">new 已驗</span> <span class="pill partial">renew/topup 待數據</span></td><td>newcorp/個人/CPA/加值項)實測返回 orderId、PlanPrice 正確(含 KYC 月租 PCS=月數renew/topup 路由已寫,但本地無可續費租戶數據,待後端「按郵箱查」或先建租戶</td></tr>
<tr><td>single-query / payments</td><td><span class="pill partial">mock</span></td><td>按決策暫留 mock<code>PLANS_PLUS_MOCK=true</code> 兜底);支付本輪除外</td></tr>
<tr><td>聯絡我們(不下單)</td><td><span class="pill partial">改接 contact</span></td><td>復用 <code>routes/contact.js</code><code>CreateFeedback</code>;前端調用點由 <code>/subscribe-offline</code><code>/contact</code>,推薦人路由由 BFF 補</td></tr>
</tbody>
</table>
<p class="small">端到端驗證(<code>npm run start:local</code> + 本地 docker 後端):<code>/editions</code>10 行業 + CPA 切換)· <code>/plans/catalog</code>standard/cpa/addons 全對)· <code>/countries</code>249· <code>/agents/:code</code>(無數據 found:false· <code>/subscribe</code>new 各變體均返回 orderId、訂單入庫。頁面 <code>GET /plans-plus</code> HTTP 200。</p>
</section>
<!-- ───────────── 7 ───────────── -->
<section id="s7">
<h2 class="sec">7. plans-plus 相對旧站的新增需求</h2>
<table>
<thead><tr><th>#</th><th>新增/變更</th><th>對接影響</th><th>狀態</th></tr></thead>
<tbody>
<tr><td>1</td><td><b>單次查詢 single-query</b>(按次付費,第 4 種流程)</td><td>後端零支持,需新增或保留 mock</td><td><span class="pill todo">需後端/mock</span></td></tr>
<tr><td>2</td><td><b>聯絡我們/推薦人</b>(發訂單摘要 · <b>不創建訂單</b></td><td>復用既有 <code>contact</code>/<code>CreateFeedback</code>;「送推薦人」需後端加 <code>AssignedAgentUserId</code> 收件人字段(小改)+ BFF 拼裝 <code>message</code></td><td><span class="pill partial">復用+後端小改</span></td></tr>
<tr><td>3</td><td><b>agent tiers</b>(推薦人可售級別過濾方案)</td><td>SearchUser 不含 tiers需經 GetPlanList(agentUserId) 推導</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>4</td><td><b>agent email/phone</b>(聯絡推薦人)</td><td>AppUserDto 已含 Email/PhoneNumberBFF 帶出</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>5</td><td><b>主體類型 corp/individual</b>(新租戶)</td><td>個人主體無 BR/CI ⇒ 撞後端 BR/CI 必填</td><td><span class="pill todo">阻塞</span></td></tr>
<tr><td>6</td><td><b>BR/CI 仍必填</b>(更正舊版「已放開」結論)</td><td><code>OrderService.cs:154,190</code> 校驗仍生效</td><td><span class="pill todo">需業務決策</span></td></tr>
<tr><td>7</td><td><b>KYC 改設備月租</b>monthlyPrice × 月數)</td><td>後端 KYC 單價 ⇒ 下單 PCS=月數(見 5.2</td><td><span class="pill partial">BFF/後端確認</span></td></tr>
<tr><td>8</td><td><b>jQuota 改選配套</b>package非按量</td><td>後端 jQ plan 天然離散BFF 映 packages</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>9</td><td><b>按郵箱查租戶 + 郵箱唯一單命中</b></td><td>後端按名字搜,需按郵箱端點;不再多租戶消歧</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td>10</td><td><b>續費頁展示 referrer</b></td><td>TenantPropertyDto.AgentorCode/Name 映射</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>11</td><td><b>續費續價 / 顯示舊方案</b>Req 9</td><td>續價後端不認前端 price舊方案卡屬前端渲染</td><td><span class="pill todo">後端確認</span></td></tr>
<tr><td>12</td><td><b>過期加購阻斷</b>(須先續費)</td><td>純前端門檻,無對接影響</td><td><span class="pill done">無影響</span></td></tr>
<tr><td>13</td><td><b>去掉管理員密碼步驟</b></td><td>後端忽略 AdminPassword發激活郵件</td><td><span class="pill done">無影響</span></td></tr>
<tr><td>14</td><td><b>bestValue / note / edition&plan 多語言</b></td><td>後端無字段BFF 配置或後端補</td><td><span class="pill partial">BFF/後端</span></td></tr>
<tr><td>15</td><td><b>支付端點化</b>(本輪除外)</td><td>submit 暫不跳支付,直接顯示已提交</td><td><span class="tag">暫緩</span></td></tr>
</tbody>
</table>
</section>
<!-- ───────────── 8 ───────────── -->
<section id="s8">
<h2 class="sec">8. 實施建議與分期</h2>
<h3>8.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> + 多語言/過濾整理 + jQSeparated 鍵名轉換</td><td>GetEditionList</td></tr>
<tr><td><code>routes/plans.js</code>(新)</td><td><code>GET /plans/catalog</code> + filterPlan 拆分 + KYC 月租/jQ 配套 + 緩存 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,tiers,email,phone,agentorId}</td><td>SearchUserByCodeAndType (+GetPlanList)</td></tr>
<tr><td><code>routes/tenants.js</code>(新)</td><td><code>GET /tenants/lookup?email=</code> + currentSubscription 重建 + referrer</td><td>queryRenewableTenant(ByEmail)</td></tr>
<tr><td><code>routes/subscribe.js</code>(新)</td><td><code>POST /subscribe</code> 分流 + PlanList 組裝(含 KYC PCS=月數)</td><td>CreateOrder / TenantRenewal / (Topup)</td></tr>
<tr><td><code>routes/contact.js</code></td><td>已存在;「聯絡我們」復用之——把結構化 payload 序列化為 <code>message</code> 摘要 + 透傳 <code>agentUserId</code>,轉 <code>CreateFeedback</code>(收件人路由由後端 <code>AssignedAgentUserId</code> 承接)</td><td>CreateFeedback</td></tr>
<tr><td><code>mock/plans-plus.js</code></td><td>single-query / payments <b>暫留 mock</b><code>subscribe-offline</code> 改由 contact 承接(可保留為兼容別名)</td><td></td></tr>
<tr><td><code>index.js</code></td><td>代理之前追加各真實路由 <code>app.use(apiPrefix, createXxxRouter(config))</code></td><td></td></tr>
<tr><td><code>services/auth.js</code> / <code>.env.*</code></td><td>門戶訪客憑證 + <code>__tenant=Portal</code>edition/plan 文案映射</td><td></td></tr>
</tbody>
</table>
<h3>8.2 可能的後端改動(與後端團隊確認)</h3>
<ul class="tight">
<li><b>BR/CI 必填</b>:是否按 plans-plus 需求去除 <code>CreateOrder</code> 的 BR/CI 必填校驗(或僅對個人主體放開)。</li>
<li><b>按郵箱查可續費租戶</b>端點,返回聚合 currentSubscription含 planId / 上期 price / 已購加值項)+ referrer。</li>
<li><b>topup 語義</b>:確認 <code>TenantRenewal</code>PlanList 僅加值項)是否「只疊配額不延期」,或新增 <code>TenantTopup</code></li>
<li><b>KYC 計費口徑</b>:確認 KYC plan 是否可按 <code>PCS=月數</code> 計月租。</li>
<li><b>single-query</b>:是否新增後端按次查詢端點,還是短期 mock。</li>
<li><b>聯絡我們送推薦人</b><span class="pill todo">待實現</span> 已定方案——後端 <code>CreateFeedback</code><code>AssignedAgentUserId</code> 收件人字段DTO + 實體 + 服務解析郵箱 + EF 遷移):有值發推薦人、否則發平台銷售。</li>
<li>可選Plan 增 <code>bestValue</code>/多語言 <code>note</code>/<code>nameJP</code>edition 增多語言名稱。</li>
</ul>
<h3>8.3 建議分期</h3>
<table>
<thead><tr><th>階段</th><th>內容</th><th>可獨立交付</th></tr></thead>
<tbody>
<tr><td>P0 · 本地種子</td><td>補 Edition + Plan/PlanDetail 種子,對齊配置(第 6 章)</td><td>後端返回非空目錄,可對接</td></tr>
<tr><td>P1 · 只讀目錄</td><td>editions + plans/catalog + countries已就緒+ agents</td><td>頁面渲染方案/行業/國家,校驗推薦人</td></tr>
<tr><td>P2 · 新購下單</td><td>subscribe(new)(先解 BR/CI 阻塞)</td><td>新租戶下單至「已提交成功」(支付除外)</td></tr>
<tr><td>P3 · 續費/加購</td><td>tenants/lookup後端新端點+ subscribe(renew/topup)</td><td>續費/加購閉環(支付除外)</td></tr>
<tr><td>P4 · 單次查詢/聯絡/支付</td><td>single-query + 聯絡我們(接 contact API + 推薦人路由)+ payments後端就緒後</td><td>完整流程 + 在線支付</td></tr>
</tbody>
</table>
</section>
<!-- ───────────── 9 ───────────── -->
<section id="s9">
<h2 class="sec">9. 待確認問題清單</h2>
<ol class="tight" style="padding-left:20px">
<li><b>BR/CI 必填:</b><span class="pill todo">關鍵</span> 後端 <code>CreateOrder</code> 仍要求 BR 或 CI 至少一個(<code>OrderService.cs:154,190</code>。plans-plus 新購(含個人主體)需不填也能下單——是否去除該校驗?還是前端把 corp BR 改必填、個人主體暫不開放?</li>
<li><b>單次查詢:</b>後端是否新增按次查詢options + 下單 + 計費 + 結果投遞)?本輪是否先保留 mock</li>
<li><b>聯絡我們(不下單):</b><span class="pill done">已定方案</span> 復用 <code>contact</code>/<code>CreateFeedback</code> 發送訂單摘要(不創建訂單/線索);「送推薦人」擬由後端 <code>CreateFeedback.AssignedAgentUserId</code> 承接(<span class="pill todo">待實現</span>,見 5.8)。<b>待辦</b>:後端加字段 + 服務路由 + EF 遷移;前端調用點由 <code>/subscribe-offline</code><code>POST /api/contact</code> 並透傳 <code>agentUserId</code>BFF 映為 <code>AssignedAgentUserId</code></li>
<li><b>租戶查詢:</b>能否新增「按管理員郵箱查可續費租戶」,直接帶 currentSubscriptionplanId/上期價/已購加值項)+ referrer</li>
<li><b>topup</b>純加購走 <code>TenantRenewal</code>PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?</li>
<li><b>KYC 月租:</b>KYC plan 是否可按 <code>PCS=租賃月數</code> 計費planDetail.price 當月租單價)?</li>
<li><b>續費續價:</b>後端 <code>TenantRenewal</code> 是否支持沿用上期價/自定義價?否則前端續費折扣僅展示、實際按目錄價。</li>
<li><b>agent tiers</b>BFF 經 <code>GetPlanList(agentUserId)</code> 推導 tiers還是改為服務端直接按 agent 過濾 catalog</li>
<li><b>agentorId</b><code>/agents/:code</code> 響應附帶 agent Guid id避免下單時二次查詢</li>
<li><b>鑑權憑證:</b>V2 BFF 門戶訪客帳號(<code>Portal@iconsz.com</code><code>__tenant=Portal</code>)權限是否覆蓋 CreateOrder / queryRenewableTenant / SearchUserByCodeAndType</li>
<li><b>本地種子:</b>Plan/Edition 種子走臨時 SQL 還是入 <code>AMLPortalDataSeedContributor</code>CPA edition GUID 是否同步進 <code>appsettings.local.json</code></li>
<li><b>bestValue / note / 多語言:</b>由 BFF 配置維護,還是後端在數據上補字段?</li>
</ol>
</section>
<p class="small" style="text-align:center;margin-top:48px;color:var(--muted)">
本分析基於源碼靜態閱讀 + 本地 docker 庫實測2026-07<code>plans-plus.js</code> / <code>server/*</code>(含 mock 契約)/ <code>AMLPortal</code> Controllers 與 Service / <code>CustomIdentityUserController</code> / 旧站 <code>PlanService</code> / <code>appsettings.local.json</code> 與本地數據庫。涉及後端行為BR/CI 校驗、續費續價、topup / KYC 計費語義)以實際接口 + 後端確認為準。
</p>
</div>
</body>
</html>