AML/docs/justsolutionsWebV2/pay-as-you-go接口需求单.html

534 lines
47 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>隨付即用 · Agent 展示 · 接口需求单(提给 AML_Backend)</title>
<style>
:root {
--bg: #f6f8fa;
--card: #ffffff;
--text: #24292f;
--muted: #57606a;
--border: #d0d7de;
--accent: #0969da;
--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;
--p0: #cf222e; --p0-bg: #ffebe9; --p0-bd: #ff8182;
--p1: #9a6700; --p1-bg: #fff8c5; --p1-bd: #d4a72c;
--p2: #57606a; --p2-bg: #eef1f4; --p2-bd: #d0d7de;
--fe: #8250df; --fe-bg: #f3eefe;
--bff: #0969da; --bff-bg: #ddf4ff;
--be: #1a7f37; --be-bg: #dafbe1;
}
* { 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: 1180px; margin: 0 auto; padding: 32px 24px 90px; }
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: 6px 0; color: var(--muted); }
header.page .meta { font-size: 13px; }
header.page code { font-size: 12.5px; }
.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: 40px; }
h2.sec { font-size: 21px; border-bottom: 2px solid var(--border); padding-bottom: 8px; margin: 0 0 18px; scroll-margin-top: 16px; }
h4 { font-size: 13px; margin: 16px 0 6px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; }
p { margin: 10px 0; }
table { border-collapse: collapse; width: 100%; margin: 14px 0; font-size: 13.5px; 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: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px 15px; overflow-x: auto; font-size: 12.5px; line-height: 1.55; font-family: "SF Mono", "JetBrains Mono", Consolas, monospace; margin: 12px 0; }
pre code { background: none; padding: 0; font-size: inherit; }
.ref { color: var(--muted); font-size: 12px; font-family: "SF Mono", Consolas, monospace; }
.callout { border-radius: 8px; padding: 12px 16px; margin: 14px 0; border: 1px solid; }
.callout.note { background: var(--note-bg); border-color: var(--note-border); }
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
.callout.gap { background: var(--gap-bg); border-color: var(--gap-border); }
.callout.ok { background: var(--ok-bg); border-color: var(--ok-border); }
.callout b { display: block; margin-bottom: 4px; }
ul.tight { margin: 8px 0; padding-left: 22px; }
ul.tight li { margin: 4px 0; }
.pill { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 9px; border-radius: 20px; border: 1.5px solid; letter-spacing: .3px; }
.pill.p0 { color: var(--p0); background: var(--p0-bg); border-color: var(--p0-bd); }
.pill.p1 { color: var(--p1); background: var(--p1-bg); border-color: var(--p1-bd); }
.pill.p2 { color: var(--p2); background: var(--p2-bg); border-color: var(--p2-bd); }
.pill.plain { color: var(--muted); background: var(--code-bg); border-color: var(--border); font-weight: 600; }
/* 需求卡片 */
.req { background: var(--card); border: 1px solid var(--border); border-radius: 11px; padding: 4px 24px 20px; margin: 20px 0; }
.req h3 { font-size: 17px; margin: 20px 0 4px; scroll-margin-top: 16px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.req h3 .no { font-family: "SF Mono", Consolas, monospace; color: var(--accent); }
.req .lede { color: var(--muted); font-size: 13.5px; margin: 2px 0 12px; }
table.kv { font-size: 13.5px; }
table.kv th { width: 92px; white-space: nowrap; background: var(--th-bg); font-weight: 600; color: var(--muted); font-size: 12.5px; }
table.kv tr:nth-child(even) td { background: var(--card); }
table.sum td.id { font-family: "SF Mono", Consolas, monospace; font-weight: 700; white-space: nowrap; }
table.sum td.pr { white-space: nowrap; }
.layer { display: inline-block; font-size: 10.5px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase; padding: 1px 7px; border-radius: 20px; }
.l-fe { background: var(--fe); color: #fff; }
.l-bff { background: var(--bff); color: #fff; }
.l-be { background: var(--be); color: #fff; }
hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
.small { font-size: 12.5px; color: var(--muted); }
footer.page { color: var(--muted); font-size: 12.5px; border-top: 1px solid var(--border); padding-top: 16px; margin-top: 40px; }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1>隨付即用(PAYG)· Agent 展示 —— 接口需求单</h1>
<p>提出方:<b>justsolutionsWebV2</b>(Node BFF + 静态站,官网 v2)。承接方:<b>AML_Backend</b> — <code>iCON.Abp.AMLPortal</code> / <code>iCON.Abp.AML</code>。</p>
<p>范围只有两块:官网的<b>隨付即用页</b>(<code>public/{region}/pages/pay-as-you-go.html</code>)与三处共用的<b>代理商卡片展示</b>(<code>_shared/js/agent-cards.js</code>,contact / plans-plus / pay-as-you-go)。订阅主流程(新购 / 续费 / 加购)不在本单内。</p>
<p class="meta">复核日期 2026-08-19 · 对照后端提交 <code>ef733325</code> · 对照前端 <code>justsolutionsWebV2@main</code>。下文所有「现状」都是照着这两份代码读出来的,不是推测。</p>
</header>
<div class="toc">
<h2>目录</h2>
<ol>
<li><a href="#howto">怎么读这份单子</a></li>
<li><a href="#sum">需求汇总表</a></li>
<li><a href="#payg">一 · 隨付即用(A1–A10)</a></li>
<li><a href="#agent">二 · Agent 展示(B1–B7)</a></li>
<li><a href="#appendix">附录 · 现有端点与枚举速查</a></li>
</ol>
</div>
<section id="howto">
<h2 class="sec">怎么读这份单子</h2>
<p>每条需求都按同一格式写:<b>现状</b>(后端现在是什么样,附代码位置)→ <b>问题</b>(这个现状让前端付出了什么代价)→ <b>建议</b>(希望后端提供什么)→ <b>不做的后果</b>。前端已经落地的临时方案一律写明,方便后端就绪后对照删除。</p>
<table>
<thead><tr><th style="width:80px">优先级</th><th>含义</th></tr></thead>
<tbody>
<tr><td><span class="pill p0">P0</span></td><td><b>阻断</b>:真实环境跑不通、或已经在往数据库里写脏数据、或存在数据泄露。上线前必须解决。</td></tr>
<tr><td><span class="pill p1">P1</span></td><td><b>完善</b>:现在能跑,但形态不对 —— 前端靠约定/猜测撑着,后端一改就静默出错,或用户体验有明显缺口。</td></tr>
<tr><td><span class="pill p2">P2</span></td><td><b>口径确认</b>:多半只需要后端回一句「是/不是」,或补一个配置项,不一定要写代码。</td></tr>
</tbody>
</table>
<div class="callout note">
<b>一个共同的背景</b>
官网 v2 不直接打后端:前端 → Node BFF(<code>justsolutionsWebV2/server/</code>)→ AMLPortal 门户端点(<code>[AbpAutoAuth("Portal")]</code>,免 token)。所以下面凡是说「BFF 现在自己兜着」,都指 <code>server/routes/*.js</code> 里写了一段本不该由前端承担的业务逻辑 —— 那正是希望搬回后端的部分。
</div>
</section>
<section id="sum">
<h2 class="sec">需求汇总表</h2>
<table class="sum">
<thead>
<tr><th style="width:52px">编号</th><th>需求</th><th style="width:300px">涉及端点 / DTO</th><th style="width:66px">优先级</th></tr>
</thead>
<tbody>
<tr><td class="id"><a href="#A1">A1</a></td><td>优惠码校验端点 + 建单入参</td><td>新端点 · <code>CreatePayAsYouGoOrderParam</code></td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#A2">A2</a></td><td>零价(免付款)单的真实路径,别再借线下审核单</td><td><code>CreatePayAsYouGoOrder</code> · <code>Order</code> 实体</td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#A3">A3</a></td><td><code>SubjectName</code> 允许为空(主体改由用户在检测链接页自填)</td><td><code>CreatePayAsYouGoOrder</code> · <code>UpdateOfflinePaygOrder</code> · <code>ActivatePaygOrder</code></td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#A4">A4</a></td><td>建单时校验该 agent 是否真的承接 P2G 方案</td><td><code>CreatePayAsYouGoOrder</code>(<code>AgentUserPlan</code>)</td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#A5">A5</a></td><td>权威的隨付即用报价端点,替掉三标签约定</td><td>新端点 <code>GetPayAsYouGoOffer</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#A6">A6</a></td><td>按订单号查 PAYG 单状态 / 重发检测链接</td><td>新端点 <code>QueryPaygOrder</code>(扩 <code>QueryOrderPayable</code> 亦可)</td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#A7">A7</a></td><td>本单包含哪些检测项(4 项固定)要能查</td><td>新端点 / 并入 A5</td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#A8">A8</a></td><td>香港以外市场的在线收款网关</td><td><code>PaymentGatewayEnums</code> · <code>PaymentWebhook</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#A9">A9</a></td><td>站点配置表(<code>IsPayAsYouGoEnabled</code> 等)</td><td>后端无此表,现写死在前端注册表</td><td class="pr"><span class="pill p2">P2</span></td></tr>
<tr><td class="id"><a href="#A10">A10</a></td><td><code>TwoC.ExpireOrderMins</code> 口径确认(含配 0 = 永不过期)</td><td><code>ClearExpiredPaygOrder</code></td><td class="pr"><span class="pill p2">P2</span></td></tr>
<tr><td class="id"><a href="#B1">B1</a></td><td><code>AgentByCountryDto</code> 补 <code>AgentRemark</code>,消掉 BFF 的 N+1</td><td><code>GetAgentsByCountry</code></td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#B2">B2</a></td><td>匿名的用户查询端点返回了完整用户对象(含邮箱电话)</td><td><code>SearchUserByCodeAndType</code></td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#B3">B3</a></td><td>代理列表要过滤停用用户、并按用户去重</td><td><code>GetAgentsByCountry</code></td><td class="pr"><span class="pill p0">P0</span></td></tr>
<tr><td class="id"><a href="#B4">B4</a></td><td>展示字段扩展(公司 / 城市 / 语言 / 网站 / 头像)与备注换行口径</td><td><code>AgentUserSetting</code> · <code>AgentRemark</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#B5">B5</a></td><td>上架开关 <code>IsPublished</code> + 排序 <code>DisplayOrder</code></td><td><code>AgentUserSetting</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#B6">B6</a></td><td>线下收款方式要能给出收款指引,而不只是一个枚举名</td><td><code>GetAgentPaymentMethodsByCode</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
<tr><td class="id"><a href="#B7">B7</a></td><td>代理列表支持按方案品类过滤(A4 的另一种实现口)</td><td><code>GetAgentsByCountryParam</code></td><td class="pr"><span class="pill p1">P1</span></td></tr>
</tbody>
</table>
</section>
<section id="payg">
<h2 class="sec">一 · 隨付即用(Pay-As-You-Go)</h2>
<p class="small">页面 <code>public/{region}/pages/pay-as-you-go.html</code> + <code>_shared/js/pay-as-you-go.js</code>;BFF <code>server/routes/single-query.js</code>、<code>promos.js</code>、<code>payments.js</code>。三条取件路径:<b>优惠码免付款</b> / <b>在线支付</b>(目前只有香港 · QFPay)/ <b>联络代理商</b>(线下)。</p>
<div class="req">
<h3 id="A1"><span class="no">A1</span> 优惠码校验端点 + 建单入参 <span class="pill p0">P0</span></h3>
<p class="lede">页面的默认取件方式就是优惠码,而后端至今没有任何优惠码概念。</p>
<table class="kv">
<tr><th>现状</th><td>
<code>CreatePayAsYouGoOrderParam</code> 只有 <code>PlanId / PlanDetailId / SubjectType / SubjectName / ContactEmail / IsOfflinePayment / AgentCode / AgentorId</code>,价格一律取 <code>PlanDetail.Price</code>,既无优惠码入参也无折扣位;<code>Order</code> 实体同样没有 <code>PromoCode</code> / <code>DiscountAmount</code>。
<div class="ref">AML_Backend/modules/iCON.Abp.AMLPortal/…/OrderAppLayer/CreatePayAsYouGoOrderParam.cs · Domain/DbEntity/Order.cs</div>
</td></tr>
<tr><th>前端现状</th><td>
BFF 里放了一个空壳模块 <code>server/routes/promos.js</code>:mock 环境(<code>APP_ENV=test</code>)用内置码表让流程能演示,<b>真实环境一律判无效并在日志点名</b>。也就是说现在真实站点上没有任何一个优惠码是能用的。校验必须在服务端做(建单时 <code>single-query.js</code> 会回头再验一次),因为前端那句「已套用」只是显示。
<div class="ref">justsolutionsWebV2/server/routes/promos.js:62 · server/routes/single-query.js:130</div>
</td></tr>
<tr><th>建议</th><td>
<p style="margin-top:0">① 一个校验端点(门户匿名可调,与其余 portal 端点同层):</p>
<pre><code>POST /api/amlPortal/Promo/portal/VerifyPromoCode
{ "Code": "FREEQ2026", "CountryCode": "HKG", "Scene": "PayAsYouGo" }
→ { "code": 0, "data": {
"code": "FREEQ2026",
"kind": "Free", // Free | Percent | Amount —— 前端按 kind 分流
"value": 0, // Percent: 折扣率;Amount: 直减额;Free: 0
"currencyCode": "HKD", // kind=Amount 时必需
"labelEN": "...", "labelCN": "...", "labelJP": "...",
"validTo": "2026-12-31T23:59:59",
"remainingCount": 998 // 可为 null(不限量)
} }</code></pre>
<p>② <code>CreatePayAsYouGoOrderParam</code> 增 <code>PromoCode</code>,后端建单时<b>再验一次</b>并自行算价(前端传来的金额一概不采信),把 <code>PromoCode</code> / <code>DiscountAmount</code> / 实际应付额落到 <code>Order</code> 上。</p>
<p class="small">前端目前只用得上 <code>kind: "Free"</code> 一种 —— 页面流程是「有码就不用付款」。折扣型(打完折还要收余额)会多出一条分支,等后端把规则定下来再一起做,接口先把 <code>kind</code> 留出来即可。</p>
</td></tr>
<tr><th>不做的后果</th><td>优惠码功能在真实环境等于不存在(页面上永远显示「优惠码无效」)。市场投放的码没有一个能核销。</td></tr>
<tr><th>就绪后</th><td>BFF 只改 <code>promos.js</code> 的 <code>verifyPromo()</code> 一个函数,路由与建单流程都不用动。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A2"><span class="no">A2</span> 零价(免付款)单的真实路径 <span class="pill p0">P0</span></h3>
<p class="lede">现在的免单是拿「线下待审核单」顶替的,而且金额还是原价。</p>
<table class="kv">
<tr><th>现状</th><td>
后端没有 PAYG 的零价路径。BFF 只能把优惠码免单按 <code>IsOfflinePayment=true</code> 建(<code>PenddingAudit</code> + 邮件通知归属 agent),<b>但 <code>Order.PlanPrice</code> 仍是全价</b>。
<div class="ref">justsolutionsWebV2/server/routes/single-query.js:144 · AML_Backend/…/OrderService.cs:1894</div>
</td></tr>
<tr><th>问题</th><td>
代理 / 财务在订单列表里看到的是一张<b>全价待审核单</b>,跟「真的要向客户收钱的线下单」长得一模一样,无从区分,也无从对账。免单量一大,这批脏单就得靠人肉记忆去认。<br>
对照:订阅侧是有零价路径的(<code>PendingActive</code> + 激活邮件 + <code>ActiveFreeOrder</code>),PAYG 侧没有对应物。
<div class="ref">AML_Backend/…/OrderController.cs:212 <code>ActiveFreeOrder</code></div>
</td></tr>
<tr><th>建议</th><td>
应付额为 0 时(无论是优惠码还是将来别的原因)走独立路径:<code>PaymentGateway = null</code>(实体注释里已经写明「免费单不经任何收款渠道,为 null」),直接置已付并调 <code>ActivatePaygOrder</code> 发检测链接;或与订阅侧对齐,先 <code>PendingActive</code> + 激活邮件,用户点击后再发链接。两种都行,请后端定一种。<br>
订单上要留得下痕迹:<code>PromoCode</code>、<code>DiscountAmount</code>、<code>PayableAmount</code>(或明确 <code>PlanPrice</code> 为原价、<code>PaidAmount=0</code> 的口径)。
</td></tr>
<tr><th>接口影响</th><td><code>CreatePayAsYouGoOrder</code> 的响应需要能让前端判断「这单不用付款、链接已发/待激活」 —— 目前 BFF 是自己拼 <code>requiresPayment</code> 字段给前端的,后端给出权威值最好。</td></tr>
<tr><th>不做的后果</th><td>免单继续污染线下单池;且免单用户拿不到自动发出的检测链接,全靠人工跟进。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A3"><span class="no">A3</span> <code>SubjectName</code> 允许为空 <span class="pill p0">P0</span></h3>
<p class="lede">购买页已经不收集检测主体了(付款后由用户在检测链接页自填),但后端这条链只拆了一半。</p>
<table class="kv">
<tr><th>现状</th><td>
<ul class="tight" style="margin-top:0">
<li><code>OrderService.cs:1835</code> —— 必填校验已被注释掉;</li>
<li><code>OrderService.cs:1899</code> —— 仍是 <code>SubjectName = input.SubjectName.Trim()</code>,传 null 直接 <b>NullReferenceException</b>;</li>
<li><code>ActivatePaygOrder</code> 用 <code>order.SubjectName</code> 作 <code>ConsumerCustomer.Name</code> 建检测链接;</li>
<li><code>UpdateOfflinePaygOrder</code> 仍强制 <code>SubjectName</code> 非空 —— 管理端确认线下收款前必须先补一个主体名。</li>
</ul>
<div class="ref">AML_Backend/…/OrderService.cs:1835 / 1899 / 667 · AML/…/ConsumerPortalService.cs:653</div>
</td></tr>
<tr><th>前端现状</th><td>
BFF 被迫塞占位:优先用联系人姓名,其次结果接收邮箱。<b>这个占位会一路流进 <code>Order.SubjectName</code> 和 <code>ConsumerCustomer.Name</code></b>,也就是检测客户会被命名成下单人的名字或一串邮箱。
<div class="ref">justsolutionsWebV2/server/routes/single-query.js:123-127</div>
</td></tr>
<tr><th>建议</th><td>
请后端二选一并明确告知:<br>
<b>(a)</b> 全链路允许空主体 —— 建单接受 null(<code>input.SubjectName?.Trim()</code>)、<code>ActivatePaygOrder</code> 建链接时主体名可空并由用户在链接页填写后回填、管理端改单也放开必填;<br>
<b>(b)</b> 主体仍必须在下单时收集 —— 那前端把这一栏加回购买页。<br>
现在这种「校验放开了但 <code>.Trim()</code> 还在、管理端又强制」的中间态是最糟的:前端不敢传空,只能造假数据。
</td></tr>
<tr><th>不做的后果</th><td>订单表与检测客户表持续被占位数据污染,且这批数据事后无法区分真假。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A4"><span class="no">A4</span> 建单时校验 agent 是否承接 P2G 方案 <span class="pill p0">P0</span></h3>
<p class="lede">订阅侧严格按 <code>AgentUserPlan</code> 裁剪可售方案,PAYG 侧完全不看。</p>
<table class="kv">
<tr><th>现状</th><td>
<code>CreatePayAsYouGoOrder</code> 对 <code>AgentCode</code> <b>只校验「这个人存在」</b>(<code>SearchUserByCodeAndType</code> 有结果就取 <code>users[0].Id</code>),不检查 <code>AgentUserPlan</code> 里有没有这条 P2G 计划的关联。
<div class="ref">AML_Backend/…/OrderService.cs:1871-1879 ↔ 对照 PlanService.cs:100-116(GetPlanList 的 AgentCode 裁剪)</div>
</td></tr>
<tr><th>问题</th><td>
隨付即用页的代理卡片列的是<b>该市场的全部代理</b>(<code>GetAgentsByCountry</code> 不带方案维度),用户完全可能选到一个根本不做隨付即用的代理。单照建、邮件照发给对方,对方收到一笔自己不承接的业务。<br>
这跟订阅页的取向也不一致 —— 那边「代理没有关联方案 = 不能买」是前端硬阻断的(<code>agentNoPlans</code>)。
</td></tr>
<tr><th>建议</th><td>建单时校验 <code>AgentUserPlan</code> 是否包含该 <code>PlanId/PlanDetailId</code>,不含则明确报错(前端会把错误原文展示给用户)。如果更希望在选择阶段就挡住,见 <a href="#B7">B7</a> —— 两者做一个即可,都做更好。</td></tr>
<tr><th>不做的后果</th><td>线下单派给错误的代理,用户等不到跟进;代理侧收到无法处理的订单邮件。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A5"><span class="no">A5</span> 权威的隨付即用报价端点 <span class="pill p1">P1</span></h3>
<p class="lede">现在价格是靠三个标签的约定,在两个地方各筛了一遍。</p>
<table class="kv">
<tr><th>现状</th><td>
没有专门的 PAYG 报价端点。前端和 BFF 各自调 <code>GetPlanList(countryCode)</code>,再按 <code>tag1Code=B · tag2Code=P2G · tag3Code=SP</code> 三个标签从一百条方案里挑出「零售直客价」那条(P2G 里还混着代理批发价 A6/A8 与 jQuota 充值包,所以三个标签一个都不能少)。
<div class="ref">justsolutionsWebV2/public/_shared/js/pay-as-you-go.js:104 · server/routes/single-query.js:77</div>
</td></tr>
<tr><th>问题</th><td>
<ul class="tight" style="margin-top:0">
<li>同一套筛选逻辑写了两份,改一处漏一处就会出现「页面标价」与「实际结算价」不一致;</li>
<li>标签是后台可配的自由文本 —— 运营改一次标签,页面就静默变成「暂无价格」,没有任何报错;</li>
<li>某市场若配出两条 <code>SP</code>,取到哪条取决于返回顺序。</li>
</ul>
</td></tr>
<tr><th>建议</th><td>
<pre><code>POST /api/amlPortal/plan/portal/GetPayAsYouGoOffer
{ "CountryCode": "HKG" }
→ { "code": 0, "data": {
"planId": "...", "planDetailId": "...",
"price": 200, "taxAmount": 0, "currencyCode": "HKD",
"nameEN": "Pay-As-You-Go", "nameCN": "隨付即用", "nameJP": "従量課金",
"functionCodes": ["V","E","A","D"], // 见 A7
"isAvailable": true // 该市场是否开售,见 A9
} }</code></pre>
<p class="small">要点是「本市场的隨付即用卖什么价」这个判断由后端一次给准,前端不再需要知道 tag 体系的存在。</p>
</td></tr>
<tr><th>不做的后果</th><td>标签或目录一动,官网静默显示「暂无价格」,且下单入口一起失效;排查要跨两个仓库。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A6"><span class="no">A6</span> 按订单号查 PAYG 单状态 / 重发检测链接 <span class="pill p1">P1</span></h3>
<p class="lede">付款成功之后,前端就彻底失明了。</p>
<table class="kv">
<tr><th>现状</th><td>
唯一可查的是 <code>QueryOrderPayable</code>,它只回「能不能付 + 金额」(<code>payable / reason / amount</code>),供付款失败后的「重新付款」判断用。检测链接由 <code>ActivatePaygOrder</code> 生成后直接发邮件,前端拿不到任何状态。
<div class="ref">AML_Backend/…/OrderService.cs:1545 · AML/…/ConsumerPortalService.cs:611</div>
</td></tr>
<tr><th>前端现状</th><td>付款返回页只能说「处理中,结果发邮件」,且明确不宣称已开通。用户没收到邮件时,页面上没有任何自助出路。<div class="ref">justsolutionsWebV2/public/_shared/js/pay-return.js:1-20</div></td></tr>
<tr><th>建议</th><td>
<pre><code>POST /api/amlPortal/Order/portal/QueryPaygOrder
{ "OrderCode": "P26...", "ContactEmail": "a@b.com" } // 邮箱作为轻量校验,防止按单号枚举
→ { "code": 0, "data": {
"orderCode": "...", "orderStatus": 400, "paymentStatus": 1,
"linkIssued": true, "linkSentTo": "a***@b.com",
"canResendLink": true
} }</code></pre>
<p>配套一个 <code>ResendPaygLink</code>(同样带邮箱校验 + 频率限制)就能把「没收到邮件」这条最常见的客诉在页面上自助解决。</p>
</td></tr>
<tr><th>不做的后果</th><td>「付了钱没收到链接」只能走人工客服,且客服也要到后台一条条查。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A7"><span class="no">A7</span> 本单包含哪些检测项要能查 <span class="pill p1">P1</span></h3>
<table class="kv">
<tr><th>现状</th><td>
检测项固定 4 项(<code>FunctionCodeEnums</code> 的 <code>V</code> 证件核验 / <code>E</code> 名单筛查 / <code>A</code> AI 增强 / <code>D</code> 失信),由后端 <code>GetPaygFunctionCodes()</code> 内部写死,<b>没有任何端点能读到</b>。mock 环境下 BFF 自己造了一份 <code>GET /api/single-query/options</code> 供演示,真实环境没有对应物。
<div class="ref">justsolutionsWebV2/server/mock/plans-plus.js:381(仅 mock)</div>
</td></tr>
<tr><th>问题</th><td>正式页面因此干脆不展示「这次买的包含什么」—— 一个按次付费的产品,页面上说不清卖的是什么。</td></tr>
<tr><th>建议</th><td>随 <a href="#A5">A5</a> 的报价一起返回 <code>functionCodes</code>,外加一份多语言名称/说明(或给一个 <code>GetFunctionCodeDisplay</code> 字典端点,前端自行拼装)。若产品口径上这 4 项会随方案变化,就更必须由后端给了。</td></tr>
<tr><th>不做的后果</th><td>页面继续只卖一个价格、不说明内容;文案若前端自己写死,后端调整检测项时官网不会跟着变。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A8"><span class="no">A8</span> 香港以外市场的在线收款网关 <span class="pill p1">P1</span></h3>
<table class="kv">
<tr><th>现状</th><td><code>PaymentGatewayEnums</code> 只有 <code>Offline=100</code> 与 <code>QFPay=200</code>,QFPay 只做 HK/HKD。</td></tr>
<tr><th>问题</th><td>global 站(市场 <code>All</code>,USD)已经开了隨付即用,但没有可用网关 —— 那里的用户只能走「联络代理商」线下。日本站目前整个不提供隨付即用,将来要开也是同一个问题。</td></tr>
<tr><th>建议</th><td>确认第二个网关的选型与时间点;接入时需要 <code>PaymentGatewayEnums</code> 增值 + <code>PaymentWebhook</code> 增分支 + 代理支付方式配置里可选。前端侧只需在 <code>ONLINE_PAYMENT_MARKETS</code> 加一个市场码,其余逻辑已经按「市场 → 是否开通线上收款」写好了。</td></tr>
<tr><th>不做的后果</th><td>非香港市场的隨付即用实际上只有线下一条路,转化率受限。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A9"><span class="no">A9</span> 站点配置表(<code>IsPayAsYouGoEnabled</code> 等) <span class="pill p2">P2</span></h3>
<table class="kv">
<tr><th>现状</th><td>设计上「本站是否提供隨付即用」应由后端站点配置 <code>SiteConfigs.IsPayAsYouGoEnabled</code> 决定,但该表与字段在后端各分支、本地库、线上 swagger 均查无。前端只能在注册表里按站点写死(香港 ✓ / 日本 ✗ / 其他國家 ✓)。同一档的还有各地区币别与支付方式(多地区设计 Q7 仍待定)。<div class="ref">justsolutionsWebV2/server/regions.js:182</div></td></tr>
<tr><th>建议</th><td>确认这张表要不要做。要做的话给一个门户可读端点即可(<code>GetSiteConfigs()</code> → 每个市场的 <code>isPayAsYouGoEnabled / isSubscriptionEnabled / currencyCode / paymentMethods</code>),前端改一个函数即可接上,调用方全都不动。</td></tr>
<tr><th>不做的后果</th><td>开关每次调整都要改前端代码 + 发版;运营改不了。</td></tr>
</table>
</div>
<div class="req">
<h3 id="A10"><span class="no">A10</span> <code>TwoC.ExpireOrderMins</code> 口径确认 <span class="pill p2">P2</span></h3>
<table class="kv">
<tr><th>现状</th><td>
PAYG 单的过期清理走独立任务 <code>ClearExpiredPaygOrder</code>,用的是 <code>TwoC.ExpireOrderMins</code>(<b>不是</b>订阅单那个 <code>Portal.ExpireOrderMins</code>),且<b>配成 0 或不配就永不过期</b>;过期只置 <code>Expired</code> 不软删(晚到的回调仍能复活)。
<div class="ref">AML_Backend/modules/iCON.Abp.AML/…/ConsumerPortalService.cs:696</div>
</td></tr>
<tr><th>要确认</th><td>各环境这两个值分别配的是多少、是否一致。前端「付款失败 → 还能不能重付」的时限提示目前是按 5 分钟写的(那是订阅单的口径)。若 PAYG 用了别的值,页面文案要跟着改。</td></tr>
<tr><th>不做的后果</th><td>用户被告知的时限与实际不符;线上配 0 时会积压永不过期的待支付单。</td></tr>
</table>
</div>
</section>
<section id="agent">
<h2 class="sec">二 · Agent 展示</h2>
<p class="small">三个页面共用一个组件 <code>_shared/js/agent-cards.js</code>:<b>contact</b>(侧栏代理卡片,选中即把查询指派给对方)、<b>plans-plus</b>(推薦人,决定可售方案与支付方式)、<b>pay-as-you-go</b>(联络代理商取件)。数据来自 <code>GET /api/agents?countryCode=</code> 与 <code>GET /api/agents/:code</code>,BFF 实现在 <code>server/routes/agents.js</code>。</p>
<div class="req">
<h3 id="B1"><span class="no">B1</span> <code>AgentByCountryDto</code> 补 <code>AgentRemark</code> <span class="pill p0">P0</span></h3>
<p class="lede">卡片正文就是这段备注,而列表端点偏偏不给 —— BFF 只能一个一个再查一遍。</p>
<table class="kv">
<tr><th>现状</th><td>
<code>GetAgentsByCountry</code> 返回的 <code>AgentByCountryDto</code> 只有 <code>AgentUserId / UserName / Name / UserCode / CountryCode</code>。而卡片正文要的 <code>AgentRemark</code>(代理自填的介绍 / 联络方式 / 收款账号)是 <code>IdentityUser</code> 的扩展属性,<b>后端其他地方都在用</b>(<code>SaveAgentUserSetting</code>、<code>SaveMyAgentRemark</code>、<code>AgentUserSettingDto</code>、线下单通知邮件),唯独这个列表 DTO 没带。
<div class="ref">AML_Backend/…/PlanAppLayer/AgentByCountryDto.cs · PlanService.cs:499-539</div>
</td></tr>
<tr><th>前端现状</th><td>
BFF 拿到列表后,<b>对每个 agent 再扇出一次</b> <code>GET /api/identity/users/SearchUserByCodeAndType/{code}/true</code> 去捞备注(N+1),靠一个 5 分钟的按市场整表缓存硬撑;单个查失败就让那张卡备注留空,不拖垮整张列表。
<div class="ref">justsolutionsWebV2/server/routes/agents.js:70-84</div>
</td></tr>
<tr><th>建议</th><td><code>AgentByCountryDto</code> 增 <code>AgentRemark</code>(<code>user.GetProperty&lt;string&gt;("AgentRemark")</code>,<code>PlanService.cs:415</code> 已有现成写法)。一并考虑 <a href="#B4">B4</a> 的其余展示字段,一次加完。</td></tr>
<tr><th>不做的后果</th><td>每次打开三个页面中任意一个,后端就要多承受 N 次用户查询;且这个 N+1 正是 <a href="#B2">B2</a> 那个泄露端点的唯一调用理由。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B2"><span class="no">B2</span> 匿名端点返回了完整用户对象 <span class="pill p0">P0</span></h3>
<p class="lede">这一条是安全问题,独立于前端要不要用它。</p>
<table class="kv">
<tr><th>现状</th><td>
<code>GET /api/identity/users/SearchUserByCodeAndType/{userCode}/{isFullMatch}</code> 标着 <code>[AllowAnonymous]</code>,返回完整的 <code>AppUserDto</code> —— 含 <code>email</code>、<code>phoneNumber</code> 和全部 <code>extraProperties</code>。<code>isFullMatchUserCode=false</code> 时 UserCode 还是<b>模糊匹配</b>(<code>Contains</code>)。
<div class="ref">AML_Backend/src/iCON.Abp.FX.HttpApi/Controllers/CustomIdentityUserController.cs:304-311 · EntityFrameworkCore/CustomIdentityUserRepository.cs:52</div>
</td></tr>
<tr><th>问题</th><td>任何人不带 token、用一个短前缀做模糊匹配,就能把平台上的代理用户连同邮箱、电话、全部扩展属性成批拉走。</td></tr>
<tr><th>建议</th><td>
<ul class="tight" style="margin-top:0">
<li>做完 <a href="#B1">B1</a> 后,前端就<b>不再需要这个端点</b>(BFF 会把这段扇出删掉)—— 届时请把它收回鉴权,或至少禁掉匿名下的模糊匹配;</li>
<li>若仍要保留匿名可调,请换成 portal 专用的精简 DTO(只给 <code>userCode / name / agentRemark</code>),别把 <code>AppUserDto</code> 直接吐出来。</li>
</ul>
</td></tr>
<tr><th>顺带</th><td>BFF 的 <code>GET /api/agents/:code</code>(详情)目前也靠它取邮箱/电话,用于「联络代理商」成功后展示对方联络方式。这个能力仍需要 —— 建议做成 portal 侧的 <code>GetAgentPublicProfile(agentCode)</code>,字段由后端决定哪些可公开。</td></tr>
<tr><th>不做的后果</th><td>用户联络方式持续处于可匿名枚举状态。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B3"><span class="no">B3</span> 列表要过滤停用用户、并按用户去重 <span class="pill p0">P0</span></h3>
<table class="kv">
<tr><th>现状</th><td>
<code>GetAgentsByCountry</code> 是 <code>foreach (settings)</code> 后按 <code>AgentUserId</code> 找用户拼结果,取用户用的 <code>GetUsersByIDs</code> <b>不看 <code>IsActive</code></b>。
<div class="ref">AML_Backend/…/PlanService.cs:507-537 · CustomIdentityUserRepository.cs:108</div>
</td></tr>
<tr><th>问题</th><td>
<ul class="tight" style="margin-top:0">
<li><b>停用/离职的代理仍会公开展示</b>,而且可被选中下单 —— 线下单会派给一个已经停用的账号;</li>
<li>遍历的是 <code>AgentUserSetting</code> 而不是用户:同一 agent 在同一国家有两条设置记录时,卡片会重复出现两张。</li>
</ul>
</td></tr>
<tr><th>建议</th><td>列表过滤 <code>IsActive == true</code>(或把 <code>isActive</code> 返回给前端由调用方决定),并按 <code>AgentUserId</code> 去重。前端不打算自己猜哪个代理还有效。</td></tr>
<tr><th>不做的后果</th><td>官网公开页面上出现失效代理,用户的查询/订单进入无人跟进的黑洞。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B4"><span class="no">B4</span> 展示字段扩展与备注换行口径 <span class="pill p1">P1</span></h3>
<p class="lede">现在一张代理卡片上能显示的东西,只有姓名、代码和一段自由文本。</p>
<table class="kv">
<tr><th>现状</th><td>卡片 = 头像占位符(写死的 emoji)+ <code>name</code> + <code>userCode</code> + <code>agentRemark</code> 纯文本。<div class="ref">justsolutionsWebV2/public/_shared/js/agent-cards.js:76-92</div></td></tr>
<tr><th>建议</th><td>
<p style="margin-top:0">若产品上希望「代理展示」是一块真正的展示位(而不只是一个选择器),需要后端在 <code>AgentUserSetting</code> 或用户扩展属性上补字段,并从门户 DTO 返回。建议字段:</p>
<table>
<thead><tr><th style="width:150px">字段</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>CompanyName</code></td><td>卡片主标题(个人名之外的机构身份)</td></tr>
<tr><td><code>City</code> / <code>CountryCode</code></td><td>「就近选代理」的排序与筛选依据</td></tr>
<tr><td><code>Languages</code></td><td>服务语言(<code>zh-HK;en;ja</code>),多语言站点按当前语言优先展示</td></tr>
<tr><td><code>WebsiteUrl</code></td><td>卡片上的外链</td></tr>
<tr><td><code>LogoUrl</code> / <code>AvatarUrl</code></td><td>替掉现在写死的 emoji 占位</td></tr>
<tr><td><code>Industries</code></td><td>擅长行业,可与订阅页的所屬行業联动</td></tr>
</tbody>
</table>
<p><b>另需确认 <code>AgentRemark</code> 的换行/富文本口径:</b>邮件侧已经在做 <code>\n → &lt;br /&gt;</code> 的转换(<code>EmailMessageService.cs:1302</code>),而网页侧是纯转义输出(防 XSS),所以代理在后台敲的换行到了官网会被压平成一段。要么约定它是纯文本(后台输入框也别让人换行),要么约定它保留换行、由前端按行渲染。</p>
</td></tr>
<tr><th>不做的后果</th><td>代理展示停留在「一段文字」的水平,无法做筛选、排序或任何视觉区分。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B5"><span class="no">B5</span> 上架开关 + 排序 <span class="pill p1">P1</span></h3>
<table class="kv">
<tr><th>现状</th><td><code>AgentUserSetting</code> 只有 <code>AgentUserId</code> + <code>CountryCode</code> 两个业务字段。卡片顺序 = 数据库返回顺序,没有任何控制手段。</td></tr>
<tr><th>问题</th><td>无法「让某个代理暂时不对外展示但保留其存量客户关系」,也无法把主推代理排在前面。当前唯一的下架手段是删掉设置记录 —— 那会连带影响已有归属关系。</td></tr>
<tr><th>建议</th><td><code>AgentUserSetting</code> 增 <code>IsPublished</code>(默认 true)与 <code>DisplayOrder</code>(默认 0,升序),<code>GetAgentsByCountry</code> 按 <code>IsPublished=true</code> 过滤并按 <code>DisplayOrder, Name</code> 排序;<code>SaveAgentUserSettingParam</code> 同步加这两个字段供后台维护。</td></tr>
<tr><th>不做的后果</th><td>代理展示顺序不可控;下架只能靠删数据。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B6"><span class="no">B6</span> 线下收款方式要给出收款指引 <span class="pill p1">P1</span></h3>
<table class="kv">
<tr><th>现状</th><td><code>GetAgentPaymentMethodsByCode</code> 返回的是网关枚举列表 —— <code>{ value, name, displayName }</code>,也就是 <code>Offline</code> / <code>QFPay</code> 两个名字。<div class="ref">AML_Backend/…/PlanService.cs:545-578</div></td></tr>
<tr><th>问题</th><td>用户选了「联络代理商 / 线下付款」之后,页面上给不出任何下一步:转账到哪、联系谁、要备注什么,全靠事后邮件。现在只能把代理的 <code>AgentRemark</code> 当收款指引用 —— 但那是一段自由文本,格式无保证。</td></tr>
<tr><th>建议</th><td>为 <code>Offline</code> 这一项附带结构化收款信息(收款账户 / 收款人 / 银行 / 备注模板,或至少一段专门的 <code>OfflinePaymentInstruction</code> 字段),与介绍性质的 <code>AgentRemark</code> 分开。</td></tr>
<tr><th>顺带</th><td>请确认「该代理没有配置任何支付方式」的语义:BFF 现在把<b>查询失败</b>与<b>明确的空列表</b>分开处理(失败 → null → 退回市场默认方式;空数组 → 该代理确实不支持任何网关 → 关闭下单入口)。若后端在未配置时也返回空数组,会让这些代理整批不可下单。</td></tr>
<tr><th>不做的后果</th><td>线下付款流程在页面上是断的,全量依赖人工邮件跟进。</td></tr>
</table>
</div>
<div class="req">
<h3 id="B7"><span class="no">B7</span> 代理列表支持按方案品类过滤 <span class="pill p1">P1</span></h3>
<table class="kv">
<tr><th>现状</th><td><code>GetAgentsByCountryParam</code> 只有 <code>CountryCode</code>,返回的是该国全部代理,不带任何业务维度。</td></tr>
<tr><th>问题</th><td>隨付即用页因此列出了包括「不承接隨付即用」在内的所有代理(见 <a href="#A4">A4</a>)。订阅页那边前端是靠改选代理后向 <code>GET /api/plans/available</code> 重查来发现「这个代理没有可售方案」的 —— 属于事后补救,而且多一次往返。</td></tr>
<tr><th>建议</th><td><code>GetAgentsByCountryParam</code> 增可选 <code>PlanTag2Code</code>(或 <code>PlanId</code>),按 <code>AgentUserPlan</code> 裁剪出真正承接该品类的代理。这样卡片列表从一开始就只显示可选项,用户不会选到死路。<br><span class="small">与 <a href="#A4">A4</a> 是同一问题的两个口子:A4 是建单兜底(必须做),B7 是选择阶段前置(体验)。</span></td></tr>
<tr><th>不做的后果</th><td>用户在代理卡片里选中一个无效选项,要到提交时才被拒。</td></tr>
</table>
</div>
</section>
<section id="appendix">
<h2 class="sec">附录 · 现有端点与枚举速查</h2>
<h4>本单涉及的现有端点</h4>
<table>
<thead><tr><th style="width:340px">端点</th><th style="width:110px">鉴权</th><th>用途 / 本单相关条目</th></tr></thead>
<tbody>
<tr><td><code>POST /api/amlPortal/Order/portal/CreatePayAsYouGoOrder</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>PAYG 建单 —— A1 / A2 / A3 / A4</td></tr>
<tr><td><code>POST /api/amlPortal/Order/portal/QueryOrderPayable</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>订单能否重付 —— A6 的现有近亲</td></tr>
<tr><td><code>POST /api/amlPortal/Order/portal/PaymentWebhook</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>QFPay 回调 —— A8</td></tr>
<tr><td><code>GET /api/amlPortal/Order/ActiveFreeOrder</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>订阅侧零价单激活 —— A2 的参照物(PAYG 无对应)</td></tr>
<tr><td><code>POST /api/amlPortal/plan/portal/GetPlanList</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>方案目录(PAYG 价格现在从这里筛)—— A5</td></tr>
<tr><td><code>POST /api/amlPortal/plan/portal/GetAgentsByCountry</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>代理列表 —— B1 / B3 / B5 / B7</td></tr>
<tr><td><code>POST /api/amlPortal/plan/portal/GetAgentPaymentMethodsByCode</code></td><td><code>AbpAutoAuth("Portal")</code></td><td>代理支付方式 —— B6</td></tr>
<tr><td><code>GET /api/identity/users/SearchUserByCodeAndType/{code}/{full}</code></td><td><b><code>AllowAnonymous</code></b></td><td>BFF 靠它补代理备注/联络方式 —— B1 / B2</td></tr>
</tbody>
</table>
<h4>枚举速查</h4>
<table>
<thead><tr><th style="width:190px">枚举</th><th>取值</th></tr></thead>
<tbody>
<tr><td><code>OrderTypeEnums</code></td><td><code>NewReg=100</code> · <code>Renewal=200</code> · <code>PayAsYouGo=400</code></td></tr>
<tr><td><code>OrderStatusEnums</code></td><td><code>PendingActive=100</code> · <code>PenddingPaid=200</code> · <code>Paid=300</code> · <code>Completed=400</code> · <code>Expired=500</code> · <code>PenddingAudit=600</code></td></tr>
<tr><td><code>PaymentGatewayEnums</code></td><td><code>Offline=100</code> · <code>QFPay=200</code>(免费单为 <code>null</code>)</td></tr>
<tr><td><code>FunctionCodeEnums</code></td><td>PAYG 固定 4 项:<code>V</code> 证件核验 · <code>E</code> ES 名单筛查 · <code>A</code> AI 检测 · <code>D</code> 失信查询</td></tr>
<tr><td>市场码</td><td><code>HKG</code>(香港站)· <code>JPN</code>(日本站)· <code>All</code>(其他國家站,注意与 <code>Plan.CountryCode='*'</code> 全国家通用不是一回事)</td></tr>
</tbody>
</table>
<h4>PAYG 现有链路(供对照)</h4>
<table>
<thead><tr><th style="width:110px">路径</th><th>订单落点</th><th>后续</th></tr></thead>
<tbody>
<tr><td>在线支付</td><td><code>PenddingPaid</code> + <code>QFPay</code></td><td>收银台 → <code>PaymentWebhook</code> → <code>OnPaygPaymentSuccess</code> → <code>ActivatePaygOrder</code>(建检测链接 + 发邮件 + 置 <code>Completed</code>)</td></tr>
<tr><td>联络代理商</td><td><code>PenddingAudit</code> + <code>Offline</code></td><td><code>SendOfflinePaygOrderMailToAgent</code> → 人工确认收款(管理端 <code>UpdateOfflinePaygOrder</code> 需补主体名)→ <code>ActivatePaygOrder</code></td></tr>
<tr><td>优惠码免单</td><td colspan="2"><span class="pill p0">缺</span> 目前<b>混用「联络代理商」那条线</b>且金额为全价 —— 正是 <a href="#A2">A2</a> 要解决的。</td></tr>
</tbody>
</table>
<div class="callout note">
<b>前端侧的对应改动</b>
A1 就绪 → 改 <code>server/routes/promos.js</code> 的 <code>verifyPromo()</code>;A5 就绪 → 删 <code>pay-as-you-go.js</code> 与 <code>single-query.js</code> 里两份标签筛选;A9 就绪 → 改 <code>server/regions.js</code> 的 <code>payAsYouGoEnabledFor()</code>;B1 就绪 → 删 <code>server/routes/agents.js</code> 的 <code>fetchAgentRemark</code> 扇出(同时解掉 B2 的调用理由)。这些位置在代码里都已写好注释标明「后端就绪后改这一处」。
</div>
</section>
<footer class="page">
justsolutionsWebV2 · 隨付即用与 Agent 展示接口需求单 · 2026-08-19 · 对照后端 <code>ef733325</code>
</footer>
</div>
</body>
</html>