docs(justsolutionsWebV2): 新增隨付即用与 Agent 展示接口需求单

按 PAYG(A1–A10)与 Agent 展示(B1–B7)两块整理提给 AML_Backend 的接口缺口,
每条含现状(附代码位置)/ 问题 / 建议接口 / 不做的后果,并标出前端已落地的
临时方案与后端就绪后要改的那一处。

P0:优惠码接口缺失、零价单借线下审核单顶替(金额仍为全价)、SubjectName
处于「校验放开但 .Trim() 仍在、管理端又强制」的中间态、建单不校验 agent 与
P2G 的 AgentUserPlan 关联;AgentByCountryDto 缺 AgentRemark 迫使 BFF 走 N+1、
该 N+1 打的 SearchUserByCodeAndType 是匿名且返回完整 AppUserDto、代理列表
不过滤 IsActive 也不按用户去重。

对照后端 ef733325 / 前端 justsolutionsWebV2@main 复核。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
fengruixiang 2026-08-19 15:17:47 +08:00
parent 3171cc18bf
commit 290a82b190
1 changed files with 533 additions and 0 deletions

View File

@ -0,0 +1,533 @@
<!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>