docs(justsolutions): add API analysis for plans-plus real backend integration
Add docs/justsolutionsWebV2/plan-plus-api分析.html mapping each plans-plus mock endpoint to the real AMLPortal backend (the same endpoints the old justsolutionsWeb PlanService uses), via the V2 BFF pattern. Covers per-endpoint field mappings, transformation notes, and the gaps: - tenant lookup is by email but queryRenewableTenant searches by name - the new 'topup' type has no backend endpoint - CreateOrder/TenantRenewal PlanList needs PlanId+PlanDetailId back-fill - CreateOrder ignores AdminPassword; BR-or-CI required; no bestValue/note fields - QFPay payment signing must move server-side Also includes the justsolutionsWeb API-call inventory (api-calls.md/html) that this analysis builds on.main
parent
94c2469f56
commit
4519399cb7
|
|
@ -0,0 +1,427 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>justsolutionsWeb — 后端 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;
|
||||||
|
--get: #1a7f37;
|
||||||
|
--post: #9a6700;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "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: 1040px;
|
||||||
|
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: 28px;
|
||||||
|
}
|
||||||
|
header.page h1 { margin: 0 0 8px; font-size: 26px; }
|
||||||
|
header.page p { margin: 4px 0; color: var(--muted); }
|
||||||
|
.toc {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 28px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.toc h2 { font-size: 15px; 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 {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 32px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
h2 { font-size: 21px; margin: 0 0 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
||||||
|
h3 { font-size: 17px; margin: 24px 0 12px; }
|
||||||
|
p { margin: 12px 0; }
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th { background: var(--th-bg); font-weight: 600; }
|
||||||
|
tbody tr:nth-child(even) { background: var(--row-alt); }
|
||||||
|
code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cf222e;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
td code { color: #0550ae; }
|
||||||
|
.method { font-weight: 700; font-size: 12px; letter-spacing: .5px; }
|
||||||
|
.method.get { color: var(--get); }
|
||||||
|
.method.post { color: var(--post); }
|
||||||
|
.num { text-align: center; font-variant-numeric: tabular-nums; color: var(--muted); }
|
||||||
|
.callout {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 16px 0;
|
||||||
|
border-left: 4px solid;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
|
||||||
|
.callout.note { background: var(--note-bg); border-color: var(--note-border); }
|
||||||
|
.callout p { margin: 6px 0; }
|
||||||
|
ul.tight li { margin: 5px 0; }
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 1px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
footer { text-align: center; color: var(--muted); font-size: 13px; margin-top: 30px; }
|
||||||
|
.small { font-size: 13px; color: var(--muted); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>justsolutionsWeb — 后端 API 调用清单</h1>
|
||||||
|
<p>前端:<code>justsolutions-web</code> v1.2.0 · Angular 16 · Angular CLI 16.2.14</p>
|
||||||
|
<p class="small">基于源码静态分析(<code>src/app</code> 下所有 <code>*.ts</code>)整理。共 14 个后端端点。</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="toc">
|
||||||
|
<h2>目录</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#arch">调用架构概览</a></li>
|
||||||
|
<li><a href="#list">调用清单(按端点汇总)</a></li>
|
||||||
|
<li><a href="#auth">鉴权(OAuth Token)</a></li>
|
||||||
|
<li><a href="#amlquery">AML 查询模块</a></li>
|
||||||
|
<li><a href="#contact">联系我们模块</a></li>
|
||||||
|
<li><a href="#plans">套餐与下单模块</a></li>
|
||||||
|
<li><a href="#prefix">端点前缀分布</a></li>
|
||||||
|
<li><a href="#flow">速查 · 典型流程</a></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="arch">
|
||||||
|
<h2>1. 调用架构概览</h2>
|
||||||
|
<p>所有业务 API 调用都收口在统一的 <strong><code>HttpService</code></strong>(<code>src/app/services/http.service.ts</code>):</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong>基地址</strong>:请求时把相对路径拼到 <code>environment.apis.url</code> 之后,即 <code>environment.apis.url + request.url</code>。</li>
|
||||||
|
<li><strong>鉴权</strong>:自动从 <code>localStorage</code> 或 <code>sessionStorage</code> 读取 <code>aml_access_token</code>,存在时加上请求头 <code>Authorization: Bearer <token></code>。</li>
|
||||||
|
<li><strong>请求模型</strong>:统一使用 <code>Http.Request<T></code>(<code>src/app/models/http.ts</code>),字段含 <code>method</code> / <code>url</code> / <code>body</code> / <code>params</code> / <code>headers</code> / <code>responseType</code> 等。</li>
|
||||||
|
</ul>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><strong>例外</strong>:支付跳转直接 <code>location.href</code> 重定向到 QFPay 外部网关(见第 6 节),不经过 <code>HttpService</code>。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>1.1 环境与基地址配置</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>环境文件</th><th><code>apis.url</code>(API 基地址)</th><th><code>apis.amlUrl</code>(AML 门户来源)</th><th><code>apis.test</code></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>environment.ts</code>(本地默认)</td><td><code>https://api-aml.iconsz.com</code></td><td><code>https://aml.iconsz.com</code></td><td><code>true</code></td></tr>
|
||||||
|
<tr><td><code>environment.dev.ts</code></td><td><code>https://api-aml.iconsz.com</code></td><td><code>https://aml.iconsz.com</code></td><td><code>true</code></td></tr>
|
||||||
|
<tr><td><code>environment.prod.ts</code></td><td><code>https://api.justsolutions.ai</code></td><td><code>https://aml.justsolutions.ai</code></td><td><code>false</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><code>apis.amlUrl</code>:仅用于 <code>plans</code> 页面与父窗口(AML 门户 iframe 宿主)之间的 <code>postMessage</code> 通信,<strong>并非 HTTP 端点</strong>。</li>
|
||||||
|
<li><code>apis.test</code>:为 <code>true</code> 时支付金额固定写死为 <code>10</code>(分),用于测试支付(见第 6 节)。</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="list">
|
||||||
|
<h2>2. 调用清单(按端点汇总)</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th class="num">#</th><th>方法</th><th>端点(相对 <code>apis.url</code>)</th><th>用途</th><th>前端方法</th><th>所在文件</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="num">1</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/connect/token?__tenant=2C</code></td>
|
||||||
|
<td>OAuth 取 access token</td>
|
||||||
|
<td><code>getToken()</code></td>
|
||||||
|
<td><code>aml-query.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">2</td><td><span class="method get">GET</span></td>
|
||||||
|
<td><code>/api/aml/ConsumerPortal/GetPrice/{inputCode}</code></td>
|
||||||
|
<td>查询消费者查询价格</td>
|
||||||
|
<td><code>getPrice()</code></td>
|
||||||
|
<td><code>aml-query.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">3</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/customer/CreateFeedback</code></td>
|
||||||
|
<td>提交联系表单反馈</td>
|
||||||
|
<td><code>sendContactEmailAndReturnFeedback()</code></td>
|
||||||
|
<td><code>contact.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">4</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/aml/emailQueue/CreateEmailQueue</code></td>
|
||||||
|
<td>发送通知邮件(管理员 + 用户回执两次调用)</td>
|
||||||
|
<td><code>sendContactEmail()</code> / <code>sendContactByReturnEmail()</code></td>
|
||||||
|
<td><code>contact.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">5</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/plan/portal/GetPlanList</code></td>
|
||||||
|
<td>获取套餐列表(基础/jQ/加人/KYC)</td>
|
||||||
|
<td><code>getAllPlanList()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">6</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/GetEditionList</code></td>
|
||||||
|
<td>获取系统定义的 edition(所属行业)列表</td>
|
||||||
|
<td><code>getEditionList()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">7</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/getCategoryByTypes</code></td>
|
||||||
|
<td>获取国家/地区列表(<code>typeCodes: ['COUNTRY']</code>)</td>
|
||||||
|
<td><code>getCountryList()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">8</td><td><span class="method get">GET</span></td>
|
||||||
|
<td><code>/api/identity/users/SearchUserByCodeAndType/{userCode}/true</code></td>
|
||||||
|
<td>校验优惠码/代理商是否存在</td>
|
||||||
|
<td><code>getAgentor()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">9</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/queryRenewableTenant</code></td>
|
||||||
|
<td>查询可续费租户</td>
|
||||||
|
<td><code>queryTenant()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">10</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/ExistsByOrganizationBRCI</code></td>
|
||||||
|
<td>校验商业登记号(BR)/公司编号(CI)是否已存在</td>
|
||||||
|
<td><code>checkBRCI()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">11</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/customer/portal/CheckEmailExists/{email}</code></td>
|
||||||
|
<td>校验管理员邮箱是否已存在</td>
|
||||||
|
<td><code>checkEmailExist()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">12</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/CreateOrder</code></td>
|
||||||
|
<td>新租户下单</td>
|
||||||
|
<td><code>createOrder()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">13</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/TenantRenewal</code></td>
|
||||||
|
<td>租户续费下单</td>
|
||||||
|
<td><code>tenantRenewal()</code></td>
|
||||||
|
<td><code>plan.service.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="num">14</td><td><span class="method post">POST</span></td>
|
||||||
|
<td><code>/api/amlPortal/Order/portal/PaymentWebhook</code></td>
|
||||||
|
<td>支付完成回调(作为 <code>notify_url</code> 传给 QFPay,由其服务端回调,<strong>非前端直接调用</strong>)</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td><code>plan4.component.ts</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="auth">
|
||||||
|
<h2>3. 鉴权(OAuth Token)</h2>
|
||||||
|
<p><strong>端点 1</strong> — <code>POST /connect/token?__tenant=2C</code></p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li>位置:<code>AmlQueryService</code>。在该服务<strong>构造函数中自动触发</strong>:当 <code>localStorage</code> 无 <code>aml_access_token</code> 时,调用 <code>getToken()</code> 并把返回的 <code>access_token</code> 写入 <code>localStorage</code>。</li>
|
||||||
|
<li>请求头:<code>Content-Type: application/x-www-form-urlencoded</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>Body(<code>x-www-form-urlencoded</code>,硬编码凭证):</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>字段</th><th>值</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>grant_type</code></td><td><code>password</code></td></tr>
|
||||||
|
<tr><td><code>response_type</code></td><td><code>token</code></td></tr>
|
||||||
|
<tr><td><code>username</code></td><td><code>customer1</code></td></tr>
|
||||||
|
<tr><td><code>password</code></td><td><code>1qaz@WSX</code></td></tr>
|
||||||
|
<tr><td><code>scope</code></td><td><code>FX</code></td></tr>
|
||||||
|
<tr><td><code>client_id</code></td><td><code>customer1_client</code></td></tr>
|
||||||
|
<tr><td><code>client_secret</code></td><td><code>1qaz@WSX</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout warn">
|
||||||
|
<p>⚠️ 用户名/密码/client secret 以明文硬编码在前端源码中(<code>aml-query.service.ts:47-51</code>),属于公开网站内置的固定访客凭证。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="amlquery">
|
||||||
|
<h2>4. AML 查询模块(aml-query)</h2>
|
||||||
|
<p><strong>端点 2</strong> — <code>GET /api/aml/ConsumerPortal/GetPrice/{inputCode}</code></p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li>调用方:<code>aml-query-layout.component.ts</code>(<code>ngOnInit</code> 时以 <code>getPrice('E')</code> 拉取价格)。</li>
|
||||||
|
<li>价格的真正计算逻辑在 <code>AmlQueryService.calculatePrice()</code>(本地计算,不走接口)。</li>
|
||||||
|
</ul>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><code>aml-query/pages/result/result.component.ts</code>(评估报告页)当前使用<strong>本地 mock 数据 / <code>Math.random()</code></strong> 生成风险评估结果,PDF 下载指向 <code>assets/sample-report.pdf</code>,<code>apiIntegration()</code> 弹窗里的 <code>api.credit-risk.example.com</code> 等仅为示例文案,<strong>不是真实后端调用</strong>。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="contact">
|
||||||
|
<h2>5. 联系我们模块(contact)</h2>
|
||||||
|
<p>调用方:<code>contact/pages/contact/contact.component.ts</code> 提交表单时触发。</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong>端点 3</strong> <code>POST /api/amlPortal/customer/CreateFeedback</code>:保存反馈记录。
|
||||||
|
<br><span class="small">Body(<code>CreateFeedbackParam</code>):<code>companyName</code>、<code>customerName</code>、<code>customerEmail</code>、<code>typeOfQuery</code>、<code>message</code>、<code>userHost</code>、<code>destEmail</code>、<code>emailSubject</code>、<code>emailBody</code>。</span>
|
||||||
|
</li>
|
||||||
|
<li><strong>端点 4</strong> <code>POST /api/aml/emailQueue/CreateEmailQueue</code>:进入邮件发送队列,被调用<strong>两次</strong>:
|
||||||
|
<ul class="tight">
|
||||||
|
<li><code>sendContactEmail()</code> → 通知管理员(收件人 <code>environment.email.contact</code>)。</li>
|
||||||
|
<li><code>sendContactByReturnEmail()</code> → 给提交者发送回执邮件(收件人为用户填写邮箱)。</li>
|
||||||
|
</ul>
|
||||||
|
<span class="small">Body(<code>CreateEmailQueueParam</code>):<code>subject</code>、<code>to</code>、<code>body</code>(HTML)、<code>businessType: 2100</code>、<code>businessId: ''</code>。</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="plans">
|
||||||
|
<h2>6. 套餐与下单模块(plans / Order)</h2>
|
||||||
|
<p><code>PlanService</code> 集中了套餐查询、校验与下单接口。各端点的页面调用关系:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>端点</th><th>调用页面</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>getAllPlanList()</code><span class="pill">端点 5</span></td><td><code>plans.component.ts</code>(进入套餐页 / 收到父窗口 <code>postMessage</code> 后)</td></tr>
|
||||||
|
<tr><td><code>getEditionList()</code><span class="pill">端点 6</span></td><td><code>plans.component.ts</code>、<code>plan3.component.ts</code></td></tr>
|
||||||
|
<tr><td><code>getCountryList()</code><span class="pill">端点 7</span></td><td><code>plans.component.ts</code>、<code>plan3.component.ts</code></td></tr>
|
||||||
|
<tr><td><code>getAgentor()</code><span class="pill">端点 8</span></td><td><code>plan3.component.ts</code>(输入优惠码校验)</td></tr>
|
||||||
|
<tr><td><code>queryTenant()</code><span class="pill">端点 9</span></td><td><code>plan1.component.ts</code>、<code>plan3.component.ts</code>(续费选租户)</td></tr>
|
||||||
|
<tr><td><code>checkBRCI()</code><span class="pill">端点 10</span></td><td><code>plan3.component.ts</code></td></tr>
|
||||||
|
<tr><td><code>checkEmailExist()</code><span class="pill">端点 11</span></td><td><code>plan1.component.ts</code>、<code>plan3.component.ts</code></td></tr>
|
||||||
|
<tr><td><code>createOrder()</code><span class="pill">端点 12</span></td><td><code>plan4.component.ts</code>(<code>type === 'new'</code>)</td></tr>
|
||||||
|
<tr><td><code>tenantRenewal()</code><span class="pill">端点 13</span></td><td><code>plan4.component.ts</code>(<code>type === 'renew'</code>)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>关键请求体</h3>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong><code>getAllPlanList</code></strong>(<code>GetPlanListParam</code>):<code>pageIndex: 0</code>、<code>pageSize: 9999</code>、<code>tag1List: ['B','jQ','j','AdlU','KYC']</code>、<code>tag2List</code>/<code>tag3List</code>(由调用方传入)、<code>agentUserId</code>。</li>
|
||||||
|
<li><strong><code>getCountryList</code></strong>:<code>{ typeCodes: ['COUNTRY'] }</code>。</li>
|
||||||
|
<li><strong><code>checkBRCI</code></strong>:通过 <code>params</code> 传 <code>br</code> / <code>ci</code>(query string)。</li>
|
||||||
|
<li><strong><code>createOrder</code></strong>(<code>CreateOrderParam</code>):<code>planList[]</code>(<code>planId</code>/<code>planDetailId</code>/<code>pcs</code>)、<code>tenantName</code>、<code>tenantAdminEmail</code>、<code>jurisdiction</code>、<code>organizationBR</code>、<code>organizationCI</code>、<code>organizationReference</code>、<code>contactPerson</code>、<code>companyAddress</code>、<code>phone</code>、<code>effectiveStartTime</code>、<code>agentorId</code>、<code>isOfflinePayment: false</code>、<code>isAgentBehalf</code>。</li>
|
||||||
|
<li><strong><code>tenantRenewal</code></strong>(<code>TenantRenewalParam</code>):<code>targetTenantID</code>、<code>planList[]</code>、<code>agentorId</code>、<code>tenantAdminEmail</code>、<code>isOfflinePayment: false</code>、<code>isAgentBehalf</code>。</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>6.1 支付跳转(QFPay 外部网关,不经过 HttpService)</h3>
|
||||||
|
<p>位置:<code>plan4.component.ts</code> 的 <code>payFn()</code>。下单成功(<code>createOrder</code>/<code>tenantRenewal</code> 返回 <code>code === 0</code>)后:</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li>若订单 <code>paymentStatus === 200</code> 或 <code>txamt === 0</code> → 直接跳转 <code>plans-plus/payment-success</code>。</li>
|
||||||
|
<li>否则通过 <code>location.href</code> 重定向到 QFPay 收银台:<br>
|
||||||
|
<code>https://openapi-hk.qfapi.com/checkstand/#/?<参数>&sign=<sha256></code></li>
|
||||||
|
</ul>
|
||||||
|
<p>主要参数:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>参数</th><th>值</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>appcode</code></td><td><code>environment.qfpay.appcode</code>(固定值)</td></tr>
|
||||||
|
<tr><td><code>goods_name</code></td><td>下单返回的 <code>planName</code></td></tr>
|
||||||
|
<tr><td><code>out_trade_no</code></td><td>下单返回的 <code>orderCode</code></td></tr>
|
||||||
|
<tr><td><code>paysource</code></td><td><code>remotepay_checkout</code></td></tr>
|
||||||
|
<tr><td><code>return_url</code></td><td><code>{baseUrl}/plans-plus/payment-success</code></td></tr>
|
||||||
|
<tr><td><code>failed_url</code></td><td><code>{baseUrl}/plans-plus/payment-failure</code></td></tr>
|
||||||
|
<tr><td><code>notify_url</code></td><td><code>{apis.url}/api/amlPortal/Order/portal/PaymentWebhook</code>(端点 14,QFPay 服务端回调后端)</td></tr>
|
||||||
|
<tr><td><code>txamt</code></td><td><code>apis.test ? 10 : planPrice * 100</code>(单位:分)</td></tr>
|
||||||
|
<tr><td><code>txcurrcd</code></td><td><code>HKD</code></td></tr>
|
||||||
|
<tr><td><code>sign</code></td><td><code>sha256(<排序后的参数串> + api_key)</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="prefix">
|
||||||
|
<h2>7. 端点前缀分布</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前缀</th><th>含义</th><th>涉及端点</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>/connect/*</code></td><td>OpenIddict/IdentityServer OAuth</td><td>端点 1</td></tr>
|
||||||
|
<tr><td><code>/api/aml/*</code></td><td>AML 主模块(消费者查询、邮件队列)</td><td>端点 2、4</td></tr>
|
||||||
|
<tr><td><code>/api/amlPortal/*</code></td><td>AML 门户模块(客户、套餐、订单)</td><td>端点 3、5、6、7、9、10、11、12、13、14</td></tr>
|
||||||
|
<tr><td><code>/api/identity/*</code></td><td>ABP Identity(用户)</td><td>端点 8</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="flow">
|
||||||
|
<h2>8. 速查 · 典型流程(按调用次序)</h2>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><strong>应用启动</strong>:<code>AmlQueryService</code> 构造 → 端点 1 取 token。</li>
|
||||||
|
<li><strong>AML 查询页加载</strong>:端点 2 取价格。</li>
|
||||||
|
<li><strong>联系页提交</strong>:端点 3 + 端点 4(×2)。</li>
|
||||||
|
<li><strong>套餐购买流程</strong>:
|
||||||
|
<ol>
|
||||||
|
<li>进入套餐页:端点 5、6、7。</li>
|
||||||
|
<li>填写租户/续费信息:端点 8(优惠码)、9(租户)、10(BR/CI)、11(邮箱)。</li>
|
||||||
|
<li>确认下单:端点 12(新购)或 13(续费)。</li>
|
||||||
|
<li>跳转 QFPay 支付 → 支付完成后 QFPay 回调端点 14。</li>
|
||||||
|
</ol>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
justsolutionsWeb 后端 API 调用清单 · 基于源码静态分析整理
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
# justsolutionsWeb — 后端 API 调用清单
|
||||||
|
|
||||||
|
> 本文档整理 `justsolutionsWeb`(Angular 16,`justsolutions-web` v1.2.0)前端对后端 API 的全部调用。
|
||||||
|
> 统计基于源码静态分析(`src/app` 下所有 `*.ts`)。
|
||||||
|
|
||||||
|
## 1. 调用架构概览
|
||||||
|
|
||||||
|
所有业务 API 调用都收口在统一的 **`HttpService`**(`src/app/services/http.service.ts`):
|
||||||
|
|
||||||
|
- **基地址**:请求时把相对路径拼到 `environment.apis.url` 之后,即 `environment.apis.url + request.url`。
|
||||||
|
- **鉴权**:自动从 `localStorage` 或 `sessionStorage` 读取 `aml_access_token`,存在时加上请求头 `Authorization: Bearer <token>`。
|
||||||
|
- **请求模型**:统一使用 `Http.Request<T>`(`src/app/models/http.ts`),字段含 `method` / `url` / `body` / `params` / `headers` / `responseType` 等。
|
||||||
|
|
||||||
|
> 例外:支付跳转直接 `location.href` 重定向到 QFPay 外部网关(见第 6 节),不经过 `HttpService`。
|
||||||
|
|
||||||
|
### 1.1 环境与基地址配置
|
||||||
|
|
||||||
|
| 环境文件 | `apis.url`(API 基地址) | `apis.amlUrl`(AML 门户来源) | `apis.test` |
|
||||||
|
|----------|--------------------------|-------------------------------|-------------|
|
||||||
|
| `environment.ts`(本地默认) | `https://api-aml.iconsz.com` | `https://aml.iconsz.com` | `true` |
|
||||||
|
| `environment.dev.ts` | `https://api-aml.iconsz.com` | `https://aml.iconsz.com` | `true` |
|
||||||
|
| `environment.prod.ts` | `https://api.justsolutions.ai` | `https://aml.justsolutions.ai` | `false` |
|
||||||
|
|
||||||
|
- `apis.amlUrl`:仅用于 `plans` 页面与父窗口(AML 门户 iframe 宿主)之间的 `postMessage` 通信,并非 HTTP 端点。
|
||||||
|
- `apis.test`:为 `true` 时支付金额固定写死为 `10`(分),用于测试支付(见第 6 节)。
|
||||||
|
|
||||||
|
## 2. 调用清单(按端点汇总)
|
||||||
|
|
||||||
|
| # | 方法 | 端点(相对 `apis.url`) | 用途 | 前端方法 | 所在文件 |
|
||||||
|
|---|------|--------------------------|------|----------|----------|
|
||||||
|
| 1 | POST | `/connect/token?__tenant=2C` | OAuth 取 access token | `getToken()` | `modules/aml-query/services/aml-query.service.ts` |
|
||||||
|
| 2 | GET | `/api/aml/ConsumerPortal/GetPrice/{inputCode}` | 查询消费者查询价格 | `getPrice()` | `modules/aml-query/services/aml-query.service.ts` |
|
||||||
|
| 3 | POST | `/api/amlPortal/customer/CreateFeedback` | 提交联系表单反馈 | `sendContactEmailAndReturnFeedback()` | `modules/contact/services/contact.service.ts` |
|
||||||
|
| 4 | POST | `/api/aml/emailQueue/CreateEmailQueue` | 发送通知邮件(管理员 + 用户回执两次调用) | `sendContactEmail()` / `sendContactByReturnEmail()` | `modules/contact/services/contact.service.ts` |
|
||||||
|
| 5 | POST | `/api/amlPortal/plan/portal/GetPlanList` | 获取套餐列表(基础/jQ/加人/KYC) | `getAllPlanList()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 6 | POST | `/api/amlPortal/Order/portal/GetEditionList` | 获取系统定义的 edition(所属行业)列表 | `getEditionList()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 7 | POST | `/api/amlPortal/Order/getCategoryByTypes` | 获取国家/地区列表(`typeCodes: ['COUNTRY']`) | `getCountryList()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 8 | GET | `/api/identity/users/SearchUserByCodeAndType/{userCode}/true` | 校验优惠码/代理商是否存在 | `getAgentor()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 9 | POST | `/api/amlPortal/Order/portal/queryRenewableTenant` | 查询可续费租户 | `queryTenant()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 10 | POST | `/api/amlPortal/Order/portal/ExistsByOrganizationBRCI` | 校验商业登记号(BR)/公司编号(CI)是否已存在 | `checkBRCI()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 11 | POST | `/api/amlPortal/customer/portal/CheckEmailExists/{email}` | 校验管理员邮箱是否已存在 | `checkEmailExist()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 12 | POST | `/api/amlPortal/Order/portal/CreateOrder` | 新租户下单 | `createOrder()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 13 | POST | `/api/amlPortal/Order/portal/TenantRenewal` | 租户续费下单 | `tenantRenewal()` | `modules/plans/services/plan.service.ts` |
|
||||||
|
| 14 | POST | `/api/amlPortal/Order/portal/PaymentWebhook` | 支付完成回调(作为 `notify_url` 传给 QFPay,由 QFPay 服务端回调,**非前端直接调用**) | — | `modules/plans/pages/tabs/plan4/plan4.component.ts` |
|
||||||
|
|
||||||
|
## 3. 鉴权(OAuth Token)
|
||||||
|
|
||||||
|
**端点 1** — `POST /connect/token?__tenant=2C`
|
||||||
|
|
||||||
|
- 位置:`AmlQueryService`。在该服务 **构造函数中自动触发**:当 `localStorage` 无 `aml_access_token` 时,调用 `getToken()` 并把返回的 `access_token` 写入 `localStorage`。
|
||||||
|
- 请求头:`Content-Type: application/x-www-form-urlencoded`
|
||||||
|
- Body(`x-www-form-urlencoded`,硬编码凭证):
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| `grant_type` | `password` |
|
||||||
|
| `response_type` | `token` |
|
||||||
|
| `username` | `customer1` |
|
||||||
|
| `password` | `1qaz@WSX` |
|
||||||
|
| `scope` | `FX` |
|
||||||
|
| `client_id` | `customer1_client` |
|
||||||
|
| `client_secret` | `1qaz@WSX` |
|
||||||
|
|
||||||
|
> ⚠️ 用户名/密码/client secret 以明文硬编码在前端源码中(`aml-query.service.ts:47-51`),属于公开网站内置的固定访客凭证。
|
||||||
|
|
||||||
|
## 4. AML 查询模块(aml-query)
|
||||||
|
|
||||||
|
**端点 2** — `GET /api/aml/ConsumerPortal/GetPrice/{inputCode}`
|
||||||
|
|
||||||
|
- 调用方:`aml-query-layout.component.ts`(`ngOnInit` 时以 `getPrice('E')` 拉取价格)。
|
||||||
|
- 价格的真正计算逻辑在 `AmlQueryService.calculatePrice()`(本地计算,不走接口)。
|
||||||
|
|
||||||
|
> 备注:`aml-query/pages/result/result.component.ts`(评估报告页)当前使用 **本地 mock 数据 / `Math.random()`** 生成风险评估结果,PDF 下载指向 `assets/sample-report.pdf`,`apiIntegration()` 弹窗里的 `api.credit-risk.example.com` 等仅为示例文案,**不是真实后端调用**。
|
||||||
|
|
||||||
|
## 5. 联系我们模块(contact)
|
||||||
|
|
||||||
|
调用方:`contact/pages/contact/contact.component.ts` 提交表单时触发。
|
||||||
|
|
||||||
|
- **端点 3** `POST /api/amlPortal/customer/CreateFeedback`:保存反馈记录。
|
||||||
|
- Body(`CreateFeedbackParam`):`companyName`、`customerName`、`customerEmail`、`typeOfQuery`、`message`、`userHost`、`destEmail`、`emailSubject`、`emailBody`。
|
||||||
|
- **端点 4** `POST /api/aml/emailQueue/CreateEmailQueue`:进入邮件发送队列,被调用 **两次**:
|
||||||
|
- `sendContactEmail()` → 通知管理员(收件人 `environment.email.contact`)。
|
||||||
|
- `sendContactByReturnEmail()` → 给提交者发送回执邮件(收件人为用户填写邮箱)。
|
||||||
|
- Body(`CreateEmailQueueParam`):`subject`、`to`、`body`(HTML)、`businessType: 2100`、`businessId: ''`。
|
||||||
|
|
||||||
|
## 6. 套餐与下单模块(plans / Order)
|
||||||
|
|
||||||
|
`PlanService` 集中了套餐查询、校验与下单接口。各端点的页面调用关系:
|
||||||
|
|
||||||
|
| 端点 | 调用页面 |
|
||||||
|
|------|----------|
|
||||||
|
| `getAllPlanList()`(端点 5) | `plans.component.ts`(进入套餐页 / 收到父窗口 `postMessage` 后) |
|
||||||
|
| `getEditionList()`(端点 6) | `plans.component.ts`、`plan3.component.ts` |
|
||||||
|
| `getCountryList()`(端点 7) | `plans.component.ts`、`plan3.component.ts` |
|
||||||
|
| `getAgentor()`(端点 8) | `plan3.component.ts`(输入优惠码校验) |
|
||||||
|
| `queryTenant()`(端点 9) | `plan1.component.ts`、`plan3.component.ts`(续费选租户) |
|
||||||
|
| `checkBRCI()`(端点 10) | `plan3.component.ts` |
|
||||||
|
| `checkEmailExist()`(端点 11) | `plan1.component.ts`、`plan3.component.ts` |
|
||||||
|
| `createOrder()`(端点 12) | `plan4.component.ts`(`type === 'new'`) |
|
||||||
|
| `tenantRenewal()`(端点 13) | `plan4.component.ts`(`type === 'renew'`) |
|
||||||
|
|
||||||
|
关键请求体:
|
||||||
|
|
||||||
|
- **`getAllPlanList`**(`GetPlanListParam`):`pageIndex: 0`、`pageSize: 9999`、`tag1List: ['B','jQ','j','AdlU','KYC']`、`tag2List`/`tag3List`(由调用方传入)、`agentUserId`。
|
||||||
|
- **`getCountryList`**:`{ typeCodes: ['COUNTRY'] }`。
|
||||||
|
- **`checkBRCI`**:通过 `params` 传 `br` / `ci`(query string)。
|
||||||
|
- **`createOrder`**(`CreateOrderParam`):`planList[]`(`planId`/`planDetailId`/`pcs`)、`tenantName`、`tenantAdminEmail`、`jurisdiction`、`organizationBR`、`organizationCI`、`organizationReference`、`contactPerson`、`companyAddress`、`phone`、`effectiveStartTime`、`agentorId`、`isOfflinePayment: false`、`isAgentBehalf`。
|
||||||
|
- **`tenantRenewal`**(`TenantRenewalParam`):`targetTenantID`、`planList[]`、`agentorId`、`tenantAdminEmail`、`isOfflinePayment: false`、`isAgentBehalf`。
|
||||||
|
|
||||||
|
### 6.1 支付跳转(QFPay 外部网关,不经过 HttpService)
|
||||||
|
|
||||||
|
位置:`plan4.component.ts` 的 `payFn()`。下单成功(`createOrder`/`tenantRenewal` 返回 `code === 0`)后:
|
||||||
|
|
||||||
|
- 若订单 `paymentStatus === 200` 或 `txamt === 0` → 直接跳转 `plans-plus/payment-success`。
|
||||||
|
- 否则通过 `location.href` 重定向到 QFPay 收银台:
|
||||||
|
`https://openapi-hk.qfapi.com/checkstand/#/?<参数>&sign=<sha256>`
|
||||||
|
|
||||||
|
主要参数:
|
||||||
|
|
||||||
|
| 参数 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| `appcode` | `environment.qfpay.appcode`(固定值) |
|
||||||
|
| `goods_name` | 下单返回的 `planName` |
|
||||||
|
| `out_trade_no` | 下单返回的 `orderCode` |
|
||||||
|
| `paysource` | `remotepay_checkout` |
|
||||||
|
| `return_url` | `{baseUrl}/plans-plus/payment-success` |
|
||||||
|
| `failed_url` | `{baseUrl}/plans-plus/payment-failure` |
|
||||||
|
| `notify_url` | `{apis.url}/api/amlPortal/Order/portal/PaymentWebhook`(端点 14,QFPay 服务端回调后端) |
|
||||||
|
| `txamt` | `apis.test ? 10 : planPrice * 100`(单位:分) |
|
||||||
|
| `txcurrcd` | `HKD` |
|
||||||
|
| `sign` | `sha256(<排序后的参数串> + api_key)` |
|
||||||
|
|
||||||
|
## 7. 端点前缀分布
|
||||||
|
|
||||||
|
| 前缀 | 含义 | 涉及端点 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `/connect/*` | OpenIddict/IdentityServer OAuth | 端点 1 |
|
||||||
|
| `/api/aml/*` | AML 主模块(消费者查询、邮件队列) | 端点 2、4 |
|
||||||
|
| `/api/amlPortal/*` | AML 门户模块(客户、套餐、订单) | 端点 3、5、6、7、9、10、11、12、13、14 |
|
||||||
|
| `/api/identity/*` | ABP Identity(用户) | 端点 8 |
|
||||||
|
|
||||||
|
## 8. 速查(按调用次序的典型流程)
|
||||||
|
|
||||||
|
- **应用启动**:`AmlQueryService` 构造 → 端点 1 取 token。
|
||||||
|
- **AML 查询页加载**:端点 2 取价格。
|
||||||
|
- **联系页提交**:端点 3 + 端点 4(×2)。
|
||||||
|
- **套餐购买流程**:
|
||||||
|
1. 进入套餐页:端点 5、6、7。
|
||||||
|
2. 填写租户/续费信息:端点 8(优惠码)、9(租户)、10(BR/CI)、11(邮箱)。
|
||||||
|
3. 确认下单:端点 12(新购)或 13(续费)。
|
||||||
|
4. 跳转 QFPay 支付 → 支付完成后 QFPay 回调端点 14。
|
||||||
|
|
@ -0,0 +1,546 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-HK">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>justsolutionsWebV2 · plans-plus 真實 API 對接分析</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f6f8fa;
|
||||||
|
--card: #ffffff;
|
||||||
|
--text: #24292f;
|
||||||
|
--muted: #57606a;
|
||||||
|
--border: #d0d7de;
|
||||||
|
--accent: #0969da;
|
||||||
|
--accent-soft: #ddf4ff;
|
||||||
|
--code-bg: #f0f3f6;
|
||||||
|
--th-bg: #f0f3f6;
|
||||||
|
--row-alt: #fafbfc;
|
||||||
|
--warn-bg: #fff8c5;
|
||||||
|
--warn-border: #d4a72c;
|
||||||
|
--note-bg: #ddf4ff;
|
||||||
|
--note-border: #54aeff;
|
||||||
|
--gap-bg: #ffebe9;
|
||||||
|
--gap-border: #ff8182;
|
||||||
|
--ok-bg: #dafbe1;
|
||||||
|
--ok-border: #4ac26b;
|
||||||
|
--get: #1a7f37;
|
||||||
|
--post: #9a6700;
|
||||||
|
--new: #8250df;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "PingFang HK", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.65;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 24px 80px; }
|
||||||
|
header.page {
|
||||||
|
background: var(--card); border: 1px solid var(--border);
|
||||||
|
border-radius: 12px; padding: 28px 32px; margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
header.page h1 { margin: 0 0 10px; font-size: 26px; }
|
||||||
|
header.page p { margin: 4px 0; color: var(--muted); }
|
||||||
|
header.page .meta { font-size: 13px; }
|
||||||
|
.toc {
|
||||||
|
background: var(--card); border: 1px solid var(--border);
|
||||||
|
border-radius: 12px; padding: 18px 28px; margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.toc h2 { font-size: 13px; margin: 0 0 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
|
||||||
|
.toc ol { margin: 0; padding-left: 20px; columns: 2; column-gap: 36px; }
|
||||||
|
.toc li { margin: 4px 0; break-inside: avoid; }
|
||||||
|
.toc a { color: var(--accent); text-decoration: none; }
|
||||||
|
.toc a:hover { text-decoration: underline; }
|
||||||
|
section { margin-bottom: 36px; }
|
||||||
|
h2.sec {
|
||||||
|
font-size: 21px; border-bottom: 2px solid var(--border);
|
||||||
|
padding-bottom: 8px; margin: 0 0 18px; scroll-margin-top: 16px;
|
||||||
|
}
|
||||||
|
h3 { font-size: 17px; margin: 24px 0 10px; scroll-margin-top: 16px; }
|
||||||
|
h4 { font-size: 15px; margin: 16px 0 6px; color: var(--muted); }
|
||||||
|
p { margin: 10px 0; }
|
||||||
|
table {
|
||||||
|
border-collapse: collapse; width: 100%; margin: 14px 0;
|
||||||
|
font-size: 14px; background: var(--card);
|
||||||
|
border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
|
||||||
|
}
|
||||||
|
th, td { border: 1px solid var(--border); padding: 8px 11px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: var(--th-bg); font-weight: 600; }
|
||||||
|
tr:nth-child(even) td { background: var(--row-alt); }
|
||||||
|
code {
|
||||||
|
background: var(--code-bg); padding: 1.5px 6px; border-radius: 5px;
|
||||||
|
font-family: "SF Mono", "JetBrains Mono", "Fira Code", Consolas, monospace; font-size: 12.5px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #0d1117; color: #e6edf3; padding: 16px 18px;
|
||||||
|
border-radius: 8px; overflow-x: auto; font-size: 12.5px; line-height: 1.55;
|
||||||
|
}
|
||||||
|
pre code { background: none; padding: 0; color: inherit; font-size: 12.5px; }
|
||||||
|
.method { font-weight: 700; font-size: 11.5px; padding: 1px 7px; border-radius: 5px; color: #fff; display: inline-block; }
|
||||||
|
.method.get { background: var(--get); }
|
||||||
|
.method.post { background: var(--post); }
|
||||||
|
.method.new { background: var(--new); }
|
||||||
|
.tag { font-size: 11px; padding: 1px 7px; border-radius: 20px; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }
|
||||||
|
.callout { border-radius: 8px; padding: 12px 16px; margin: 14px 0; border: 1px solid; }
|
||||||
|
.callout p { margin: 4px 0; }
|
||||||
|
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
|
||||||
|
.callout.note { background: var(--note-bg); border-color: var(--note-border); }
|
||||||
|
.callout.gap { background: var(--gap-bg); border-color: var(--gap-border); }
|
||||||
|
.callout.ok { background: var(--ok-bg); border-color: var(--ok-border); }
|
||||||
|
.callout .lbl { font-weight: 700; }
|
||||||
|
.pill { display:inline-block; font-size:11px; font-weight:700; padding:1px 8px; border-radius:20px; }
|
||||||
|
.pill.done { background: var(--ok-bg); color:#1a7f37; border:1px solid var(--ok-border);}
|
||||||
|
.pill.partial { background: var(--warn-bg); color:#7a5c00; border:1px solid var(--warn-border);}
|
||||||
|
.pill.todo { background: var(--gap-bg); color:#cf222e; border:1px solid var(--gap-border);}
|
||||||
|
.pill.newreq { background:#fbefff; color:#8250df; border:1px solid #d8b9ff;}
|
||||||
|
.endpoint-head {
|
||||||
|
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
|
||||||
|
background: var(--code-bg); border:1px solid var(--border); border-radius:8px;
|
||||||
|
padding:10px 14px; margin: 6px 0 12px; font-family:"SF Mono",Consolas,monospace; font-size:13.5px;
|
||||||
|
}
|
||||||
|
.small { font-size: 13px; color: var(--muted); }
|
||||||
|
ul.tight { margin: 8px 0; padding-left: 22px; }
|
||||||
|
ul.tight li { margin: 4px 0; }
|
||||||
|
.flow { font-family:"SF Mono",Consolas,monospace; font-size:13px; background:var(--code-bg); padding:10px 14px; border-radius:8px; border:1px solid var(--border); overflow-x:auto; white-space:nowrap;}
|
||||||
|
hr.soft { border:none; border-top:1px dashed var(--border); margin: 26px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>justsolutionsWebV2 · plans-plus 真實 API 對接分析</h1>
|
||||||
|
<p>把 <code>public/plans-plus.html</code> 的單頁訂閱流程,從本地 mock 切換到 AML 後端(<code>iCON.Abp.AMLPortal</code>)真實 API。</p>
|
||||||
|
<p class="meta">範圍:<code>justsolutionsWebV2/public/js/plans-plus.js</code>(前端契約) · <code>justsolutionsWebV2/server/</code>(BFF 代理層) · <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>(後端)</p>
|
||||||
|
<p class="meta">參照:旧站 <code>justsolutionsWeb</code> 的 <code>PlanService</code>(同一套後端端點) · 见 <code>docs/justsolutionsWeb/api-calls.md</code></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="toc">
|
||||||
|
<h2>目錄</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#s1">1. 結論速覽</a></li>
|
||||||
|
<li><a href="#s2">2. 總體架構與調用鏈</a></li>
|
||||||
|
<li><a href="#s3">3. plans-plus 前端依賴的 API 契約</a></li>
|
||||||
|
<li><a href="#s4">4. 後端真實端點清單(AMLPortal)</a></li>
|
||||||
|
<li><a href="#s5">5. 端點逐一映射與落地方案</a></li>
|
||||||
|
<li><a href="#s5-1">5.1 GET /editions(所屬行業)</a></li>
|
||||||
|
<li><a href="#s5-2">5.2 GET /plans/catalog(方案目錄)</a></li>
|
||||||
|
<li><a href="#s5-3">5.3 GET /countries(註冊地)</a></li>
|
||||||
|
<li><a href="#s5-4">5.4 GET /agents/:code(推薦人)</a></li>
|
||||||
|
<li><a href="#s5-5">5.5 GET /tenants/lookup(續費查租戶)</a></li>
|
||||||
|
<li><a href="#s5-6">5.6 POST /subscribe(下單)</a></li>
|
||||||
|
<li><a href="#s5-7">5.7 POST /payments/create(支付)</a></li>
|
||||||
|
<li><a href="#s6">6. plans-plus 相對旧站的新增需求</a></li>
|
||||||
|
<li><a href="#s7">7. 實施建議與分期</a></li>
|
||||||
|
<li><a href="#s8">8. 待確認問題清單</a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ───────────── 1 ───────────── -->
|
||||||
|
<section id="s1">
|
||||||
|
<h2 class="sec">1. 結論速覽</h2>
|
||||||
|
<p>plans-plus 前端共依賴 <b>7 個業務端點</b>(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 <code>justsolutionsWeb</code> 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 <code>justsolutionsWebV2/server/routes/</code> 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 <code>countries.js</code> / <code>industries.js</code> 完全同一範式)。</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前端端點</th><th>對應後端</th><th>復用程度</th><th>主要缺口</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>GET /editions</code></td><td><code>Order/portal/GetEditionList</code></td><td><span class="pill partial">改造</span></td><td>多語言 edition 名稱(後端只有 displayName)</td></tr>
|
||||||
|
<tr><td><code>GET /plans/catalog</code></td><td><code>plan/portal/GetPlanList</code></td><td><span class="pill partial">改造</span></td><td>需復刻旧站 filterPlan 拆分;<code>bestValue</code>/<code>note</code> 後端無</td></tr>
|
||||||
|
<tr><td><code>GET /countries</code></td><td><code>Order/getCategoryByTypes</code></td><td><span class="pill done">已實現</span></td><td>—(<code>routes/countries.js</code> 已可用)</td></tr>
|
||||||
|
<tr><td><code>GET /agents/:code</code></td><td><code>identity/users/SearchUserByCodeAndType</code></td><td><span class="pill partial">改造</span></td><td>響應 → <code>{code,name}</code> 轉換</td></tr>
|
||||||
|
<tr><td><code>GET /tenants/lookup</code></td><td><code>Order/portal/queryRenewableTenant</code></td><td><span class="pill todo">缺口大</span></td><td>後端按<b>租戶名</b>搜,前端按<b>郵箱</b>查;<code>currentSubscription</code> 需重建</td></tr>
|
||||||
|
<tr><td><code>POST /subscribe</code></td><td><code>CreateOrder</code> / <code>TenantRenewal</code></td><td><span class="pill partial">改造</span></td><td>需補 <code>planDetailId</code>;<code>topup</code> 類型無對應端點</td></tr>
|
||||||
|
<tr><td><code>POST /payments/create</code></td><td>QFPay 收銀台(旧站前端拼 URL)</td><td><span class="pill todo">缺口大</span></td><td>需服務端簽名 + <code>PaymentWebhook</code> 回調</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout ok">
|
||||||
|
<p><span class="lbl">好消息:</span>後端 <code>CreateOrder</code> 已忽略前端傳入的 <code>AdminPassword</code>,改用 <code>appConfig.General.TenantAdminDefaultPassword</code>(<code>OrderService.cs:215,236</code>)。因此 plans-plus 去掉「管理員密碼」步驟<b>不構成阻塞</b>——後端會發重置密碼郵件給租戶管理員。</p>
|
||||||
|
</div>
|
||||||
|
<div class="callout gap">
|
||||||
|
<p><span class="lbl">兩個真正的硬缺口:</span></p>
|
||||||
|
<p>① <b>按郵箱查租戶</b>:<code>queryRenewableTenant</code> 只支持 <code>keyword</code>(按 <code>TenantName.Contains</code>)。plans-plus 用郵箱查、且要支持「一郵箱多租戶」選擇——需新增後端端點,或在 BFF 全量拉取後按郵箱過濾(不可取)。</p>
|
||||||
|
<p>② <b>topup(加購)流程</b>:plans-plus 新增的純加購(不含基礎方案、只買 增加用戶/KYC/jQuota)旧站沒有,後端亦無直接端點。需確認映射到 <code>TenantRenewal</code>(僅加值項 planList)還是新增端點。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 2 ───────────── -->
|
||||||
|
<section id="s2">
|
||||||
|
<h2 class="sec">2. 總體架構與調用鏈</h2>
|
||||||
|
<p>justsolutionsWebV2 採「<b>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(內存假數據)
|
||||||
|
└─ 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>API_BASE_URL</code> 與 OAuth 憑證(<code>.env.*</code>)。</li>
|
||||||
|
<li><b>掛載順序</b> <code>server/index.js</code>:真實環境先掛 <code>routes/*.js</code> 業務路由,再掛 proxy 兜底;mock 環境掛 <code>mock/routes.js</code>。<b>新增 plans-plus 的真實路由就照此追加 <code>app.use(apiPrefix, createXxxRouter(config))</code>。</b></li>
|
||||||
|
<li><b>鑑權</b> <code>server/services/auth.js</code>:OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的 <code>customer1</code> 訪客憑證,現改為 <code>.env</code> 配置。BFF 每次調後端用 <code>createAuthConfig()</code> 加 <code>Authorization: Bearer</code>。</li>
|
||||||
|
</ul>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><span class="lbl">關鍵差異(與旧站):</span>旧站 Angular 直接從瀏覽器調 <code>https://api-aml.iconsz.com/api/amlPortal/*</code>,token 存 localStorage。V2 改為<b>瀏覽器不直接接觸後端</b>,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 3 ───────────── -->
|
||||||
|
<section id="s3">
|
||||||
|
<h2 class="sec">3. plans-plus 前端依賴的 API 契約</h2>
|
||||||
|
<p>以下是 <code>plans-plus.js</code> 實際讀寫的字段(即 BFF <b>必須產出/接受</b>的契約,目前由 <code>mock/routes.js</code> 滿足)。真實對接時 BFF 輸出必須與此<b>逐字段一致</b>,否則前端渲染/計價會出錯。</p>
|
||||||
|
|
||||||
|
<h3>3.1 載入期(頁面初始化並發拉取)</h3>
|
||||||
|
<p><code>loadAll()</code> 並發請求 <code>/editions</code>、<code>/plans/catalog</code>、<code>/countries</code>,三者均以 <code>{ success:true, data:… }</code> 為成功標誌。</p>
|
||||||
|
<pre><code>GET /editions → { success, data:{ editionList:[{id, displayName, nameCN, nameJP}],
|
||||||
|
jQSeparatedEditions:{ editionIds:[…] } } }
|
||||||
|
GET /plans/catalog→ { success, data:{ standard:[Plan], cpa:[Plan], addons:{
|
||||||
|
user:{unitPrice,…}, kyc:{unitPrice,…},
|
||||||
|
jquota:{ packages:[{id, jq, price, nameCN/EN/JP}] } } } }
|
||||||
|
GET /countries → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] }
|
||||||
|
|
||||||
|
Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths,
|
||||||
|
price, originalPrice, qCount(-1=無限), userCountLimit,
|
||||||
|
bestValue, noteCN, noteEN, noteJP }</code></pre>
|
||||||
|
|
||||||
|
<h3>3.2 交互期(按需)</h3>
|
||||||
|
<pre><code>GET /tenants/lookup?email=&name=
|
||||||
|
→ { success, match:'none'|'unique'|'multiple',
|
||||||
|
tenant:Tenant|null, candidates:[{tenantId, tenantName}] }
|
||||||
|
GET /agents/:code → { success, found:bool, data:{code, name}|null }
|
||||||
|
|
||||||
|
Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci,
|
||||||
|
currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price,
|
||||||
|
qCount, userCountLimit, startDate, expiryDate, usedQuota,
|
||||||
|
addons:{ users:int, kyc:bool, jquotaPackageId:string } } }</code></pre>
|
||||||
|
|
||||||
|
<h3>3.3 提交期(兩步串聯)</h3>
|
||||||
|
<p><code>submit()</code> 先 <code>POST /subscribe</code> 拿 <code>orderId</code>,再 <code>POST /payments/create</code> 拿 <code>redirectUrl</code> 跳轉支付。</p>
|
||||||
|
<pre><code>POST /subscribe body = {
|
||||||
|
type:'new'|'renew'|'topup', edition, isCpa, startDate,
|
||||||
|
plan:Plan, planPrice, isRenewalRate,
|
||||||
|
addons:{ users, userUnitPrice, kyc, kycUnitPrice,
|
||||||
|
jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice },
|
||||||
|
agentCode, subtotal, total,
|
||||||
|
// type=new 追加: company, jurisdiction, br, contact, phoneCode, phone, email, address
|
||||||
|
// type=renew/topup 追加: tenantId, company, email, currentSubscription
|
||||||
|
} → { success, data:{ orderId, status, createdAt } }
|
||||||
|
|
||||||
|
POST /payments/create body = { orderId, amount, currency:'HKD', company, email }
|
||||||
|
→ { success, data:{ paymentId, status, gateway, redirectUrl } }</code></pre>
|
||||||
|
<div class="callout warn">
|
||||||
|
<p><span class="lbl">注意:</span><code>/subscribe</code> 的 body 不含 <code>planDetailId</code>,也不含各加值項(增加用戶/KYC/jQuota)對應的 <code>planId</code>/<code>planDetailId</code>。而後端 <code>CreateOrder</code>/<code>TenantRenewal</code> 的 <code>PlanList</code> 每項都<b>必須</b>同時帶 <code>PlanId</code>+<code>PlanDetailId</code>(<code>OrderService.cs:176</code> 聯合校驗)。⇒ BFF 需在服務端依 <code>/plans/catalog</code> 結果<b>反查補齊</b> planDetailId(見 5.6)。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 4 ───────────── -->
|
||||||
|
<section id="s4">
|
||||||
|
<h2 class="sec">4. 後端真實端點清單(AMLPortal)</h2>
|
||||||
|
<p>以下端點均在 <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>,並已被旧站 <code>justsolutionsWeb/PlanService</code> 使用驗證過。前綴 <code>/api/amlPortal/*</code>,鑑權走 <code>[AbpAutoAuth("Portal")]</code>(門戶訪客 token)。</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>方法 / 路由</th><th>用途</th><th>Controller</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1</td><td><span class="method post">POST</span> <code>/api/amlPortal/plan/portal/GetPlanList</code></td><td>取方案列表(B/jQ/j/AdlU/KYC)</td><td>PlanController:37</td></tr>
|
||||||
|
<tr><td>2</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/GetEditionList</code></td><td>取 edition(所屬行業)+ jQSeparatedEditions</td><td>OrderController:136</td></tr>
|
||||||
|
<tr><td>3</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/getCategoryByTypes</code></td><td>取國家列表(<code>typeCodes:['COUNTRY']</code>)</td><td>OrderController:184</td></tr>
|
||||||
|
<tr><td>4</td><td><span class="method get">GET</span> <code>/api/identity/users/SearchUserByCodeAndType/{code}/true</code></td><td>校驗推薦人/代理 code</td><td>Identity(非 AMLPortal)</td></tr>
|
||||||
|
<tr><td>5</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/queryRenewableTenant</code></td><td>查可續費租戶(按 <code>keyword</code>=租戶名)</td><td>OrderController:308</td></tr>
|
||||||
|
<tr><td>6</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/ExistsByOrganizationBRCI</code></td><td>校驗 BR/CI 是否已存在</td><td>OrderController:150</td></tr>
|
||||||
|
<tr><td>7</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/portal/CheckEmailExists/{email}</code></td><td>校驗管理員郵箱是否已用</td><td>CustomerController:121</td></tr>
|
||||||
|
<tr><td>8</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/CreateOrder</code></td><td>新租戶下單</td><td>OrderController:53</td></tr>
|
||||||
|
<tr><td>9</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/TenantRenewal</code></td><td>租戶續費下單</td><td>OrderController:321</td></tr>
|
||||||
|
<tr><td>10</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/PaymentWebhook</code></td><td>支付回調(QFPay 服務端調,非前端)</td><td>OrderController:163</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="small">DTO 位置:<code>Application.Contracts/PlanAppLayer/*</code>(GetPlanListParam、PlanDto、PlanDetailDto)、<code>Application.Contracts/OrderAppLayer/*</code>(CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto)。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 5 ───────────── -->
|
||||||
|
<section id="s5">
|
||||||
|
<h2 class="sec">5. 端點逐一映射與落地方案</h2>
|
||||||
|
<p class="small">每節:前端契約 → 對應後端 → 字段映射 → BFF 轉換要點 → 差異/缺口。新增的 BFF 文件建議放 <code>server/routes/</code>,與 <code>countries.js</code> 同範式(<code>createAuthConfig()</code> + axios + 字段映射 + <code>{success,data}</code> 包裝)。</p>
|
||||||
|
|
||||||
|
<!-- 5.1 -->
|
||||||
|
<h3 id="s5-1">5.1 GET /editions(所屬行業) <span class="pill partial">改造</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method get">GET</span> /api/editions → <span class="method post">POST</span> /api/amlPortal/Order/portal/GetEditionList</div>
|
||||||
|
<p>後端返回 <code>{ code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }</code>。前端要 <code>editionList:[{id, displayName, nameCN, nameJP}]</code> 與 <code>jQSeparatedEditions.editionIds</code>。</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前端字段</th><th>後端來源</th><th>說明</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>id</code></td><td><code>editionList[].id</code>(Guid)</td><td>直接映射;後續作 <code>OrganizationReference</code></td></tr>
|
||||||
|
<tr><td><code>displayName</code></td><td><code>editionList[].displayName</code></td><td>英文名</td></tr>
|
||||||
|
<tr><td><code>nameCN</code> / <code>nameJP</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr>
|
||||||
|
<tr><td><code>jQSeparatedEditions.editionIds</code></td><td>同名字段</td><td>驅動 CPA/標準方案集切換(<code>state.isCpa</code>)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout gap">
|
||||||
|
<p><span class="lbl">缺口:</span>edition 多語言名稱。後端 <code>GetEditionList</code> 只有 <code>displayName</code>,旧站也僅用 displayName。plans-plus 的 <code>editionName()</code> 在 tc/jp 下會優先取 <code>nameCN</code>/<code>nameJP</code>,缺失時已能 fallback 回 displayName ⇒ <b>不阻塞,但中日文會顯示英文</b>。</p>
|
||||||
|
<p><span class="lbl">建議:</span>BFF 維護一份 <code>editionId/displayName → {nameCN,nameJP}</code> 映射表(與 <code>industries.js</code> 現有靜態行業翻譯同思路),或後端在 edition 上補多語言字段。</p>
|
||||||
|
</div>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><span class="lbl">旧站行為(可選沿用):</span><code>getEditionList()</code> 會<b>過濾掉 <code>Standard</code></b>、並把 <code>Others</code> 排到列表末尾,同時把 editionIds <code>toLowerCase()</code>。plans-plus 目前未做此整理;若要與旧站一致,這段邏輯應放 BFF。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.2 -->
|
||||||
|
<h3 id="s5-2">5.2 GET /plans/catalog(方案目錄) <span class="pill partial">改造</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method get">GET</span> /api/plans/catalog → <span class="method post">POST</span> /api/amlPortal/plan/portal/GetPlanList</div>
|
||||||
|
<p>BFF 以旧站同款請求體調 <code>GetPlanList</code>,再復刻旧站 <code>filterPlan()</code> 把扁平 <code>items</code> 按 <code>tag1Code</code> 拆成 5 組,組裝成 <code>{standard, cpa, addons}</code>。</p>
|
||||||
|
<h4>請求體(沿用 PlanService.getAllPlanList)</h4>
|
||||||
|
<pre><code>{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false,
|
||||||
|
tag1List:['B','jQ','j','AdlU','KYC'], tag2List, tag3List, agentUserId }</code></pre>
|
||||||
|
<h4>後端 items → 前端 Plan 字段映射</h4>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前端 Plan</th><th>後端來源(item / planDetails[0])</th><th>備註</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>planId</code></td><td><code>item.id</code></td><td></td></tr>
|
||||||
|
<tr><td><code>planDetailId</code> ⚠️</td><td><code>item.planDetails[0].id</code></td><td><b>前端契約現缺此字段</b>,但下單必需 ⇒ BFF 須補進 Plan,提交時回填(見 5.6)</td></tr>
|
||||||
|
<tr><td><code>tag2Code</code></td><td><code>item.tag2Code</code></td><td>Std/P2G/CPA/Pre…,CPA 判定用</td></tr>
|
||||||
|
<tr><td><code>nameCN</code>/<code>nameEN</code></td><td><code>item.nameCN</code>/<code>nameEN</code></td><td>旧站會截掉「(…」後綴,可沿用</td></tr>
|
||||||
|
<tr><td><code>nameJP</code></td><td><b>後端無</b></td><td>缺口,fallback EN</td></tr>
|
||||||
|
<tr><td><code>periodMonths</code></td><td><code>planDetails[0].periodMonths</code></td><td></td></tr>
|
||||||
|
<tr><td><code>price</code>/<code>originalPrice</code></td><td><code>planDetails[0].price</code>/<code>originalPrice</code></td><td>劃線價用 originalPrice</td></tr>
|
||||||
|
<tr><td><code>qCount</code></td><td><code>planDetails[0].qCount</code></td><td><code>-1</code> 表無限</td></tr>
|
||||||
|
<tr><td><code>userCountLimit</code></td><td><code>planDetails[0].userCountLimit</code></td><td></td></tr>
|
||||||
|
<tr><td><code>bestValue</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr>
|
||||||
|
<tr><td><code>noteCN/EN/JP</code></td><td><code>item.description</code>?(無多語言)</td><td>缺口,見下</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h4>addons 拆分(按 tag1Code)</h4>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><code>standard</code> ← <code>tag1Code==='B' && tag2Code!=='CPA'</code>;<code>cpa</code> ← <code>tag1Code==='B' && tag2Code==='CPA'</code></li>
|
||||||
|
<li><code>addons.user.unitPrice</code> ← <code>tag1Code==='AdlU'</code> 的 <code>planDetails[0].price</code>(並記其 planId/planDetailId 備下單)</li>
|
||||||
|
<li><code>addons.kyc.unitPrice</code> ← <code>tag1Code==='KYC'</code> 同上</li>
|
||||||
|
<li><code>addons.jquota.packages[]</code> ← <code>tag1Code==='jQ'||'j'</code>:每項 <code>{ id:planId, jq:qCount, price:planDetails[0].price, name* }</code>,並各自記 planDetailId</li>
|
||||||
|
</ul>
|
||||||
|
<div class="callout gap">
|
||||||
|
<p><span class="lbl">缺口:</span><code>bestValue</code>(最超值標記)、<code>note*</code>(賣點文案)後端 <code>PlanDto</code>/<code>PlanDetailDto</code> 無對應字段。建議:① BFF 配置(按 systemCode 標 bestValue / 文案);或 ② 後端在 Plan 增 <code>bestValue</code> 與多語言 note。短期可在 BFF 寫死映射,不阻塞。</p>
|
||||||
|
</div>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><span class="lbl">提示:</span>plans-plus 的 jQuota 改為「<b>選配套</b>」(<code>jquotaPackageId</code>),不再是旧站的「按數量」。後端 jQ 計劃本就是離散 plan,天然契合——每個 jQ plan 即一個 package。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.3 -->
|
||||||
|
<h3 id="s5-3">5.3 GET /countries(註冊地) <span class="pill done">已實現</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method get">GET</span> /api/countries → <span class="method post">POST</span> /api/amlPortal/Order/getCategoryByTypes</div>
|
||||||
|
<p><code>server/routes/countries.js</code> <b>已實現並可直接用</b>:取 <code>typeCodes:['COUNTRY']</code>,從 <code>categoryTranslations</code> 提 zh-hk/zh-cn/ja/en-us,輸出 <code>{code, name, nameTC, nameSC, nameJP, phoneCode}</code>,正好滿足 plans-plus 的 <code>countryDisplay()</code> 與 <code>phoneCode</code> 自動填充。<b>無需改動。</b></p>
|
||||||
|
|
||||||
|
<!-- 5.4 -->
|
||||||
|
<h3 id="s5-4">5.4 GET /agents/:code(推薦人代理) <span class="pill partial">改造</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method get">GET</span> /api/agents/:code → <span class="method get">GET</span> /api/identity/users/SearchUserByCodeAndType/{code}/true</div>
|
||||||
|
<p>前端要 <code>{ success, found, data:{code, name} }</code>。後端(Identity)返回匹配到的用戶(含 name/userName/extraProperties.code 等)。BFF 取首條 → <code>found:true, data:{code, name}</code>;無匹配 → <code>found:false, data:null</code>。</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前端</th><th>後端用戶字段</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>data.code</code></td><td>用戶 code(agent code)</td></tr>
|
||||||
|
<tr><td><code>data.name</code></td><td><code>name</code> / <code>surname</code> / <code>userName</code></td></tr>
|
||||||
|
<tr><td>(下單用)<code>agentorId</code></td><td>用戶 <code>id</code>(Guid)→ 提交時作 <code>CreateOrderParam.AgentorId</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout warn">
|
||||||
|
<p><span class="lbl">關鍵:</span>前端只展示 <code>name</code>,但下單需要 agent 的 <b>Guid id</b>(<code>CreateOrderParam.AgentorId</code>)。BFF 應在 <code>/agents/:code</code> 響應內<b>順帶緩存 code→id</b>(或前端 <code>data</code> 內含 id),否則 <code>/subscribe</code> 階段需再查一次。建議 <code>data</code> 增隱藏字段 <code>agentorId</code>。</p>
|
||||||
|
<p>備註:AMLPortal 另有 <code>Agentor/portal/GetAgentorByCode</code>,但已標【廢棄】;旧站用的是 Identity 的 <code>SearchUserByCodeAndType</code>,沿用之。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.5 -->
|
||||||
|
<h3 id="s5-5">5.5 GET /tenants/lookup(續費查租戶) <span class="pill todo">缺口大</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method get">GET</span> /api/tenants/lookup?email=&name= → <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenant</div>
|
||||||
|
<p>前端按<b>郵箱</b>查租戶,支持「none / unique / multiple」三態,命中後帶出 <code>currentSubscription</code> 預填續費表單。但後端 <code>queryRenewableTenant</code> 只接受 <code>{keyword}</code> 且按 <b><code>TenantName.Contains(keyword)</code></b> 搜索(<code>OrderService.cs:2363</code>),返回 <code>List<TenantPropertyDto></code>(內部已把 admin 郵箱填入 <code>TenantAdminEmail</code>)。</p>
|
||||||
|
<h4>兩個不匹配點</h4>
|
||||||
|
<ol class="tight" style="padding-left:20px">
|
||||||
|
<li><b>查詢維度不符</b>:前端給郵箱,後端按名字搜。<br>
|
||||||
|
短期 BFF 變通:以空/寬 keyword 拉取後在服務端按 <code>TenantAdminEmail===email</code> 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。<br>
|
||||||
|
正解:<b>後端新增「按管理員郵箱查可續費租戶」端點</b>(返回同 <code>TenantPropertyDto</code> 結構)。</li>
|
||||||
|
<li><b>currentSubscription 需重建</b>:前端的 <code>currentSubscription</code>(planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons)並非 <code>TenantPropertyDto</code> 直接字段,需由 TenantProperty + 其 <code>Order</code> + 正在生效的 plan 推導。可參考 <code>GetCurrServiceInfo</code>(<code>CurrServiceInfoDto.ActiveBPlans</code>、EffectiveEndTime、QCountLeft 等)。</li>
|
||||||
|
</ol>
|
||||||
|
<h4>TenantPropertyDto → 前端 Tenant 映射(可得部分)</h4>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>前端</th><th>TenantPropertyDto</th><th>備註</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>tenantId</code></td><td><code>TargetTenantID</code></td><td></td></tr>
|
||||||
|
<tr><td><code>tenantName</code></td><td><code>TenantName</code></td><td></td></tr>
|
||||||
|
<tr><td><code>editionId</code>/<code>editionName</code></td><td><code>EditionId</code>/<code>EditionName</code></td><td></td></tr>
|
||||||
|
<tr><td><code>currentSubscription.expiryDate</code></td><td><code>EffectiveEndTime</code></td><td>過期判定(前端 daysUntil)</td></tr>
|
||||||
|
<tr><td><code>currentSubscription.startDate</code></td><td><code>EffectiveStartTime</code></td><td>未過期續費 → 默認接續此日</td></tr>
|
||||||
|
<tr><td><code>currentSubscription.qCount</code>/<code>usedQuota</code></td><td><code>QCountPurchase</code> / <code>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></td><td>需經 <code>Order</code>/<code>OrderDetail</code> 反查</td><td><b>推導</b>,續費續價(沿用上期 price)依賴此</td></tr>
|
||||||
|
<tr><td><code>currentSubscription.addons.users</code>/<code>jquotaPackageId</code></td><td>需經訂單明細反查</td><td><b>推導</b></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout gap">
|
||||||
|
<p><span class="lbl">建議後端改動:</span>新增端點 <code>queryRenewableTenantByEmail(email[, tenantName])</code>,直接返回 plans-plus 所需的「租戶 + currentSubscription(含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.6 -->
|
||||||
|
<h3 id="s5-6">5.6 POST /subscribe(下單) <span class="pill partial">改造</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method post">POST</span> /api/subscribe → 按 type 分流</div>
|
||||||
|
<p>BFF 依 <code>body.type</code> 分流到不同後端端點,並把前端的「人類友好」payload 翻成後端 DTO;核心是<b>組裝 <code>PlanList</code></b>(基礎方案 + 各加值項,每項補 <code>planDetailId</code>)。</p>
|
||||||
|
|
||||||
|
<h4>① type='new' → CreateOrder</h4>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>CreateOrderParam</th><th>前端 payload</th><th>備註</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>PlanList[]</code></td><td>plan + addons</td><td>見下「PlanList 組裝」</td></tr>
|
||||||
|
<tr><td><code>TenantName</code></td><td><code>company</code></td><td>必填;後端校驗重名</td></tr>
|
||||||
|
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td><td>必填;後端再校驗未註冊</td></tr>
|
||||||
|
<tr><td><code>Jurisdiction</code></td><td><code>jurisdiction</code></td><td>必填</td></tr>
|
||||||
|
<tr><td><code>OrganizationReference</code></td><td><code>edition</code>(Guid)</td><td>必填;校驗 edition 存在</td></tr>
|
||||||
|
<tr><td><code>OrganizationBR</code> / <code>OrganizationCI</code></td><td><code>br</code> / —</td><td><b>BR 或 CI 至少一個</b>(OrderService.cs:154);plans-plus 僅收 BR 且標可選 ⇒ 缺口</td></tr>
|
||||||
|
<tr><td><code>EffectiveStartTime</code></td><td><code>startDate</code></td><td></td></tr>
|
||||||
|
<tr><td><code>ContactPerson</code></td><td><code>contact</code></td><td></td></tr>
|
||||||
|
<tr><td><code>CompanyAddress</code></td><td><code>address</code></td><td>可選</td></tr>
|
||||||
|
<tr><td><code>Phone</code></td><td><code>phoneCode + phone</code></td><td>可選;BFF 拼接</td></tr>
|
||||||
|
<tr><td><code>AgentorId</code></td><td>由 <code>agentCode</code> 解析的 Guid</td><td>見 5.4</td></tr>
|
||||||
|
<tr><td><code>AdminPassword</code></td><td>—</td><td><b>忽略</b>(後端用默認密碼,發重置郵件)</td></tr>
|
||||||
|
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>—</td><td>固定 <code>false</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>② type='renew' → TenantRenewal</h4>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>TenantRenewalParam</th><th>前端 payload</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>TargetTenantID</code></td><td><code>tenantId</code></td></tr>
|
||||||
|
<tr><td><code>PlanList[]</code></td><td>plan + addons(同下組裝)</td></tr>
|
||||||
|
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td></tr>
|
||||||
|
<tr><td><code>AgentorId</code></td><td>空 → 延續原代理</td></tr>
|
||||||
|
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>固定 <code>false</code></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>③ type='topup' → <span class="pill newreq">新增需求 · 無對應端點</span></h4>
|
||||||
|
<p>純加購(無基礎方案,只買 增加用戶/KYC/jQuota)。後端無直接端點。候選方案:</p>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><b>A(推薦)</b>:復用 <code>TenantRenewal</code>,<code>PlanList</code> 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。</li>
|
||||||
|
<li><b>B</b>:後端新增 <code>TenantTopup</code> 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC,不延長有效期」。<code>UpdateTenantProperty</code>(admin)邏輯可借鑒(它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>PlanList 組裝(三種 type 共用,核心難點)</h4>
|
||||||
|
<pre><code>PlanList = []
|
||||||
|
// 基礎方案(topup 不加)
|
||||||
|
if (type!=='topup') PlanList.push({ PlanId: plan.planId,
|
||||||
|
PlanDetailId: plan.planDetailId, PCS: 1 })
|
||||||
|
// 增加用戶:PCS = addons.users
|
||||||
|
if (addons.users>0) PlanList.push({ PlanId: AdlU.planId,
|
||||||
|
PlanDetailId: AdlU.planDetailId, PCS: addons.users })
|
||||||
|
// KYC:PCS = 1
|
||||||
|
if (addons.kyc) PlanList.push({ PlanId: KYC.planId,
|
||||||
|
PlanDetailId: KYC.planDetailId, PCS: 1 })
|
||||||
|
// jQuota:選中的配套 ×1
|
||||||
|
if (addons.jquotaPackageId)
|
||||||
|
PlanList.push({ PlanId: jqPack.planId,
|
||||||
|
PlanDetailId: jqPack.planDetailId, PCS: 1 })</code></pre>
|
||||||
|
<div class="callout warn">
|
||||||
|
<p><span class="lbl">planDetailId 從哪來:</span>前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 <code>/plans/catalog</code> 的服務端版本(含 planDetailId),按 <code>plan.planId</code> 與加值項類型<b>反查補齊</b>。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),<code>/subscribe</code> 時直接查表。</p>
|
||||||
|
</div>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><span class="lbl">後端返回(OperationDto):</span>下單成功返回含 <code>orderCode</code>、<code>planName</code>、<code>paymentStatus</code>、<code>txamt</code> 等(旧站 <code>plan4.component</code> 用其拼 QFPay URL)。BFF 應把其中 <code>orderId/orderCode</code> 回給前端的 <code>data.orderId</code>,並<b>暫存 orderCode/amount</b> 供下一步支付用。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.7 -->
|
||||||
|
<h3 id="s5-7">5.7 POST /payments/create(支付) <span class="pill todo">缺口大</span></h3>
|
||||||
|
<div class="endpoint-head"><span class="method post">POST</span> /api/payments/create → QFPay 收銀台(服務端拼簽名 URL)</div>
|
||||||
|
<p>旧站<b>沒有</b>獨立的 create-payment 後端端點:<code>plan4.component.payFn()</code> 在前端直接用 <code>orderCode</code>/<code>planName</code>/<code>txamt</code> 拼 QFPay checkstand URL 並 <code>sha256</code> 簽名後 <code>location.href</code> 跳轉。plans-plus 改成調 <code>/payments/create</code> 拿 <code>redirectUrl</code> ⇒ <b>這段簽名邏輯應移到 BFF 服務端</b>(API key 不可暴露前端)。</p>
|
||||||
|
<h4>BFF 實現要點(對標旧站 QFPay 參數)</h4>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>QFPay 參數</th><th>來源</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>appcode</code></td><td>環境配置(<code>.env</code>)</td></tr>
|
||||||
|
<tr><td><code>goods_name</code></td><td>下單返回 <code>planName</code> / 前端 <code>planName</code></td></tr>
|
||||||
|
<tr><td><code>out_trade_no</code></td><td>下單返回 <code>orderCode</code></td></tr>
|
||||||
|
<tr><td><code>txamt</code></td><td><code>apis.test ? 10 : amount*100</code>(單位:分)</td></tr>
|
||||||
|
<tr><td><code>txcurrcd</code></td><td><code>HKD</code></td></tr>
|
||||||
|
<tr><td><code>return_url</code>/<code>failed_url</code></td><td>V2 的 <code>pay-return</code> / 失敗頁</td></tr>
|
||||||
|
<tr><td><code>notify_url</code></td><td><code>{API_BASE_URL}/api/amlPortal/Order/portal/PaymentWebhook</code>(端點 10,QFPay 服務端回調後端)</td></tr>
|
||||||
|
<tr><td><code>sign</code></td><td><code>sha256(排序參數串 + api_key)</code>,<b>服務端計算</b></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>BFF 返回 <code>{ success, data:{ redirectUrl:<QFPay checkstand URL> } }</code>,前端 <code>location.href</code> 跳轉。支付結果由 QFPay 異步回調後端 <code>PaymentWebhook</code> 落單(權威來源);前端返回頁可輪詢訂單狀態。</p>
|
||||||
|
<div class="callout note">
|
||||||
|
<p><span class="lbl">mock 對照:</span>當前 mock 用 <code>/payments/create</code> + <code>/payments/:pid</code>(輪詢)+ <code>/payments/:pid/webhook</code> 模擬「下單→收銀台→webhook→返回頁輪詢」全鏈路。真實環境收銀台與 webhook 由 QFPay 提供,BFF 只需:① 拼簽名 URL;② 提供查單(透傳後端訂單狀態)給返回頁輪詢。</p>
|
||||||
|
</div>
|
||||||
|
<div class="callout warn">
|
||||||
|
<p><span class="lbl">免支付分支:</span>旧站當訂單 <code>paymentStatus===200</code> 或 <code>txamt===0</code>(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(<code>redirectUrl</code> 直接給成功頁)。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 6 ───────────── -->
|
||||||
|
<section id="s6">
|
||||||
|
<h2 class="sec">6. plans-plus 相對旧站的新增需求</h2>
|
||||||
|
<p>以下是 plans-plus 單頁流程相對旧站 4 步嚮導<b>新增或改變</b>的點,逐一標注對接影響。</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>新增/變更</th><th>對接影響</th><th>狀態</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1</td><td><b>topup 加購類型</b>(無基礎方案,只買加值項)</td><td>後端無端點,需確認映射 TenantRenewal 或新增端點</td><td><span class="pill todo">需後端</span></td></tr>
|
||||||
|
<tr><td>2</td><td><b>按郵箱查租戶</b> + 一郵箱多租戶選擇</td><td>後端 queryRenewableTenant 按名字搜,需新增按郵箱端點</td><td><span class="pill todo">需後端</span></td></tr>
|
||||||
|
<tr><td>3</td><td><b>jQuota 改為選配套</b>(package,非按量)</td><td>BFF 把 jQ plan 映成 packages 即可</td><td><span class="pill partial">BFF</span></td></tr>
|
||||||
|
<tr><td>4</td><td><b>KYC「已購買」鎖定</b>(topup 已有則禁買)</td><td>依賴租戶 <code>EnableKYC</code>,BFF 帶出即可</td><td><span class="pill partial">BFF</span></td></tr>
|
||||||
|
<tr><td>5</td><td><b>續費續價</b>(沿用上期 price,顯示續費折扣)</td><td>依賴 currentSubscription.price,需訂單反查</td><td><span class="pill todo">需後端</span></td></tr>
|
||||||
|
<tr><td>6</td><td><b>去掉管理員密碼步驟</b></td><td>後端已忽略 AdminPassword,發重置郵件</td><td><span class="pill done">無影響</span></td></tr>
|
||||||
|
<tr><td>7</td><td><b>bestValue / 賣點 note</b>(方案卡片標記與文案)</td><td>後端 Plan 無此字段,BFF 配置或後端補</td><td><span class="pill partial">BFF/後端</span></td></tr>
|
||||||
|
<tr><td>8</td><td><b>edition 多語言名稱</b>(中/日)</td><td>後端只有 displayName,BFF 補映射</td><td><span class="pill partial">BFF</span></td></tr>
|
||||||
|
<tr><td>9</td><td><b>BR 標為可選</b>(UI)</td><td>後端要求 BR 或 CI 至少一個;需 UI 補 CI 或設必填</td><td><span class="pill todo">需對齊</span></td></tr>
|
||||||
|
<tr><td>10</td><td><b>支付 create 端點化</b>(前端拿 redirectUrl)</td><td>QFPay 簽名邏輯移到 BFF 服務端</td><td><span class="pill todo">BFF</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 7 ───────────── -->
|
||||||
|
<section id="s7">
|
||||||
|
<h2 class="sec">7. 實施建議與分期</h2>
|
||||||
|
<h3>7.1 新增 / 改動文件清單(justsolutionsWebV2/server)</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>文件</th><th>職責</th><th>後端依賴</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>routes/editions.js</code>(新)</td><td><code>GET /editions</code> + 多語言/過濾整理</td><td>GetEditionList</td></tr>
|
||||||
|
<tr><td><code>routes/plans.js</code>(新)</td><td><code>GET /plans/catalog</code> + filterPlan 拆分 + 緩存 planDetailId</td><td>GetPlanList</td></tr>
|
||||||
|
<tr><td><code>routes/countries.js</code></td><td>已存在,直接掛載</td><td>getCategoryByTypes</td></tr>
|
||||||
|
<tr><td><code>routes/agents.js</code>(新)</td><td><code>GET /agents/:code</code> → {code,name,agentorId}</td><td>SearchUserByCodeAndType</td></tr>
|
||||||
|
<tr><td><code>routes/tenants.js</code>(新)</td><td><code>GET /tenants/lookup</code>(依賴後端新端點)</td><td>queryRenewableTenant(ByEmail)</td></tr>
|
||||||
|
<tr><td><code>routes/subscribe.js</code>(新)</td><td><code>POST /subscribe</code> 分流 + PlanList 組裝</td><td>CreateOrder / TenantRenewal / (Topup)</td></tr>
|
||||||
|
<tr><td><code>routes/payments.js</code>(新)</td><td><code>POST /payments/create</code> 拼 QFPay 簽名 URL + 查單</td><td>PaymentWebhook(回調)</td></tr>
|
||||||
|
<tr><td><code>index.js</code></td><td>在真實環境段 <code>app.use(apiPrefix, createXxxRouter(config))</code> 追加各路由</td><td>—</td></tr>
|
||||||
|
<tr><td><code>.env.*</code> / <code>config.js</code></td><td>補 QFPay <code>appcode/api_key</code>、edition/plan 文案映射等</td><td>—</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>7.2 可能的後端改動(建議與後端團隊確認)</h3>
|
||||||
|
<ul class="tight">
|
||||||
|
<li><b>按郵箱查可續費租戶</b>端點,返回聚合 currentSubscription(含 planId / 上期 price / 已購加值項)。</li>
|
||||||
|
<li><b>topup 語義</b>:明確「無基礎方案的加購」如何下單(TenantRenewal 是否允許 / 新增 Topup 端點)。</li>
|
||||||
|
<li>(可選)Plan 增 <code>bestValue</code> 與多語言 <code>note</code>;edition 增多語言名稱。</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>7.3 建議分期</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>階段</th><th>內容</th><th>可獨立交付</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>P1 · 只讀目錄</td><td>editions + plans/catalog + countries(已就緒)+ agents</td><td>頁面可正常渲染方案/行業/國家,校驗推薦人</td></tr>
|
||||||
|
<tr><td>P2 · 新購下單</td><td>subscribe(new) + payments/create(QFPay)</td><td>新租戶完整下單支付閉環</td></tr>
|
||||||
|
<tr><td>P3 · 續費</td><td>tenants/lookup(後端新端點)+ subscribe(renew)</td><td>續費閉環,含續價</td></tr>
|
||||||
|
<tr><td>P4 · 加購</td><td>topup(後端確認後)</td><td>加購閉環</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ───────────── 8 ───────────── -->
|
||||||
|
<section id="s8">
|
||||||
|
<h2 class="sec">8. 待確認問題清單</h2>
|
||||||
|
<ol class="tight" style="padding-left:20px">
|
||||||
|
<li><b>租戶查詢:</b>後端能否新增「按管理員郵箱查可續費租戶」?返回是否能直接帶 currentSubscription(planId / 上期價 / 已購加值項)?</li>
|
||||||
|
<li><b>topup:</b>純加購走 TenantRenewal(PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?還是需新端點?</li>
|
||||||
|
<li><b>BR/CI:</b>plans-plus 把 BR 標為可選,但後端要求 BR 或 CI 至少一個——UI 是否補 CI 字段,或把 BR 設為必填?</li>
|
||||||
|
<li><b>bestValue / note / edition 多語言:</b>由 BFF 配置維護,還是後端在數據上補字段?</li>
|
||||||
|
<li><b>QFPay:</b>V2 是否沿用旧站同一 <code>appcode</code>/<code>api_key</code>/收銀台域名?簽名算法是否一致(sha256 排序串 + key)?</li>
|
||||||
|
<li><b>0 元 / P2G:</b>免支付分支的判定(<code>paymentStatus===200</code> 或 <code>txamt===0</code>)是否照搬?</li>
|
||||||
|
<li><b>agentorId:</b><code>/agents/:code</code> 響應是否可附帶 agent 的 Guid id,避免下單時二次查詢?</li>
|
||||||
|
<li><b>鑑權憑證:</b>V2 BFF 用的門戶訪客 OAuth 帳號(<code>.env</code>)權限是否覆蓋 CreateOrder / queryRenewableTenant(均 <code>[AbpAutoAuth("Portal")]</code>,應可)?</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="small" style="text-align:center;margin-top:48px;color:var(--muted)">
|
||||||
|
本分析基於源碼靜態閱讀:<code>plans-plus.js</code> / <code>server/*</code> / <code>AMLPortal</code> Controllers 與 DTO / 旧站 <code>PlanService</code>。涉及後端行為(續費續價、topup 語義)以實際接口為準。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue