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

816 lines
106 KiB
HTML
Raw Permalink 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.newreq { background:#fbefff; border-color:#d8b9ff; }
.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>
<p class="meta updated">更正2026-07<b>BR/CI 非必填</b>(業務決策)——去除 <code>CreateOrder</code><code>OrderService.cs:154</code>/<code>:190</code> 兩處必填校驗(保留唯一性校驗)。文檔原「仍必填 / 阻塞 / 需業務決策」表述作廢;第 1/5.7/6.3/7-9 章已統一。<b>核對:當前 working copy 這兩處仍為 active需確認放開已在對接/部署環境應用。</b></p>
<p class="meta updated">再次更新2026-07<b>單次查詢(隨付即用 · Pay-As-You-Go後端已支持</b>——由 AML 主模塊 <code>ConsumerPortal</code>「即時檢測」承接。<b>調用方已確認為 AML 後端內部編排</b><b>訂單支付完成後</b>,由後端在 2C 租戶上下文內依次 <code>CreateConsumerLink</code>(建鏈接、拿 accessToken<code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件);<b>justsolutionsWebV2 只負責下單 + 收款,不調這兩個接口</b>(既非 V2 BFF、也非瀏覽器發起。原 5.6「後端零支持」結論<b>作廢</b>,第 1/2/4/5.6/7-9 章已據此改寫。</p>
<p class="meta updated">租戶查詢落定2026-07<b><code>GET /tenants/lookup?email=</code> 需後端新增「按管理員郵箱查可續費租戶」端點</b>。現 <code>queryRenewableTenant</code> 在 DB 層<b>只能按租戶名 <code>Contains</code></b><code>TenantAdminEmail</code> 是查完後遍歷<b>全部租戶用戶</b>逐條回填(<code>OrderService.cs:2372-2379</code><b>無法按郵箱在 DB 過濾</b>。BFF「以空 keyword 拉全部可續費租戶、再本地按郵箱篩」的變通<b>不採用</b>(現端點按租戶名搜、返回整張租戶列表,查詢維度與聚合歸屬都更該在後端);<b>後端按郵箱全庫掃描本身可接受</b>(租戶數少、低頻交互,可復用現 <code>GetAllUsers()</code> 全庫掃描、改按郵箱過濾)。第 1 / 5.5 / 7 / 8 / 9 章已據此改寫,並補入新端點的具體設計(入參、反查邏輯、聚合返回)。</p>
<p class="meta updated">單次查詢定價修正2026-07<b>隨付即用PAYG的收款金額改由 AML <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價決定</b>(不再是 V2 <code>/single-query/options</code> 分項單價之和);<b>檢測內容為固定 4 項、純展示、用戶不可勾選/取消</b>——<code>證件核驗(V)</code><code>名單篩查(ES=E)</code><code>AI 增強型篩查(A)</code><code>信貸記錄篩查(失信人=D)</code>(叫法後續可能再調整)。頁面 <code>/plans-jp</code>(日本別名,<code>server/index.js</code> 直接以 <code>plans-plus.html</code> 承接、URL 不變;日本 IP 訪 <code>/plans</code> 自動 302 至此)對應 <code>countryCode:"JPN"</code><b>目前 <code>countryCode</code> 僅日本一國</b>PAYG 為<b>單一整包價</b>,非分項之和)。原 5.6「op→功能碼非 1:1 / 公司查冊歸屬 / V2 分項單價與後端 jQ 計費對齊」等缺口據此<b>收斂</b>;第 1 / 3.1 / 5.6 / 7 / 8 / 9 章已改寫。</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-queryConsumerPortal 即時檢測)</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>queryRenewableTenant</code><b>新增</b> <code>queryRenewableTenantByEmail</code></td><td><span class="pill todo">需後端新增</span></td><td><b>已定:後端新增按郵箱端點</b>。現端點 DB 層只按<b>租戶名 <code>Contains</code></b> 搜、郵箱查後回填→不能按郵箱過濾;前端<b>按郵箱、唯一命中</b>(不再多租戶消歧);新端點直接聚合 <code>currentSubscription</code> + <code>referrer</code>(見 5.5</td></tr>
<tr><td><code>GET /single-query/options</code><br><code>POST /single-query</code></td><td>V2 只到「下單+收款」;<br>支付後 <b>AML 後端內部</b> <code>CreateConsumerLink</code>+<code>CreateConsumerOrder</code></td><td><span class="pill partial">改造</span></td><td><b>更正:後端已支持</b>AML 即時檢測);兩步為<b>後端內部編排</b>V2/瀏覽器均不調;<b>PAYG 定價改由 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b>、檢測內容固定 4 項(V/E/A/D)純展示不可選(見 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>plans-plus 新購(含個人主體 / 空 BR/CI需能下單 ⇒ <b>BR/CI 不作必填</b>。實現=注释 <code>CreateOrder</code> 兩處必填校驗(<code>OrderService.cs:154</code><code>:190</code>);唯一性校驗 <code>ExistsByOrganizationBRCI</code> 對「BR、CI 皆空」返回 <code>false</code><code>:1124</code><b>空值安全通過,無需其它改動</b><span class="pill todo">核對</span> <b>當前 working copy 這兩處仍為 active</b>——若對接/部署環境尚未放開,空 BR/CI 仍會被攔,需確認該改動已應用。注意此為全局改動(旧站共用 <code>CreateOrder</code>),「空 BR 不再攔截」影響請業務知悉(見第 9 章 Q1</p>
<p><b>單次查詢single-query後端已支持本輪更正原「零支持」結論作廢</b>:走 AML 主模塊的 <b>ConsumerPortal 即時檢測</b>,且<b>調用方為 AML 後端內部編排</b>——<b>訂單支付完成後</b>,後端在 2C 租戶上下文內 <code>CreateConsumerLink</code>(建鏈接、拿 accessToken<code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件)。<b>V2 只下單 + 收款,不調這兩個接口</b><b>本輪再定PAYG 檢測內容固定為 <code>證件核驗 / 名單篩查(ES) / AI 增強型篩查 / 信貸記錄篩查(失信人)</code> 4 項純展示(用戶不可勾選/取消,確定性映射 <code>V/E/A/D</code>),收款金額取自 <code>GetPlanList</code><code>countryCode</code><code>/plans-jp→JPN</code>)返回的 PAYG(P2G) 方案價</b>——原「op→功能碼非 1:1 / 公司查冊無碼 / V2 分項單價與後端 jQ 對齊」缺口收斂。<code>CreateConsumerLink</code> 非匿名(<code>RealtimeScreeningManagement</code> + <code>AML.ConsumerPortal.Enable</code>)在後端 2C 租戶上下文中天然滿足,故 <b>V2 BFF 無需持該租戶憑證</b><span class="pill partial">改造</span> 詳見 5.6。</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>/subscribe</code> 為止;支付見 5.9 暫緩。</p>
</div>
<div class="callout note">
<p><span class="lbl">single-query 是 BFF 範式的例外:</span>其餘端點都是「瀏覽器 → V2 <code>/api/*</code> → BFF 代理後端」;但單次查詢的<b>即時檢測觸發CreateConsumerLink/CreateConsumerOrder不走 BFF 代理</b>——由 <b>AML 後端在支付完成後內部編排</b>2C 租戶上下文V2 只到「下單 + 收款」為止。詳見 5.6。</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, nameCN/EN/JP, descCN/EN/JP}] } } // ← 固定 4 項純展示,不可勾選/取消price 不再逐項計
// 固定 4 項:證件核驗(V) / 名單篩查ES(E) / AI 增強型篩查(A) / 信貸記錄篩查失信人(D)
// PAYG 收款價 → 取 GetPlanList(countryCode) 的 PAYG(P2G) 方案 price單一整包價非分項之和
POST /amlPortal/plan/portal/GetPlanList body = { // ← PAYG 定價來源(沿用旧站請求體 + countryCode
pageIndex:0, pageSize:100, filter:"", getAllItems:false,
tag1List:[], tag2List:[], tag3List:[], countryCode:"JPN" } // 目前 countryCode 僅日本一國
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>
<div class="callout note">
<p><span class="lbl">單次查詢PAYG目錄改為「固定展示 + 外部定價」:</span><code>/single-query/options</code> 的 4 項<b>不再是可勾選的計費項</b>,而是<b>固定信息展示</b><code>證件核驗 / 名單篩查(ES) / AI 增強型篩查 / 信貸記錄篩查(失信人)</code>,用戶不可選/取消;名稱後續可能調整)——可由前端硬編碼、或後端返回固定 4 項,<b>不含 <code>price</code></b>。PAYG 的<b>收款金額</b>單獨取自 <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價:<code>/plans-jp</code>(日本別名頁)傳 <code>countryCode:"JPN"</code>——<b>目前 <code>countryCode</code> 僅日本一國</b>(後端 <code>GetPlanListParam.CountryCode</code> 支持「該國專屬 + 全球通用(CountryCode=null)」過濾,將來擴國時沿用同一機制)。該價為<b>單一整包價</b>。⇒ 前端需依 URL/地域推導 <code>countryCode</code> 並在載入期取價(見 5.6)。</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>111 均在 <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>,並已被旧站 <code>justsolutionsWeb/PlanService</code> 使用驗證過。前綴 <code>/api/amlPortal/*</code>agent 校驗端點在 Identity 模塊 <code>/api/identity/*</code>)。<b>1213 為單次查詢用到的方法,位於 AML 主模塊 <code>iCON.Abp.AML</code><code>/api/aml/ConsumerPortal/*</code>);但<u>由 AML 後端在支付完成後內部編排調用</u>V2 BFF 不代理、瀏覽器不直調</b>(見 5.6)。</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>12</td><td><span class="method post">POST</span> <code>/api/aml/ConsumerPortal/CreateConsumerLink</code><br><span class="small">(後端內部編排調用)</span></td><td>單次查詢第 1 步:建即時檢測鏈接(回 <code>accessToken</code><b>·非匿名</b>RealtimeScreeningManagement + <code>AML.ConsumerPortal.Enable</code>)——在後端 2C 租戶上下文內滿足</td><td>ConsumerPortalController:340 · ConsumerPortalService:964</td></tr>
<tr><td>13</td><td><span class="method post">POST</span> <code>/api/aml/ConsumerPortal/CreateConsumerOrder</code><br><span class="small">(後端內部編排調用)</span></td><td>單次查詢第 2 步:憑 <code>accessToken</code> 建訂單並即時檢測(異步跑模塊 + 結果郵件)<b>·匿名</b></td><td>ConsumerPortalController:78 · ConsumerPortalService:325</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>。單次查詢 DTO<code>iCON.Abp.AML/Application.Contracts/ConsumerPortalAppLayer/*</code>CreateConsumerLinkParam、CreateConsumerCustomerDto、CreateConsumerLinkDto、CreateConsumerOrderParam、ConsumerLinkDto+ <code>IndividualAppLayer/CreateIndividualDto</code><code>OrgLayer/CreateOrganizationDto</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 new">NEW</span> /api/amlPortal/Order/portal/<b>queryRenewableTenantByEmail</b></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> 在 DB 層搜索(<code>OrderService.cs:2362-2364</code>),返回 <code>List&lt;TenantPropertyDto&gt;</code>——<code>TenantAdminEmail</code> 是查完命中租戶後、再 <code>GetAllUsers()</code> 遍歷<b>全庫用戶</b><code>UserName=='admin' &amp;&amp; TenantId</code> 逐條回填(<code>:2369-2379</code><b>邏輯上「先有租戶、後有郵箱」,無法在 DB 層按郵箱過濾</b>。⇒ 前端「給郵箱、要唯一命中」的查詢維度與現端點<b>根本錯位</b></p>
<div class="callout gap">
<p><span class="lbl">已定決策:後端新增「按管理員郵箱查可續費租戶」端點。</span>不採用 BFF 變通(以空 <code>keyword</code> 拉全部可續費租戶、再服務端按 <code>TenantAdminEmail===email</code> 過濾)——現端點按<b>租戶名</b>搜、返回<b>整張可續費租戶列表</b>,讓 BFF 承接全表列表並自拼 <code>currentSubscription</code>/<code>referrer</code> 的訂單反查,既非其職責、也易錯;<b>查詢維度與聚合歸屬都更該落在後端</b><b>(注:問題不在掃描成本——後端側按郵箱全庫掃描本身可接受,租戶數少、低頻交互;而在查詢維度錯位與聚合歸屬。)</b>新端點把查詢維度反過來(<b>先按郵箱定位 admin 用戶 → 再取其租戶</b>),順帶一次性聚合 plans-plus 續費/加購頁所需的 <code>currentSubscription</code> + <code>referrer</code>,避免 BFF 拼裝易錯的訂單反查。</p>
</div>
<h4>新增端點設計(供後端實作參照)</h4>
<div class="endpoint-head"><span class="method new">NEW</span> <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenantByEmail &nbsp;<span class="tag">[AbpAutoAuth("Portal")]</span></div>
<ul class="tight">
<li><b>入參</b> <code>QueryRenewableTenantByEmailParam { string Email }</code>(新 DTO置於 <code>Application.Contracts/OrderAppLayer/</code>,與 <code>QueryRenewableTenantParam</code> 並列)。鑑權沿用現 <code>queryRenewableTenant</code><code>[AbpAutoAuth("Portal")]</code>BFF 免自帶 token見第 6 章)。</li>
<li><b>反查邏輯(維度反轉,且省掉全庫掃描)</b><br>
① 禁用多租戶過濾 <code>_dataFilter.Disable&lt;IMultiTenant&gt;()</code>按郵箱定位租戶管理員用戶——沿用現端點「admin 郵箱 = <code>UserName=='admin'</code> 用戶的 Email」語義可直接<b>復用現 <code>GetAllUsers()</code> 全庫掃描</b>(與現 <code>queryRenewableTenant</code> 一致),改在內存按 <code>u.UserName=='admin' &amp;&amp; u.Email==Email</code> 過濾即可(郵箱全局唯一 → 至多 1 條)。<b>全庫掃描在此可接受</b>——租戶 / 管理員用戶數量少、續費查詢為低頻交互,<b>無需為此下推 DB 過濾或加索引</b>。無命中 → 回空 ⇒ 前端 <code>match:'none'</code><br>
② 取該用戶 <code>TenantId</code> 對應的 <b>active</b> <code>TenantProperty</code><code>IsActive &amp;&amp; TargetTenantID==tenantId</code>,按 <code>CreationTime</code> 取最新一條),並校驗其可續費(<code>TenantStatus</code> / <code>EffectiveEndTime</code>——過期仍返回,由前端做「過期/臨期」展示,見 5.7 topup 阻斷)。<br>
③ 組裝聚合返回:<code>referrer</code> 直取 <code>AgentorCode/AgentorName</code><code>currentSubscription</code> 明細planId / 名稱 / periodMonths / price / addons沿用 <code>GetCurrServiceInfo</code><code>OrderService.cs:398-418</code>)的 <b>Order → OrderDetails → Plan/PlanDetail</b> 反查思路,區別是它以 <code>CurrentTenant.Id</code> 為基、此處以顯式 <code>TargetTenantID</code> 為基Portal 上下文 + 禁多租戶過濾)。</li>
<li><b>返回</b>:建議新增聚合 DTO<code>RenewableTenantDto</code> = <code>TenantPropertyDto</code> 關鍵字段 + <code>referrer</code> + 明細化 <code>currentSubscription</code>),讓 BFF 幾乎<b>直通轉發</b>,不必自己再拼訂單明細。</li>
</ul>
<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 note">
<p><span class="lbl">聚合由誰拼裝:</span>上表「可得部分」是 <code>TenantPropertyDto</code> 直出字段;而 <code>currentSubscription</code><b>planId / 名稱 / 上期 price / periodMonths</b><b>addons.users / jquotaPackageId</b> 均需經 <code>Order → OrderDetails</code>(按 <code>Tag1Code</code>B=基礎、AdlU=增購用戶、jQ/j=jQuota、KYC反查、<code>usedQuota = QCountPurchase QCountLeft</code>。按本次決策,<b>這段反查應落在新端點 <code>queryRenewableTenantByEmail</code> 內(服務端一次算好)</b>,而非 BFF——後端已有 <code>GetCurrServiceInfo</code><code>OrderService.cs:398</code>)反查 <code>ActiveBPlans</code> 的現成範式可借用只需擴出加值項明細與上期價。BFF <code>routes/tenants.js</code> 因此退化為<b>字段直通 + 命中態none/unique包裝</b></p>
</div>
<!-- 5.6 -->
<h3 id="s5-6">5.6 單次查詢 single-query隨付即用 · Pay-As-You-Go <span class="pill partial">後端已支持 · 後端內部編排</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/single-query/options固定展示 &nbsp;·&nbsp; <span class="method post">POST</span> /amlPortal/plan/portal/GetPlanList <span class="small">{countryCode}PAYG 定價)</span> &nbsp;·&nbsp; <span class="method post">POST</span> /api/single-query &nbsp;<span class="small">(收單 + 支付)</span>&nbsp;⟶ 支付完成 ⟶&nbsp; <span class="tag">AML 後端內部</span> CreateConsumerLink → CreateConsumerOrder</div>
<div class="callout newreq">
<p><span class="lbl">本輪定案PAYG 定價 + 固定檢測項):</span>隨付即用的<b>檢測內容固定為 4 項、純信息展示,用戶不可勾選/取消</b>——<code>證件核驗</code><code>名單篩查(即 ES 檢測)</code><code>AI 增強型篩查</code><code>信貸記錄篩查(即失信人檢測)</code>(叫法後續可能再調整),確定性映射後端功能碼 <code>V / E / A / D</code>(無 <code>S</code> 風險評估問卷、<code>R</code> CDD 報告、<code>O</code> OCR<b>收款金額不再由 V2 分項單價相加,而是取自 AML <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價</b><code>/plans-jp</code> 頁傳 <code>countryCode:"JPN"</code><b>目前 <code>countryCode</code> 僅日本一國</b>,前端依 URL/地域推導),該價為<b>單一整包價</b>(非分項之和)。⇒ 原「op→功能碼非 1:1、公司查冊無獨立碼、V2 分項單價與後端 jQ 對齊」三處缺口<b>收斂</b>:檢測項固定 4 = E/A/V/D定價口徑統一到 <code>GetPlanList</code></p>
</div>
<div class="callout ok">
<p><span class="lbl">本輪更正(原「後端零支持」結論作廢;並釐清調用方):</span>單次查詢由 <b>AML 主模塊的 ConsumerPortal「即時檢測」</b>承接(<code>iCON.Abp.AML</code>)。<b>調用方已確認:這兩步不是 V2 BFF、也不是瀏覽器發起而是 AML 後端在「訂單支付完成後」、於 2C 租戶上下文<u>內部編排</u>執行</b>——<code>CreateConsumerLink</code>(建鏈接拿 <code>accessToken</code>)→ <code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件)。<b>justsolutionsWebV2 的職責止於「下單 + 收款」,完全不觸碰這兩個接口。</b></p>
</div>
<div class="callout note">
<p><span class="lbl">為何不能放在 V2 BFF</span><code>CreateConsumerLink</code> <b>非匿名</b><code>[Authorize(RealtimeScreeningManagement)]</code> + <code>[RequiresFeature(AML.ConsumerPortal.Enable)]</code><code>ConsumerPortalController.cs:337-340</code>),且在 <code>CurrentTenant</code> 下建鏈接。V2 BFF 只持「門戶訪客」憑證(<code>__tenant=Portal</code>),既無實時篩查權限、也非 2C 租戶上下文 ⇒ 調不動。放到後端內部後2C 租戶上下文與功能開關天然滿足,<b>鑑權不再是 BFF 的問題</b><code>CreateConsumerOrder</code> 雖是 <code>[AllowAnonymous]</code><code>:76-78</code>,憑 <code>accessToken</code>),但既然第 1 步已在後端,第 2 步同處後端內部串接最自然。</p>
</div>
<h4>職責邊界與調用鏈</h4>
<div class="flow">V2 · 瀏覽器 + BFF
&nbsp;&nbsp;GET /api/single-query/options ……… 查詢項目目錄V2 自定義,見 3.1
&nbsp;&nbsp;POST /api/single-query ………………… 建「待支付」單次查詢單(存 郵箱/主體/所選項目/金額)→ orderId
&nbsp;&nbsp;POST /api/payments/create …………… 收款(見 5.9;本輪支付除外)
──────────────── 支付完成webhook / 回調)────────────────
AML 後端 · 2C 租戶上下文內部編排](非 V2、非瀏覽器發起
&nbsp;&nbsp;固定 4 項檢測 → functionCodes = E/A/V/D證件核驗V·名單篩查ES=E·AI增強A·失信D無 S/R/O
&nbsp;&nbsp;① CreateConsumerLink { 郵箱, functionCodes:"EAVD", validHours } → accessToken
&nbsp;&nbsp;② CreateConsumerOrder { accessToken, objectType, individual|organization, functionCodes:["E","A","V","D"] }
&nbsp;&nbsp;→ 建訂單(直接 Paid) + 每碼一條 DetectionTask → MQ 異步跑檢測 → 完成發結果郵件</div>
<div class="callout warn">
<p><span class="lbl">待後端明確的銜接點(由後端定義,非 V2 落地):</span>「支付完成」如何把<b>單次查詢單的資料(郵箱/主體/所選項目)+ 支付結果</b>交給後端這段內部編排,常見兩種接法:<br>
<b>後端持單</b>V2 在 <code>POST /single-query</code> 時即把單推給後端一個「2C 收單/意向」端點落庫;支付 webhook 直接打到<b>後端</b>,後端據單觸發 ①②。<br>
<b>V2 持單、支付後通知後端</b>V2 收到支付成功後,調後端一個<b>專門的 2C 觸發端點</b>(其鑑權/形態由後端定義),該端點內部再跑 ①②。<br>
無論哪種,<b>①② 本身都是後端內部行為</b>V2 始終不直接調 <code>CreateConsumerLink</code>/<code>CreateConsumerOrder</code></p>
</div>
<h4>後端內部兩步payload 供參考)</h4>
<p><b>第 1 步 · 建即時檢測鏈接</b> <code>CreateConsumerLink</code><code>ConsumerPortalService.cs:964</code>以「結果接收郵箱」upsert <code>ConsumerCustomer</code>,在當前 2C 租戶下建鏈接 → 回 <code>ConsumerLinkDto</code>(含 <code>accessToken</code>)。</p>
<pre><code>{ "createConsumerCustomerDto": { "email":"result@company.com", "name":null, "phone":null, "remark":null },
"createConsumerLinkDto": { "functionCodes":"EAVD", "validHours":72,
"validEndTime":"2026-07-09T06:00:47.000Z" } }
// 回OperationDto.Success(ConsumerLinkDto{ accessToken, functionCodes, expireTime, tenantId, … })</code></pre>
<p><b>第 2 步 · 建訂單並即時檢測</b> <code>CreateConsumerOrder</code><code>:325</code>):據 token 定位鏈接與租戶 → 計價(jQ) → 建 Individual/Organization 實體 → 建 <code>ConsumerPortalOrder</code><b>直接置 PaymentStatus=Paid、OrderStatus=Pendding</b><code>:388</code>)→ 每個 functionCode 建一條 <code>DetectionTask</code> → 發 RabbitMQ 消息,由 <code>ProcessConsumerPortalOrder</code> <b>異步</b>跑檢測模塊,完成後 <code>SendConsumerLinkResultMail</code> 發結果郵件。</p>
<pre><code>{ "accessToken":"xxx", "objectType":"Individual",
"individual": { "fullNameEN":"张三", "fullNameZH":"", "dateOfBirth":{"year":0,"month":0,"day":0},
"address":"", "addressOfLiving":"", "nationalityCode":null,
"documentRequiredFileIds":[], "identityDocumentTypeCode":"", "identityDocumentNumber":"",
"gender":"", "phone":"", "email":"", "occupation":"", "entityIdentityDocuments":[] },
"organization": null,
"functionCodes": ["E","A","V","D"] } // ← 固定 4 項PAYG 檢測內容不可選)
// 回OperationDto.Success({ entity, order }) // order.Id / order.OrderCode 可作結果查詢鍵</code></pre>
<h4>V2 收集字段 ↔ 後端入參映射</h4>
<p class="small">V2 通過 <code>POST /single-query</code>(或銜接端點)把下列字段交給後端;後端在內部編排時據此組裝上面兩個 payload。</p>
<table>
<thead><tr><th>V2 收集字段</th><th>後端入參</th><th>說明</th></tr></thead>
<tbody>
<tr><td><code>email</code>(結果接收郵箱)</td><td><code>createConsumerCustomerDto.email</code></td><td>必填;後端按 email upsert <code>ConsumerCustomer</code>,並作結果郵件收件人</td></tr>
<tr><td><code>subjectType</code> <code>'individual'|'company'</code></td><td><code>objectType</code> <code>'Individual'|'Organization'</code></td><td>枚舉 <code>ObjectTypeEnum</code>Individual=0 / Organization=1company→Organization</td></tr>
<tr><td><code>subject</code>(單一名稱字段)</td><td><code>individual.fullNameEN</code><code>organization.fullNameEN</code></td><td>⚠️ V2 只收一個名稱;後端 <code>CreateIndividualDto/CreateOrganizationDto</code> 字段眾多但均可空 → <b>最小可用只填全名</b>,其餘留空/默認</td></tr>
<tr><td>固定 4 項檢測V2 不收選擇)</td><td>兩處 <code>functionCodes</code>(鏈接=字符串 <code>"EAVD"</code>、訂單=數組 <code>["E","A","V","D"]</code></td><td><b>固定映射</b>證件核驗→V、名單篩查ES→E、AI 增強→A、失信→D兩步一致</td></tr>
<tr><td>(鏈接有效期)</td><td><code>createConsumerLinkDto.validHours</code></td><td>後端據此算 <code>ExpireTime</code><b>樣例中的 <code>validEndTime</code> 後端 DTO 無此字段、被忽略</b></td></tr>
</tbody>
</table>
<div class="callout ok">
<p><span class="lbl">功能碼映射(本輪固定,非 1:1 問題消除):</span>PAYG 檢測內容<b>固定 4 項</b>、用戶不可選,直接確定性對應後端功能碼(後端只認 <b>E/A/V/D/S/R</b>O=OCR 排除PAYG 僅用前 4 個):</p>
<ul class="tight">
<li><code>證件核驗</code><code>V</code>(證件核驗)</li>
<li><code>名單篩查(即 ES 檢測)</code><code>E</code>ES 實體篩查)</li>
<li><code>AI 增強型篩查</code><code>A</code>AI 檢測) <span class="small">E、A 在後端合併為一次 <code>EntitiesInvestigation</code> 計費,<code>ConsumerPortalService.cs:277</code></span></li>
<li><code>信貸記錄篩查(即失信人檢測)</code><code>D</code>(失信查詢)</li>
</ul>
<p>⇒ 原「多個 op 坍縮為同一 E/A 計費」「公司查冊無獨立碼」的映射難點<b>不復存在</b>新方案無「公司查冊」項S/R 不在 PAYG<b>定價口徑亦統一</b>V2 收款金額 = <code>GetPlanList</code><code>countryCode</code> 的 PAYG(P2G) 方案價,不再與後端逐碼 jQ 扣費逐項對賬——後端 <code>CreateConsumerOrder</code> 仍按 2C 租戶 jQ 計費/扣減(僅 CHN/HKG 計 KYC jQ<code>:297</code>),但那是後端內部帳,與 V2 對外報價解耦。</p>
</div>
<h4>鑑權(均在後端內部滿足)</h4>
<table>
<thead><tr><th>方法</th><th>鑑權</th><th>後端內部編排下的落地</th></tr></thead>
<tbody>
<tr><td><code>CreateConsumerLink</code></td><td><b>非匿名</b><code>[Authorize(RealtimeScreeningManagement)]</code> + <code>[RequiresFeature(AML.ConsumerPortal.Enable)]</code><code>ConsumerPortalController.cs:337-340</code></td><td>在後端 2C 租戶上下文內執行 ⇒ <b>租戶上下文與功能開關天然滿足,無需 V2 憑證</b>。待確認的只是「用哪個 2C 租戶」及其 ConsumerPortal 功能/權限已開</td></tr>
<tr><td><code>CreateConsumerOrder</code></td><td><b>匿名</b> <code>[AllowAnonymous]</code><code>:76-78</code></td><td><code>accessToken</code> 定位租戶;後端內部緊接第 1 步串行調用</td></tr>
</tbody>
</table>
<h4>結果回取(可選)</h4>
<ul class="tight">
<li>檢測<b>異步</b>完成後後端自動發結果郵件(滿足 V2「結果將發送至此郵箱」文案V2 可不輪詢。</li>
<li>若 V2 要頁內展示:<code>POST /api/aml/ConsumerPortal/Get2CRecordDetail/{accessToken}</code><code>/Get2CRecordDetailByOrderId/{orderId}</code>(均匿名)取檢測記錄;<code>/CheckAndGenerateReport</code> 生成 CDD 報告 PDF。<span class="small">(此類<b>只讀</b>回取因匿名,才可能由 V2/瀏覽器直取。)</span></li>
</ul>
<div class="callout gap">
<p><span class="lbl">仍存在的缺口 / 落地要點:</span></p>
<p><b>銜接點</b>V2「支付完成」與後端內部編排的對接上方 ①/② 兩種接法)由後端定義端點/webhook 承接。</p>
<p><b>op→功能碼映射</b><span class="pill done">已收斂</span>:檢測內容固定 4 項、確定性映射 <code>E/A/V/D</code>(無「公司查冊」、無 S/RV2 只需固定傳 <code>functionCodes="EAVD"</code>(或後端直接寫死該 4 項)。</p>
<p><b>2C 租戶</b>:確認/建立隨付即用專用租戶、開 <code>AML.ConsumerPortal.Enable</code> 功能、配實時篩查權限——供後端內部編排落上下文(<b>不再需要在 V2 BFF 配該租戶憑證</b>)。</p>
<p><b>對外定價</b><span class="pill done">已確認</span>V2 收款金額 = <code>GetPlanList</code><code>countryCode</code><code>/plans-jp→JPN</code>)返回的 PAYG(P2G) 方案價,<b>為單一整包價</b>(非分項之和),前端載入期取價、純展示 4 項檢測。後端 <code>CreateConsumerOrder</code> 仍把訂單直接置 <code>Paid</code> 並按 2C 租戶 jQ 扣費(<code>GetPriceTwoC</code>/線上支付分支已注釋 <code>:196</code>)——屬後端內部帳,與 V2 對外報價解耦。<b>現階段 <code>countryCode</code> 僅日本JPN</b>:即只有 <code>/plans-jp</code> 有 PAYG 定價,其餘國家待種 P2G 方案 + PlanDetail 後再開(機制沿用,不需改前端契約)。</p>
<p><b>本地環境</b>:需該租戶 + 檢測引擎(iCS)/RabbitMQ 就緒,<code>ProcessConsumerPortalOrder</code> 才能真正跑出結果;否則本地 single-query 可繼續走 mock僅在對接環境驗證真調用。</p>
</div>
<div class="callout note">
<p><span class="lbl">V2 側建議形態:</span><code>/single-query/options</code> 收斂為<b>固定 4 項純展示</b>(可前端硬編碼或後端返回固定項,<b>不含 price</b>PAYG 收款價由前端載入期<b>依 URL/地域推導 <code>countryCode</code> 調 <code>GetPlanList</code></b>取 PAYG(P2G) 方案價(或經 BFF <code>/plans/catalog</code> 帶 countryCode 一併取回)。<code>/single-query</code>(收單,返回 orderId契約不變<b>不新增對 ConsumerPortal 的代理路由</b>。V2 只需把「支付完成」與後端銜接(見上 ①/②),<b>兩步真調用全部在後端內部</b>——前端與 BFF 無需感知 CreateConsumerLink/CreateConsumerOrder<b>不傳檢測項選擇</b>(固定 4 項在後端寫死或由 V2 固定傳 <code>EAVD</code>)。</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 done">非必填</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 ok">
<p><span class="lbl">BR/CI 非必填(已定決策):</span>plans-plus 新購允許空 BR/CI——含 <b>corp 主體</b>BR 維持「可選」、可空)與 <b>individual 主體</b><code>br</code> 恒空、無 CI。實現注释 <code>CreateOrder</code><code>OrderService.cs:154</code><code>:190</code> 兩處必填校驗(<code>if (BR 空 &amp;&amp; CI 空) return "…required"</code>);緊隨的唯一性校驗 <code>ExistsByOrganizationBRCI</code> 對「BR、CI 皆空」返回 <code>false</code><code>:1124</code><b>空值安全通過,無需其它改動</b></p>
<p>⚠️ <b>核對現狀</b>:當前 working copy 的 <code>:154</code>/<code>:190</code> 仍為 <b>active</b>;若對接/部署環境尚未放開,空 BR/CI 會被攔,需先應用該改動。</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 partial">待應用/核對</span></td><td>決策=非必填:注释 <code>OrderService.cs:154,190</code> 兩處必填校驗(保留 :191 唯一性,空值安全)。<b>當前 working copy 這兩處仍 active</b>——需確認已在對接/部署環境放開</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-queryPAYG 隨付即用)</td><td><span class="pill done">前端+mock 已實現</span> <span class="pill partial">後端對接待接</span></td><td>本輪落地<b>新模型</b><b>檢測內容固定 4 項純展示、用戶不可選</b><code>/single-query/options</code> 去單價、加 <code>code</code>=V/E/A/D<b>收款價取 <code>GetPlanList</code>(countryCode) 的 PAYG(P2G) 單一整包價</b>——新增原始端點 <code>POST /amlPortal/plan/portal/GetPlanList</code> mock返回結構<b>與真後端逐字段一致</b><code>{code,msg,data:{totalCount,items:[…planDetails[].price]},version}</code>),切真後端免改前端。<code>/plans-jp→JPN</code>目前唯一有價國家mock ¥3,000 佔位);提交帶 <code>functionCodes="EAVD"</code>、幣種隨方案JPY。改動<code>public/js/plans-plus.js · plans-plus.html · translations.js</code> + <code>server/mock/plans-plus.js</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>
<p class="small"><b>PAYGsingle-query本輪驗證<code>APP_ENV=test</code> mock 模式):</b><code>/single-query/options</code> 返固定 4 項、無單價;<code>POST /amlPortal/plan/portal/GetPlanList{countryCode:"JPN"}</code>→P2G ¥3,000JPY<code>{HKG}</code>→空目錄(價格不可用);抽取 <code>plans-plus.js</code> 實際 <code>extractPaygPlan/paygPlanPrice/fmtMoneyCur</code> 跑真實響應:<code>P2G / 3000 / JPY / ¥3,000</code> 通過、HKG→<code>null/0</code>(禁用提交)。<code>GET /plans-jp</code> HTTP 200、含 <code>ppSqPriceValue</code> 橫幅。<span style="color:var(--muted)">(真後端 GetPlanList 對接與 ConsumerPortal 支付後編排見 5.6,仍待後端。)</span></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><b>後端已支持</b>ConsumerPortal 即時檢測);<b>兩步由 AML 後端在支付完成後內部編排</b>CreateConsumerLink→CreateConsumerOrderV2 只下單+收款。<b>檢測內容固定 4 項純展示不可選</b>V/E/A/D<b>定價取 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b><code>/plans-jp→JPN</code>),映射與 2C 租戶均在後端(見 5.6</td><td><span class="pill partial">改造</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/CIBR/CI 已定非必填 ⇒ 不再阻塞(待 :154/:190 放開已應用)</td><td><span class="pill done">非阻塞</span></td></tr>
<tr><td>6</td><td><b>BR/CI 非必填</b>(已定)</td><td>注释 <code>OrderService.cs:154,190</code>(保留唯一性);當前 working copy 仍 active待確認已放開</td><td><span class="pill partial">待應用/核對</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><b>已定:後端新增 <code>queryRenewableTenantByEmail</code></b>(現端點 DB 層只按租戶名搜、郵箱查後回填→不能按郵箱過濾);不再多租戶消歧;新端點聚合 currentSubscription+referrer見 5.5</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> → 字段直通 + 命中態(none/unique) 包裝currentSubscription/referrer 聚合由後端算好)</td><td><b>新增</b> queryRenewableTenant<b>ByEmail</b></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>:已定去除 <code>CreateOrder</code> 的 BR/CI 必填校驗(注释 :154/:190保留唯一性請確認已在對接/部署環境應用(當前 working copy 仍 active</li>
<li><b>按郵箱查可續費租戶</b><span class="pill todo">已定 · 待實現</span> 新增 <code>POST Order/portal/queryRenewableTenantByEmail</code>(入參 <code>{Email}</code><code>[AbpAutoAuth("Portal")]</code>)——先按 <code>UserName=='admin' &amp;&amp; Email==</code> 定位租戶(可復用現 <code>GetAllUsers()</code> 全庫掃描、改按郵箱過濾,掃描成本可接受),返回聚合 currentSubscriptionplanId / 上期 price / 已購加值項,反查思路借用 <code>GetCurrServiceInfo</code>+ referrerAgentorCode/Name。詳見 5.5。</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>:已用 ConsumerPortal 即時檢測,<b>兩步由後端內部編排</b>(見 5.6)。<b>檢測內容固定 4 項V/E/A/D、定價取 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b>。待後端確認:① V2「支付完成」與後端編排的銜接端點/webhook② 隨付即用專用「2C 租戶」+ <code>AML.ConsumerPortal.Enable</code> 功能 + 實時篩查權限(供後端落上下文,<b>V2 無需持該租戶憑證</b>);③ <s>op→功能碼映射</s><b>已定固定 EAVD</b>——僅需確認後端是「V2 固定傳 EAVD」還是「後端寫死」<s>收款金額對齊</s><b>已確認</b>PAYG = <b>單一整包價</b><b>目前 <code>countryCode</code> 僅日本JPN</b>,僅 <code>/plans-jp</code> 有 P2G 方案,餘國待種 PlanDetail 後再開。</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 已定非必填:注释 :154/:190</td><td>新租戶下單至「已提交成功」(支付除外)</td></tr>
<tr><td>P3 · 續費/加購</td><td>tenants/lookup依賴後端新端點 <code>queryRenewableTenantByEmail</code>+ subscribe(renew/topup)</td><td>續費/加購閉環(支付除外);<b>阻塞於後端新端點</b></td></tr>
<tr><td>P4 · 單次查詢/聯絡/支付</td><td>single-query支付後接 ConsumerPortal 兩步CreateConsumerLink→CreateConsumerOrder+ 聯絡我們(接 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 done">已定非必填</span> plans-plus 新購(含個人主體 / 空 BR/CI可下單。實現注释 <code>CreateOrder</code><code>OrderService.cs:154,190</code>(保留 :191 唯一性,空值安全)。<b>待辦</b>:確認該放開已在對接/部署環境生效(<b>當前 working copy 兩處仍 active</b>);並知悉全局影響(旧站共用 <code>CreateOrder</code>,空 BR 不再攔截)。</li>
<li><b>單次查詢:</b><span class="pill partial">已定方向</span> 後端<b>已支持</b>——ConsumerPortal 即時檢測;<b>調用方AML 後端內部編排</b>(支付完成後 <code>CreateConsumerLink</code><code>CreateConsumerOrder</code>V2 只下單+收款,見 5.6)。<b>本輪再定</b>:檢測內容<b>固定 4 項純展示不可選</b>證件核驗V / 名單篩查ES=E / AI 增強A / 失信D<b>收款金額取 <code>GetPlanList</code><code>countryCode</code> 的 PAYG(P2G) 方案價</b><code>/plans-jp→JPN</code>)。<b>待後端確認</b>:① V2「支付完成」與後端編排的銜接方式後端持單 + 收 webhook或 V2 支付後調後端專門 2C 觸發端點);② 隨付即用專用「2C 租戶」+ <code>AML.ConsumerPortal.Enable</code> + 實時篩查權限(供後端落上下文,<b>V2 無需該租戶憑證</b>);③ <code>functionCodes="EAVD"</code> 由 V2 固定傳、還是後端寫死;④ <span class="pill done">已確認</span> PAYG = <b>單一整包價</b>(非分項之和);<b>目前 <code>countryCode</code> 僅日本JPN</b>,僅 <code>/plans-jp</code> 有 P2G 方案 + PlanDetail餘國後續再種。</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><span class="pill done">已定新增後端端點</span> <code>queryRenewableTenantByEmail(email)</code> 直接返回「租戶 + currentSubscriptionplanId/上期價/已購加值項)+ referrer」聚合見 5.5)。<b>待後端確認</b>:① 「admin 郵箱」是否恆等於 <code>UserName=='admin'</code> 用戶的 Email<code>queryRenewableTenant</code> 即此語義)——若存在非 <code>admin</code> 用戶名的租戶管理員,需改按角色定位;② 過期租戶是否照樣返回(供前端做臨期/過期展示 + topup 阻斷);③ 聚合 DTO 形態(新 <code>RenewableTenantDto</code> vs 擴 <code>TenantPropertyDto</code>)。</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>iCON.Abp.AML</code><code>ConsumerPortalController</code><code>ConsumerPortalService</code>(單次查詢即時檢測)/ <code>CustomIdentityUserController</code> / 旧站 <code>PlanService</code> / <code>appsettings.local.json</code> 與本地數據庫。涉及後端行為BR/CI 校驗、續費續價、topup / KYC 計費語義、單次查詢 2C 計費與鑑權)以實際接口 + 後端確認為準。
</p>
</div>
</body>
</html>