701 lines
77 KiB
HTML
701 lines
77 KiB
HTML
<!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>BFF(Backend-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 →
|
||
├─ APP_ENV=test ………………… server/mock/routes.js + mock/plans-plus.js(內存假數據)
|
||
├─ dev/stag/prod + PLANS_PLUS_MOCK=true … 先掛 mock/plans-plus.js,其餘 /api/* 才透傳後端
|
||
└─ dev/stag/prod(後端就緒後)…… server/routes/*.js(BFF)→ 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=company,br/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/email(corp 另需 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 → <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 → <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<PlanDto></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 → <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 → <span class="method get">GET</span> /api/identity/users/SearchUserByCodeAndType/{code}/true (+ GetPlanList)</div>
|
||
<p>前端要 <code>{ success, found, data:{ code, name, tiers:[tag2Code…], email, phone } }</code>。後端 <code>SearchUserByCodeAndType</code> 按 <code>UserCode</code> 精確匹配,返回 <code>List<AppUserDto></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 code(EF 屬性 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= → <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<TenantPropertyDto></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>QCountPurchase−QCountLeft</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 · <span class="method post">POST</span> /api/single-query → <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 → 按 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 空 && 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 → <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<CreateFeedbackDto,Feedback></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 → <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.json):APP_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>new(corp/個人/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/PhoneNumber,BFF 帶出</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>能否新增「按管理員郵箱查可續費租戶」,直接帶 currentSubscription(planId/上期價/已購加值項)+ 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>
|