docs: pay 对接指南升 v2 契约 + 授权管理屏文档同步 + db-schema 四新列

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-11 00:44:03 +08:00
parent 22e07613a1
commit 3b5d84a7e3
9 changed files with 195 additions and 80 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill | ✅ golden | ✅ golden |
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位)fidelity 3.04.2%≤8% | ✅ fidelity | ✅ fidelity |
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.43.0%≤8% | ✅ fidelity | ✅ fidelity |
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.72.3%≤8%**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好真实端同步待实现 | ✅ fidelity | ✅ fidelity |
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.72.3%≤8%**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端同步完成**`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行 | ✅ fidelity | ✅ fidelity |
| 用户管理(桌/移) | `users.html` | `users_screen.dart``/settings/users` | 同步 | ✅ **Phase2 新独立页**KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.32.1%≤8% | ✅ fidelity | ✅ fidelity |
| 关于我们(桌/移) | `about.html` | `about_screen.dart` | 同步 | ✅ **Phase2 重建**Hero/产品信息/更新日志 timeline(/public/release 同源)fidelity 2.22.7%≤8%;授权信息卡 2026-07-04 迁至 设置→授权管理 | ✅ fidelity | ✅ fidelity |
| 登录 | `login.html` | `login_screen.dart` | 同步 | ✅ **Phase2 重建(2026-07-03**:两栏卡片(品牌渐变面板+表单)+右上主题切换器+记住我/忘记密码+lg 主按钮;fidelity 1.42.1%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity |
+6 -2
View File
@@ -214,14 +214,18 @@
<div class="keys"><span class="tag pk">PRIMARY</span> <span class="mono kd">(`id`)</span><br><span class="tag uk">UNIQUE uk_code</span> <span class="mono kd">(`code`)</span><br><span class="tag ix">INDEX idx_status</span> <span class="mono kd">(`status`)</span><br><span class="tag ix">INDEX idx_redeemed_shop</span> <span class="mono kd">(`redeemed_shop_id`)</span></div>
</div><div class="card" id="license_purchases">
<h3><code class="tname">license_purchases</code> <span class="tcomment">在线购买/续费记录</span></h3>
<div class="callout ok"><b>pay 收款中枢:</b>契约见 <code>~/code/pay-contract</code>v1.0.0)。<code>out_trade_no</code> = pay 订单号,兼作对账键与幂等键(同一单只续期一次);<code>amount</code> 为 pay 下单响应回传的权威金额,webhook 回调时逐分核对</div>
<div class="callout ok"><b>pay 收款中枢:</b>契约见 <a href="pay支付对接开发指南.html">pay 支付对接开发指南</a>v2.02026-07-11 起 jiu↔pay 走 <code>/api/v2/orders</code>)。<code>out_trade_no</code> = pay 订单号,兼作对账键与幂等键(同一单只续期一次);<code>amount_minor</code>(v2 口径,int64 分)为查单/webhook 回传的权威金额,webhook 回调时逐分核对;旧 <code>amount</code> 列(v1 元字符串)保留只读兼容,观察一版后 DROP</div>
<table>
<thead><tr><th style="width:23%">列名</th><th style="width:20%">类型</th><th style="width:7%">可空</th><th style="width:16%">默认</th><th>说明</th></tr></thead>
<tbody><tr><td class="mono"><b>id</b> <span class="tag pk">PK</span></td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr"></td><td class="mono df">AUTO_INC</td><td></td></tr>
<tr><td class="mono">shop_id <span class="tag tnt">租户</span></td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr"></td><td class="mono df"></td><td></td></tr>
<tr><td class="mono">user_id</td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr"></td><td class="mono df"></td><td>下单管理员</td></tr>
<tr><td class="mono">product_biz_code</td><td class="mono ty">VARCHAR(64)</td><td class="ctr"></td><td class="mono df"></td><td>套餐稳定码:monthly_standard / annual_standard / monthly_pro / annual_pro</td></tr>
<tr><td class="mono">amount</td><td class="mono ty">VARCHAR(16)</td><td class="ctr"></td><td class="mono df">NULL</td><td>pay 下单回传金额,如 2999.00</td></tr>
<tr><td class="mono">amount_minor</td><td class="mono ty">BIGINT</td><td class="ctr"></td><td class="mono df">0</td><td>v2 口径:最小单位金额(CNY=分),如 299900=¥2999.00;下单 best-effort 查单回填,webhook 结算前核对</td></tr>
<tr><td class="mono">currency</td><td class="mono ty">VARCHAR(8)</td><td class="ctr"></td><td class="mono df">''</td><td>v2 口径:币种码,如 CNY</td></tr>
<tr><td class="mono">pay_url</td><td class="mono ty">VARCHAR(512)</td><td class="ctr"></td><td class="mono df">NULL</td><td>v2 下单 session payload.urlrender_type=redirect 时),订单管理「继续支付」复用</td></tr>
<tr><td class="mono">renewed_to</td><td class="mono ty">DATETIME</td><td class="ctr"></td><td class="mono df">NULL</td><td>settle 续期成功后的授权到期日,订单流水「授权续期至」展示</td></tr>
<tr><td class="mono">amount</td><td class="mono ty">VARCHAR(16)</td><td class="ctr"></td><td class="mono df">NULL</td><td><b>Deprecated</b>:v1 元字符串(如 2999.00),只读兼容输出,观察一版后 DROP</td></tr>
<tr><td class="mono">out_trade_no</td><td class="mono ty">VARCHAR(64)</td><td class="ctr"></td><td class="mono df">NULL</td><td>pay 订单号(对账+幂等键)</td></tr>
<tr><td class="mono">status</td><td class="mono ty">ENUM('pending','paid','failed')</td><td class="ctr"></td><td class="mono df">pending</td><td></td></tr>
<tr><td class="mono">trade_no</td><td class="mono ty">VARCHAR(64)</td><td class="ctr"></td><td class="mono df">NULL</td><td>渠道交易号(支付宝/微信)</td></tr>
+2 -2
View File
@@ -37,7 +37,7 @@
<h2>🏗 设计方案</h2>
<ul>
<li><a href="pay支付对接开发指南.html">pay 支付对接开发指南</a><span class="tag html">HTML</span> <span class="hint">— jiu 门店应用内购买/续费授权:付款走 pay,付成功后 pay 签名 webhook 回调 jiu 直接续期。含签名算法、下单/回调接口契约、套餐 biz_code→权益映射、续期逻辑、安全红线、客户端(Web+App webview)、联调清单。pay 侧已就绪,本文档=jiu 侧要实现的部分</span></li>
<li><a href="pay支付对接开发指南.html">pay 支付对接开发指南</a><span class="tag html">HTML</span> <span class="hint">— jiu 门店应用内购买/续费授权:付款走 pay,付成功后 pay 签名 webhook 回调 jiu 直接续期。v2.02026-07-11):/api/v2 契约、amount_minor 分级金额、session render_type 多态、webhook event_type 事件模型、取消透传、订单列表;v1 历史内容存文末附录。含签名算法、接口契约、套餐 biz_code→权益映射、续期逻辑、安全红线、客户端(Web+App)、联调清单</span></li>
<li><a href="design/inventory-sale-status.html">库存三态改造(在售/库存/已售)设计方案</a><span class="tag html">HTML</span> <span class="hint">— 库存行持久化状态 + 公开酒单只挂在售 + 卖光留痕已售 + 抽屉 switch2026-07-07</span></li>
<li><a href="design/public-page-speedup.html">公开页提速方案(传输压缩 + 公开页全面去 Flutter 化)</a><span class="tag html">HTML</span> <span class="hint">— 扫码首开 30s 根因与两级修复:CI 预压缩+gzip_static+协商缓存 / 商品页+店铺列表页后端 SSR,公开动线零 Flutter2026-07-07</span></li>
<li><a href="design/order-return-prototype.html">退单原型(已审核单据)</a><span class="tag html">HTML</span></li>
@@ -80,7 +80,7 @@
<ul>
<li><a href="plans/mobile-screens-implementation.html">移动端全屏落地实现计划(m-* 原型 → Flutter2026-07-04 已执行)</a><span class="tag html">HTML</span> <span class="hint">— 底部 tab + 我的 hub + sheet 交互范式;Stage0 基建 + 4 并行工作包 + 合流验收全记录</span></li>
<li><a href="plans/security-hardening.html">安全三件套实现计划(2026-07-05)</a><span class="tag html">HTML</span> <span class="hint">— 限流/登录锁外置 Redis + 公开接口反爬(日配额/UA/防盗链)+ 边缘加固(nginx limit_conn/安全组/DDoS runbook</span></li>
<li><a href="plans/pay-v2-integration.html">授权续费对接 pay v2 改造计划(2026-07-10执行中</a><span class="tag html">HTML</span> <span class="hint">— v1→v2 契约升级(金额 int64 分/session 多态/webhook 事件模型)+ 授权管理独立屏三 tab(订单管理);执行真相源 plans/2026-07-10-pay-v2-integration.md</span></li>
<li><a href="plans/pay-v2-integration.html">授权续费对接 pay v2 改造计划(2026-07-10已完成</a><span class="tag html">HTML</span> <span class="hint">— v1→v2 契约升级(金额 int64 分/session 多态/webhook 事件模型)+ 授权管理独立屏三 tab(订单管理);本地提交态,等用户验收部署;执行真相源 plans/2026-07-10-pay-v2-integration.md</span></li>
</ul>
</body>
</html>
+33 -8
View File
@@ -72,7 +72,8 @@
<li><a href="#ch7">往来单位</a></li>
<li><a href="#ch8">基础数据</a></li>
<li><a href="#ch9">设备与设置</a></li>
<li><a href="#ch10">常见问题 FAQ</a></li>
<li><a href="#ch10">授权管理</a></li>
<li><a href="#ch11">常见问题 FAQ</a></li>
</ol>
</div>
@@ -86,7 +87,7 @@
<ol>
<li><b>填写门店信息</b>:店名、地址、联系人。门店编号由系统自动分配,不用自己起。</li>
<li><b>创建管理员账号</b>:填登录账号和密码(密码至少 6 位),这个账号就是本店的管理员,以后由它来添加其他员工。</li>
<li><b>激活授权</b>:新门店自带 <b>30 天免费试用</b>,先用起来;有兑换券的话,登录后到「系统设置 → 授权兑换券」输入短码续期。</li>
<li><b>激活授权</b>:新门店自带 <b>30 天免费试用</b>,先用起来;有兑换券的话,登录后到「授权管理 → 授权信息」输入短码续期(第 10 章)</li>
</ol>
<p>提交成功后,记下系统分配的<b>门店编号</b>,登录时要用。</p>
</div>
@@ -405,7 +406,7 @@
</ul>
</div>
<h3>9.2 系统设置(个面板)</h3>
<h3>9.2 系统设置(个面板)</h3>
<div class="card">
<table>
<thead><tr><th style="width:20%">面板</th><th>说明</th></tr></thead>
@@ -413,11 +414,10 @@
<tr><td><b>门店信息</b></td><td>店名、地址、电话、负责人,管理员可编辑;门店编号不可改。这些信息会印在标签和单据上。</td></tr>
<tr><td><b>用户管理</b></td><td>新增用户(账号+初始密码+角色)、编辑、重置密码、启用/停用。四级角色见第 2 章。仅管理员可操作。</td></tr>
<tr><td><b>编号规则</b></td><td>入库单、出库单等单号的前缀与序号。<b>不要把序号调小</b>到已用过的范围,会撞号。</td></tr>
<tr><td><b>授权兑换券</b></td><td>本系统按「时长兑换券」授权:输入形如 <code>JIUKU-XXXX-XXXX</code> 的短码点「兑换续期」,时长直接<b>叠加</b>到现有到期日上,无需订阅。新门店自带 30 天试用。管理员也可点「在线购买 / 续费」选套餐后支付宝支付,到账自动续期。</td></tr>
<tr><td><b>偏好设置</b></td><td>三套主题切换 + <b>默认仓库</b>(新建单据时自动选中的仓库)。</td></tr>
</tbody>
</table>
<div class="callout warn"><b>授权到期会怎样?</b>过期 7 天内是宽限期,一切照常;过期 7–15 天进入<b>只读</b>,只能看不能录;满 15 天锁定登录。任何阶段兑换新券立即恢复,数据不会丢</div>
<div class="callout">授权相关(兑换券续期 / 在线购买 / 订单查询)已从系统设置迁出,见第 10 章「授权管理」独立入口</div>
</div>
<h3>9.3 关于我们</h3>
@@ -425,8 +425,33 @@
<p>版本信息(有新版可一键更新)、授权状态与到期日、更新日志时间线,以及「反馈 Bug / 功能建议」入口。</p>
</div>
<!-- ═══════════════════════ 10 FAQ ═══════════════════════ -->
<h2 id="ch10">10. 常见问题 FAQ</h2>
<!-- ═══════════════════════ 10 授权管理 ═══════════════════════ -->
<h2 id="ch10">10. 授权管理</h2>
<div class="who"><b>谁用</b>:授权信息人人可看,续费/购买/订单仅管理员 · <b>入口</b>:左侧导航「授权管理」(手机端「我的」→「授权管理」)</div>
<h3>10.1 授权信息</h3>
<div class="card">
<p>三格卡片看清当前状态:<b>授权类型</b>(月度/年度/永久/试用)、<b>到期日</b><b>剩余天数</b>。下方是<b>兑换券</b>输入框:输入形如 <code>JIUKU-XXXX-XXXX</code> 的短码点「兑换续期」,时长直接<b>叠加</b>到现有到期日上,无需订阅;新门店自带 30 天试用。再下方是<b>宽限/只读/锁定</b>规则说明卡,iOS 端另有「联系客服」引导卡(应用内购买合规限制,iOS 上不展示在线购买入口)。</p>
<div class="callout warn"><b>授权到期会怎样?</b>过期 7 天内是宽限期,一切照常;过期 7–15 天进入<b>只读</b>,只能看不能录;满 15 天锁定登录。任何阶段兑换新券或续费成功立即恢复,数据不会丢。</div>
</div>
<h3>10.2 在线购买 / 续费</h3>
<div class="card">
<p>管理员可在此选套餐(月付/年付 × 标准/高级)后点「立即购买」,跳转支付宝收银台完成付款;到账后系统<b>自动续期</b>,页面轮询到状态变化即时刷新,无需手动确认。金额以下单时套餐表为准。<b>iOS 应用内该 tab 不展示</b>(App Store 合规要求,需购买请在网页版或联系客服)。</p>
</div>
<h3>10.3 订单管理</h3>
<div class="card">
<p>仅管理员可见。列出本店全部购买/续费流水:套餐、金额、状态(<b>待支付/已支付/已关闭</b>)、下单人、下单与支付时间。顶部四格显示当前授权到期日、累计购买金额、订单总数、待支付笔数(点击可快速筛选)。</p>
<ul>
<li><b>待支付</b>订单可「继续支付」(重新打开收银台)或「取消订单」(放弃本单,不影响其他单)。</li>
<li><b>已支付</b>订单显示「授权续期至」——本次续费生效后的到期日。</li>
<li><b>已关闭</b>订单(超时/取消)可「重新购买」,一键切回购买 tab 选套餐。</li>
</ul>
</div>
<!-- ═══════════════════════ 11 FAQ ═══════════════════════ -->
<h2 id="ch11">11. 常见问题 FAQ</h2>
<div class="card">
<p class="faq-q">Q1:忘记密码了怎么办?</p>
<p class="faq-a">找本店管理员:「系统设置 → 用户管理」→ 你的账号 →「重置密码」。管理员自己忘了,联系技术支持处理。</p>
@@ -447,7 +472,7 @@
<p class="faq-a">早期版本的扫码报错(502 / 页面不存在)已修复。请确认手机有网络;很旧的标签可以在库存页重打一张新标签。</p>
<p class="faq-q">Q7:系统提示授权过期、变成只读了,怎么续?</p>
<p class="faq-a">系统设置 → 授权兑换券」输入 <code>JIUKU-</code> 开头的兑换码点「兑换续期」,或管理员「在线购买 / 续费」支付宝支付,到账立即恢复。过期 15 天内数据都还在,别慌。</p>
<p class="faq-a">授权管理 → 授权信息」输入 <code>JIUKU-</code> 开头的兑换码点「兑换续期」,或管理员切到「在线购买 / 续费」tab 选套餐支付宝支付,到账立即恢复。过期 15 天内数据都还在,别慌。</p>
<p class="faq-q">Q8:进货时价格还没谈好,能先入库吗?</p>
<p class="faq-a">能。进价留空提交即可(显示「待定价」),价格定了以后在入库单详情点「确认进价」补上,成本和应付自动补齐。出库同理,售价留空、事后「确认售价」。</p>
+132 -57
View File
@@ -44,34 +44,40 @@
<p style="font-size:12px;"><a href="index.html">← 文档索引</a></p>
<h1>pay 支付对接开发指南</h1>
<div class="sub">jiu 门店在应用内购买/续费授权:付款走 pay 收款服务,付成功后 pay 回调 jiu,jiu 直接给门店续期。</div>
<div class="meta">v1.0 · 2026-07-03 · 面向 jiu 后端开发 · pay 侧已就绪(下单+签名+webhook 推送已实现验证)· 本文档 = jiu 侧要实现的部分</div>
<div class="meta">v2.0 · 2026-07-11 · 面向 jiu 后端开发 · pay 侧 v2 契约已就绪(下单+签名+webhook 推送+查单+取消均已联调验证)· 本文档 = jiu 侧要实现的部分</div>
<div class="callout ok"><b>给开发者:</b>pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)<b>已开发并联调验证完毕</b>你只需实现 jiu 这一侧的 4 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底。接口契约、签名算法、权益映射见下,照着写即可</div>
<div class="callout ok"><b>给开发者:</b>pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)<b>已开发并联调验证完毕</b>。jiu 这一侧已实现 6 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底 ⑤ 取消透传 ⑥ 订单列表(授权管理·订单管理 tab)。接口契约、签名算法、权益映射见下。</div>
<div class="callout warn"><b>v1→v2 断代(2026-07-11):</b>jiu 对接 pay 服务的契约已从 <code>/api/v1/orders</code> 全面切到 <code>/api/v2/orders</code>——下单请求体、下单/查单响应形状、金额口径(string 元 → int64 最小单位)、webhook payload(新增 <code>event_type</code> 事件模型)均已变更。<b>v1 契约历史内容保留在文末「附录 A」仅供留痕参考,新对接一律照本文档主体(v2)实现。</b> jiu 自己对外的 4 个业务接口路径不变(仍是 <code>/api/v1/license/...</code>,这是 jiu 自己的 API 版本号,与 pay 服务的 <code>/api/v2</code> 无关,两者独立编号,未来变化各走各的)。</div>
<h2>1. 名词与地址</h2>
<table>
<tr><th></th><th></th></tr>
<tr><td>pay 服务地址</td><td class="mono">https://pay.51yanmei.com</td></tr>
<tr><td>pay 下单接口</td><td class="mono">POST /api/v1/orders</td></tr>
<tr><td>pay 查单接口</td><td class="mono">GET /api/v1/orders/{out_trade_no}</td></tr>
<tr><td>pay 套餐列表</td><td class="mono">GET /api/v1/products(含 biz_code</td></tr>
<tr><td>jiu 回调接收器(<b>你要实现</b></td><td class="mono">POST /api/v1/pay/callback</td></tr>
<tr><td>共享密钥</td><td>双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: <code>BIZ_JIU_SECRET</code>jiu 侧自定,同值)</td></tr>
<tr><td>pay 下单接口</td><td class="mono">POST /api/v2/orders</td></tr>
<tr><td>pay 查单接口</td><td class="mono">GET /api/v2/orders/{order_no}</td></tr>
<tr><td>pay 取消接口</td><td class="mono">POST /api/v2/orders/{order_no}/cancel</td></tr>
<tr><td>pay retry 接口</td><td class="mono">POST /api/v2/orders/{order_no}/retry</td></tr>
<tr><td>jiu 回调接收器(<b>已实现</b></td><td class="mono">POST /api/v1/pay/callback</td></tr>
<tr><td>jiu 购买/查单/取消/订单列表(<b>已实现</b></td><td class="mono">见 §4,均挂 <code>/api/v1/license/...</code></td></tr>
<tr><td>共享密钥</td><td>双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: <code>BIZ_JIU_SECRET</code>jiu 侧 <code>PAY_SECRET</code>,同值)</td></tr>
</table>
<div class="callout">jiu <b>不接 retry</b>(门店场景单渠道支付宝,无换支付方式需求;<code>POST /orders/:no/retry</code> 409 <code>currency_mismatch</code> 按约定等价于「新建订单」,客户端「重新选择」语义已覆盖,见 §8)。<b>接 cancel</b>:客户端「重新选择」时透传取消,防 pending 单堆积挤占查单兜底每轮 50 条限额。</div>
<h2>2. 整体流程</h2>
<div class="flow">jiu 客户端(已登录, 知道 shop_id) jiu 后端 pay 服务 支付宝
│ │ │ │
①选套餐, 点购买 ──────────────────────►│ │ │
│ ②建 Purchase(pending, shop_id, biz_code) │ │
│ 调 pay 下单 (product_id + biz_ref=purchase_id, 签名) ─►建订单
│ │◄──── {pay_url} ─────────────│
◄────────── 返回 pay_url ──────────────│ │
③打开 pay_url 付款 (PC扫码/手机拉App) ───────────────────────────────────────────────────►付款
│ 调 pay v2 下单 (sku=biz_code + biz_ref=purchase_id, 签名) ─►建订单│
│ │◄─ {order_no, session:{render_type,payload}} ──────
②b best-effort 查单回填 amount_minor/currency/subject(失败不阻断)
│◄──── 返回 render_type(redirect/qr) + payload + amount_minor ─────────│ │
③redirect→打开 payload.url 付款 / qr→卡内嵌二维码扫码 ────────────────────────────────────►付款
│ │ ④支付宝→pay 入账 ✓ │
│ │◄── ⑤pay 签名 webhook ───────│
⑥验签→幂等→按 biz_code 给 shop 续期→回 SUCCESS
'客户端跳回 return_url / 刷新 → 已续期 ✓│ </div>
│ │◄ ⑤pay 签名 webhook(event_type=payment.succeeded) ─
⑥验签→时间窗→nonce 防重放→按 event_type 分发→settle:金额核对→续期→回 SUCCESS
④客户端跳回 return_url / 3s轮询+30s心跳兜底 → 已续期 ✓│
│ (若「重新选择」放弃本单 → POST /orders/:no/cancel 透传取消,防 pending 堆积) │</div>
<h2>3. 签名算法(两个方向都用它)</h2>
<div class="card">
@@ -86,7 +92,13 @@
<tr><td class="mono">X-Pay-Sign</td><td>上面算出的签名</td></tr>
</table>
<p><code>rawBody</code> = HTTP 请求体的<b>原始字节</b>(验签/签名都对同一份原始 body,勿先反序列化再重拼)。</p>
<div class="callout warn">方向:<b>jiu→pay 下单</b> 由 jiu <b>签名</b>、pay 验签;<b>pay→jiu 回调</b> 由 pay 签名、jiu <b>验签</b>。两边同一把密钥、同一算法。</div>
<div class="callout warn">方向:<b>jiu→pay 下单</b> 由 jiu <b>签名</b>、pay 验签;<b>pay→jiu 回调</b> 由 pay 签名、jiu <b>验签</b>。两边同一把密钥、同一算法v1→v2 <b>零改动</b></div>
<div class="callout"><b>v2 新增两点(jiu 侧已实现):</b>
<ul style="margin:6px 0 0;">
<li><code class="mono">X-Pay-Event</code> 头(webhook 请求另带的事件类型提示)<b>不参与签名</b>,只作日志辅助;事件类型判定<b>以 body 里的 <code>event_type</code> 字段为准</b>(见 §5.2)。</li>
<li><b>nonce 防重放</b>:pay v2 无退避无死信、60s 固定重投,重投时会带新的 <code>X-Pay-Nonce</code>;jiu 侧自建内存去重表(10 分钟滚动窗口),<b>同一 nonce 原样重发</b>直接拒签(401),合法重投(新 nonce)不受影响——真正的幂等仍靠 §4②的 <code>out_trade_no</code> 短路。</li>
</ul>
</div>
</div>
<h3>Go 参考实现(与 pay 侧 <code>util.HMACSign</code> 完全一致)</h3>
<pre><span class="c">// 签名(下单时调用)/ 验签(收 webhook 时对比)</span>
@@ -97,31 +109,34 @@ func hmacSign(secret string, parts ...string) string {
}
<span class="c">// 验签:hmac.Equal(([]byte)hmacSign(secret, "jiu", ts, nonce, rawBody), got)</span></pre>
<h2>4. jiu 实现的</h2>
<h2>4. jiu 实现的</h2>
<h3><span class="n">1</span>购买接口 <span class="tag jiu">jiu</span> <code>POST /api/v1/license/purchase</code></h3>
<p>鉴权 = jiu 自己的登录态(当前用户/门店)。步骤:</p>
<p>鉴权 = jiu 自己的登录态(仅管理员/超管,handler 内判权)。步骤:</p>
<ol>
<li>校验登录态,拿到当前 <code>shop_id</code>;入参为套餐(前端传 biz_code 或 plan 标识)。</li>
<li><code>Purchase</code> 记录(status=pending,存 shop_id / product_biz_code / amount)。</li>
<li>调 pay 下单(见 §5.1<code>biz_ref = purchase_id</code><code>return_url</code> = jiu 结果页。</li>
<li>把 pay 返回的 <code>pay_url</code>(和 out_trade_no,回写 Purchase)返给客户端</li>
<li>校验登录态,拿到当前 <code>shop_id</code>;入参 <code>{"biz_code": "annual_standard"}</code>(前端传套餐稳定码)。</li>
<li><code>LicensePurchase</code> 记录(status=pending,存 shop_id / user_id / product_biz_code)。</li>
<li>调 pay v2 下单(见 §5.1<code>sku=biz_code</code><code>biz_ref=purchase_id</code><code>return_url</code>=jiu 结果页。</li>
<li>解析响应拿到 <code>order_no</code> + <code>session:{render_type,payload}</code>,回写 <code>out_trade_no</code><b>redirect</b> 态额外把 <code>payload.url</code> 存进 <code>pay_url</code> 列(订单管理「继续支付」复用同一收银台链接,避免二次下单)</li>
<li><b>best-effort 查单回填</b>v2 下单响应<b>不回传金额</b>(D1:价格权威永远在 pay),下单成功后立即调一次查单(§5.3)拿 <code>amount_minor/currency/subject</code> 回填;查单失败不阻断下单(金额留 0,等 webhook/查单兜底时再补)。</li>
<li><code>render_type/payload/amount_minor/currency</code> 连同兼容字段 <code>pay_url</code>redirect 态=payload.url)、<code>amount</code>(分转元字符串,官网/旧客户端读这两个)一并返给客户端。</li>
</ol>
<div class="callout">建议新增<code>license_purchases</code><span class="mono">id · shop_id · product_biz_code · amount · out_trade_no(index) · status(pending/paid/failed) · paid_at · created_at</span><code>out_trade_no</code> 兼作对账键与幂等键。</div>
<div class="callout"><code>license_purchases</code> 已落库(详见 <a href="db-schema.html#license_purchases">db-schema.html</a><span class="mono">id · shop_id · user_id · product_biz_code · amount_minor(分) · currency · pay_url · renewed_to · amount(Deprecated) · out_trade_no(uk) · status(pending/paid/failed) · trade_no · channel · paid_at · created_at</span><code>out_trade_no</code> 兼作对账键与幂等键jiu 自身的三态 <code>status</code>(区别于 pay 的八态,见 §5.3)不再新增枚举,pay 侧 canceled/expired 统一映射本地 failed</div>
<h3><span class="n">2</span>webhook 接收器 <span class="tag jiu">jiu</span> <code>POST /api/v1/pay/callback</code>(核心)</h3>
<ol>
<li><b>读原始 body</b>(勿被框架提前解析掉),取 4 个签名头。</li>
<li><b>验签</b><code>hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)</code><b>校验时间戳</b> ±5 分钟。任一不过 → 401,不处理。</li>
<li><b>幂等</b><code>out_trade_no</code> 查 Purchase;若已 paid → 直接回 <code>{"code":"SUCCESS"}</code>pay 会重发,必须幂等)。</li>
<li><b>金额校验</b>payload.amount 与 Purchase.amount 一致(分级比较,防篡改)。</li>
<li><b>续期</b><code>product_biz_code</code> 映射权益(§6),给该 shop 的 License <b>直接叠加 ExpiresAt</b>(§7)。</li>
<li>标记 Purchase=paid<b>回 HTTP 200 + <code>{"code":"SUCCESS"}</code></b>pay 认响应体含 SUCCESS 才算成功,否则退避重试)。</li>
<li><b>读原始 body</b>(勿被框架提前解析掉),取 3 个签名头<code>X-Pay-Timestamp/Nonce/Sign</code><code>X-Pay-Event</code> 不参与验签)</li>
<li><b>验签</b><code>hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)</code><b>校验时间戳</b> ±5 分钟<b>nonce 防重放</b>(§3。任一不过 → 401,不处理。</li>
<li><b><code>event_type</code> 分发</b><code>payment.succeeded</code> → 走入账(settle);其余事件(<code>refund.*</code> 等,本期不接,另起任务)→ 记 <code>ALERT</code> 日志后<b>照样回 SUCCESS ack</b>v2 无退避无死信,回非 SUCCESS 会被 60s 永久重投)。</li>
<li><b>入账(settle)幂等</b>:按 <code>out_trade_no</code> 查 Purchase<code>FOR UPDATE</code> 行锁);若已 paid → 直接短路成功(pay 会重发,必须幂等)。</li>
<li><b>金额校验</b>payload 的 <code>amount_minor+currency</code> 与 Purchase 一致(int64 精确比较,防篡改);若购买单金额仍为 0(下单回填失败的残单),<b>先补查一次</b>(§5.3)再核对,仍拿不到则 <code>ErrPayAmount</code> 拒绝入账(fail-closed,等 pay 重投)。</li>
<li><b>续期</b>:按 <code>product_biz_code</code> 映射权益(§6),给该 shop 的 License <b>直接叠加 ExpiresAt</b>(§7),并把续期后的到期日回写 Purchase.<code>renewed_to</code>(订单管理「授权续期至」展示用)。</li>
<li>标记 Purchase=paid<b>回 HTTP 200 + <code>{"code":"SUCCESS"}</code></b>(pay 认响应体前 4096 字节大小写不敏感含 SUCCESS 才算成功,否则 60s 重投)。</li>
</ol>
<div class="callout danger">这是「钱已到账」的入口——<b>验签 + 幂等 + 金额校验</b>者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。</div>
<div class="callout danger">这是「钱已到账」的入口——<b>验签 + nonce 防重放 + 幂等 + 金额核对</b>者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。</div>
<h3><span class="n">3</span>续期逻辑 <span class="tag jiu">jiu</span></h3>
<p>复用/参照现有 license 服务(见 <a href="architecture/license-design.md">授权体系设计</a>)。方式 = <b>直接叠加</b>(不生成兑换码):</p>
<p>复用/参照现有 license 服务(见 <a href="architecture/license-design.md">授权体系设计</a>)。方式 = <b>直接叠加</b>(不生成兑换码)v1→v2 <b>逻辑零改动</b>,仅多一步回写 <code>Purchase.renewed_to</code></p>
<pre>base := license.ExpiresAt
if base == nil || base.Before(now) { base = now } <span class="c">// 已过期从现在起算</span>
license.ExpiresAt = base + duration(bizCode) <span class="c">// +30 或 +365 天</span>
@@ -129,39 +144,60 @@ license.Tier = tier(bizCode) <span class="c">// standard
license.Type = billType(bizCode) <span class="c">// monthly / annual</span>
license.MaxDevices = maxDevices(bizCode) <span class="c">// 2 / 5</span>
license.Features = features(bizCode) <span class="c">// 仓库数/图片额度/AI 分析</span>
license.IsActive = true</pre>
license.IsActive = true
purchase.RenewedTo = license.ExpiresAt <span class="c">// v2 新增:订单流水「授权续期至」</span></pre>
<h3><span class="n">4</span>查单兜底 <span class="tag jiu">jiu</span></h3>
<p>防 webhook 全丢:对 pending 超时(如 &gt;5 分钟的 Purchase定时主动查 pay <code>GET /api/v1/orders/{out_trade_no}</code>,若返回 <code>status=paid</code> 走与 webhook 相同的续期入账(同样幂等)</p>
<p>防 webhook 全丢:后台每 60s 扫一次 pending 超 5 分钟的 Purchase,主动查 pay §5.3。按 v2 订单<b>八态</b>映射:<code>paid</code> 走与 webhook 相同的 settle 入账(同样幂等);<code>canceled/expired</code> → 本地标 <code>failed</code><code>refunding/partially_refunded/refunded</code> → no-op 记日志(退款接入另起任务);<code>created/pending</code> → 继续等下一轮</p>
<h2>5. 与 pay 的接口契约</h2>
<h3><span class="n">5</span>取消透传 <span class="tag jiu">jiu</span> <code>POST /api/v1/license/purchase/:out_trade_no/cancel</code></h3>
<p>仅管理员;只对本店 <code>status=pending</code> 的单生效(非 pending 幂等无害不外呼 pay)。调 pay §5.4,<code>canceled=true</code> 时本地条件更新 <code>WHERE id=? AND status='pending'</code><code>failed</code><code>canceled=false</code>(取消请求到达 pay 时单已被支付的竞态)时<b>本地保持 pending</b>,等 webhook/查单兜底正常入账——绝不能因为收到 <code>canceled:false</code> 就误标失败,否则钱已收但门店权益丢失。客户端「重新选择」套餐时 fire-and-forget 调用本接口(失败静默,查单兜底兜底)。</p>
<h3>5.1 下单 <span class="tag pay">pay</span> <code>POST https://pay.51yanmei.com/api/v1/orders</code></h3>
<h3><span class="n">6</span>订单列表 <span class="tag jiu">jiu</span> <code>GET /api/v1/license/purchases?page=&amp;page_size=&amp;status=</code></h3>
<p>仅管理员;授权管理「订单管理」tab 数据源。按 <code>created_at DESC</code> 分页,可选 <code>status</code> 筛选;返回每笔购买流水(含 <code>amount_minor/amount/pay_url/renewed_to</code><code>pay_url</code> 仅 pending 单非空)+ <code>summary</code>(累计已付金额/已付笔数/待支付笔数/总笔数,独立于分页/筛选,只看店内全量)+ <code>user_name</code>(下单人显示名,<code>real_name</code> 为空回退 <code>username</code>)。</p>
<h2>5. 与 pay 的接口契约(v2</h2>
<h3>5.1 下单 <span class="tag pay">pay</span> <code>POST https://pay.51yanmei.com/api/v2/orders</code></h3>
<pre><span class="c">// Headers: Content-Type: application/json + 4 个签名头(§3)</span>
<span class="c">// Body(对这份原始 body 签名):</span>
{
"product_id": 3, <span class="c">// pay 套餐 id(见 §6,或先 GET /products 按 biz_code 查 id</span>
"sku": "annual_standard", <span class="c">// 直接用 biz_code 当 sku,无需再查 product_idv1 的 productID 缓存整段已删除</span>
"method": "alipay",
"biz_system": "jiu",
"biz_ref": "&lt;purchase_id&gt;", <span class="c">// jiu 的购买记录 id,回调原样带回</span>
"return_url": "https://jiu.51yanmei.com/license/result"
}
<span class="c">// 成功响应:</span>
{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }</pre>
<p><code>data.pay_url</code> 交给客户端打开;<code>out_trade_no</code> 回写 Purchase。<b>金额以 pay 套餐表为准</b>,不要自己传金额。</p>
<span class="c">// 成功响应(不回传金额——D1:价格权威在 pay,下单后需另调 §5.3 查单拿金额)</span>
{ "data": { "order_no": "pay-x1...", "session": { "render_type": "redirect", "payload": { "url": "https://openapi.alipay.com/..." } } } }
<span class="c">// render_type 可能的值:redirect(外跳收银台,今天唯一实装) | qr(卡内嵌二维码,payload.qr_contentpay 侧文档有口径但暂无实现) | 未知值(客户端 toast 提示升级)</span></pre>
<p><code>data.order_no</code> 回写 Purchase.<code>out_trade_no</code><code>render_type=="redirect"</code> 时把 <code>payload.url</code> 存进 <code>pay_url</code> 兼容列。<b>金额以 pay 为权威</b>,下单响应不带金额,紧接着调 §5.3 查单回填。</p>
<h3>5.2 回调 <span class="tag pay">pay</span><span class="tag jiu">jiu</span>(pay 主动 POST 到你的接收器)</h3>
<pre><span class="c">// Headers: 4 个签名头(§3),你要验签</span>
<h3>5.2 回调 <span class="tag pay">pay</span><span class="tag jiu">jiu</span>pay 主动 POST 到你的接收器,路径不变</h3>
<pre><span class="c">// Headers: X-Pay-Timestamp/Nonce/Sign 3 个签名头(§3),你要验签;X-Pay-Event 头带事件类型提示但不参与签名</span>
{
"out_trade_no": "yanmei-20260703...-xxxx",
"event_type": "payment.succeeded", <span class="c">// v2 新增:事件分发以此字段为准;未来会有 refund.* 等事件(本期 jiu 只 ack 不处理)</span>
"out_trade_no": "pay-x1...",
"biz_system": "jiu",
"biz_ref": "&lt;purchase_id&gt;",
"product_biz_code": "annual_standard",
"amount": "2999.00",
"trade_no": "2026070322001...", <span class="c">// 支付宝交易号</span>
"amount_minor": 299900, <span class="c">// v2int64 最小单位(CNY=分),不再是 v1 的 "2999.00" 字符串</span>
"currency": "CNY",
"channel": "alipay",
"paid_at": "2026-07-03T14:36:30+08:00"
<span class="c">// 无 trade_no 字段(v1 有,v2 去掉了渠道交易号)</span>
}
<span class="c">// 你必须回:HTTP 200 + {"code":"SUCCESS"}否则 pay 每 60s 重试,24h 内)</span></pre>
<span class="c">// 你必须回:HTTP 200 + body 前 4096 字节含 "SUCCESS"(大小写不敏感),标准回包 {"code":"SUCCESS"}否则 pay 每 60s 重试,固定无退避无死信</span></pre>
<h3>5.3 查单 <span class="tag pay">pay</span> <code>GET https://pay.51yanmei.com/api/v2/orders/{order_no}</code>(无鉴权)</h3>
<pre><span class="c">// 成功响应(不回传 biz_ref/trade_no):</span>
{ "data": { "order_no": "pay-x1...", "status": "paid", "subject": "岩美酒库·标准版年付", "amount_minor": 299900, "currency": "CNY", "paid_at": "2026-07-03T14:36:30+08:00" } }
<span class="c">// status 八态:created | pending | paid | canceled | expired | refunding | partially_refunded | refunded(见 §4④映射规则)</span></pre>
<p>用途两处:①下单后 best-effort 回填金额(§4①);②查单兜底/webhook 残单核对前补查(§4②④)。查单无鉴权是 pay 既定惯例(不可猜 ID + payload 无敏感字段),非设计缺陷。</p>
<h3>5.4 取消 <span class="tag pay">pay</span> <code>POST https://pay.51yanmei.com/api/v2/orders/{order_no}/cancel</code>(无签名无请求体)</h3>
<pre><span class="c">// 成功响应,恒 200</span>
{ "data": { "canceled": true } } <span class="c">// true=确认取消(钱未扣/未入账);false=已终态或已支付竞态,本地不得跟着标失败(见 §4⑤)</span></pre>
<h2>6. 套餐 biz_code → 权益映射</h2>
<table>
@@ -171,37 +207,76 @@ license.IsActive = true</pre>
<tr><td>月付·高级</td><td class="mono">monthly_pro</td><td>¥599</td><td>+30 天</td><td>pro</td><td>5</td><td>单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析</td></tr>
<tr><td>年付·高级</td><td class="mono">annual_pro</td><td>¥5999</td><td>+365 天</td><td>pro</td><td>5</td><td>单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析</td></tr>
</table>
<p>建议 <code>Features</code>License.Features JSON)编码:<code>{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}</code>。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。</p>
<p>建议 <code>Features</code>License.Features JSON)编码:<code>{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}</code>。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。上表价格仍是人类可读展示;线上口径以 pay 查单/webhook 返回的 <code>amount_minor</code>(分)为准,如 ¥2999 ⇔ <code>299900</code></p>
<div class="callout warn">jiu 当前 Tier 未做能力差异(仅 standard)。标准/高级的<b>实际功能开关</b>(多仓库限制、图片额度、AI 分析)需 jiu 侧后续按此权益表落地;先把 tier/max_devices/features 正确写入 License,能力 gating 逐步接。</div>
<h2>7. 安全红线</h2>
<ul>
<li><b>验签必过</b>才处理(防伪造付款回调)。</li>
<li><b>幂等</b>:同一 out_trade_no 只续一次(pay 会重发)。</li>
<li><b>金额核对</b>:回调 amount == Purchase.amount</li>
<li><b>金额核对</b>:回调 <code>amount_minor+currency</code> == Purchase 落库值(int64 精确比较,不用浮点)</li>
<li><b>时间戳窗口</b> ±5 分钟,防重放。</li>
<li><b>nonce 防重放</b>:10 分钟窗口内同 nonce 拒签(§3)。</li>
<li>密钥只走环境变量 / Bitwarden<b>不写代码/配置/仓库</b></li>
<li>全程 HTTPSjiu.51yanmei.com 已有)。</li>
</ul>
<h2>8. 客户端(Web + App</h2>
<ul>
<li><b>Web 入口</b>:拿到 pay_url 直接 <code>window.location</code> 跳转;付完支付宝跳回 <code>return_url</code>(带 out_trade_no),结果页轮询 jiu 授权状态</li>
<li><b>App 入口(Flutter</b>:用<b>内置 webview</b> 打开 pay_url</li>
<li>手机端 pay 会走支付宝 <code>wap.pay</code>(H5)自动拉起支付宝 Appwebview 需<b>放行 <code>alipays://</code> / <code>alipay://</code> scheme 唤起</b>(拦截非 http(s) 跳转交系统打开),付完靠 <code>return_url</code> 回跳 webview 页面</li>
<li>到账<b>以 webhook 续期为准</b>,客户端 return 后<b> jiu 授权状态</b>确认,而非只信 return。</li>
<li><b>render_type 分发</b>(§5.1):<code>redirect</code>(今天唯一实装)→ 外跳 <code>payload.url</code>Web <code>window.location</code>App <code>launchUrl</code> 外部浏览器/支付宝 App);<code>qr</code> → 卡片内嵌二维码扫码(不外跳);未知值 → toast「当前版本暂不支持该支付方式,请升级应用」</li>
<li>手机端 pay 走支付宝 <code>wap.pay</code>(H5)自动拉起支付宝 App;App 端型透传(<code>is_mobile</code>)是 pay 侧已知欠账,补契约前恒走 page.pay,不影响 PC 扫码/浏览器跳转的正常路径</li>
<li>金额展示统一用 <code>amount_minor</code> 转元(分/100,2 位小数),0 时显示「—」(金额尚未回填,等轮询/reconcile 补齐)</li>
<li>到账<b>以 webhook 续期为准</b>,客户端 return 后<b>轮询 jiu 购买单状态</b>3s 间隔 + 30s 心跳兜底)确认,而非只信 return 跳转</li>
<li>「重新选择」套餐(放弃当前 pending 单)时调 §4⑤ 取消接口,防旧单堆积;失败静默,查单兜底会最终收敛。</li>
<li><b>授权管理·订单管理</b> tab(§4⑥数据源):待支付单「继续支付」= 复用下单时存的 <code>pay_url</code> 重新打开收银台(不新建订单,不接 retry);「取消订单」= 同上取消接口;已支付单展示「授权续期至」= <code>renewed_to</code></li>
</ul>
<h2>9. 联调 Checklist</h2>
<ol>
<li>约定并配置共享密钥(pay <code>BIZ_JIU_SECRET</code> = jiu 侧同值);pay 侧配 <code>BIZ_JIU_CALLBACK_URL</code> = jiu 接收器地址。</li>
<li>pay 侧 seed 4 个真实套餐(biz_code 见 §6),jiu 记下对应 product_id(或用 GET /products 动态取)</li>
<li>jiu 实现购买接口 → 下单 → 拿 pay_url(先验证签名被 pay 接受)</li>
<li>jiu 实现 webhook 接收器 → 用 pay 真实 1 分钱订单触发(临时把某套餐改 0.01)→ 验签/幂等/续期全走通</li>
<li>验证幂等(pay 重发不重复续期)、查单兜底(模拟 webhook 丢失)</li>
<li>客户端 Web + App webview 两端各跑一遍付款→回跳→授权已续</li>
<li>pay v2 合 main 部署;生产 <code>accounts</code> 配置 alipay 且套餐种子落库;共享密钥就位pay <code>BIZ_JIU_SECRET</code> = jiu <code>PAY_SECRET</code>,同值,走 Bitwarden);pay 侧配 <code>BIZ_JIU_CALLBACK_URL</code> = jiu 接收器地址jiu 侧 <code>PAY_BASE_URL</code> 指向 v2 部署</li>
<li>¥0.01/¥1 真单走通:下单 → redirect 收银台 → 实付 → webhook 入账续期 → 客户端轮询转 success → 权限即时恢复</li>
<li>阻断 webhook(临时改 callback_url)验证查单兜底(reconcile,每 60s 扫 pending&gt;5 分钟单)能补上账</li>
<li>重投验证:webhook 接收器临时回 FAIL,观察 pay 60s 重投与 jiu 幂等(不重复续期)</li>
<li>官网 checkout(读 <code>pay_url</code> 兼容字段)+ 旧版客户端购买路径回归,确认零改动仍可用</li>
<li>取消路径:客户端「重新选择」→ 观察 pay 订单翻 <code>canceled</code>、本地翻 <code>failed</code></li>
<li>真机验证手机拉起支付宝 App(等 pay 侧端型透传补契约后再测,见 §8)。</li>
</ol>
<p style="margin-top:26px;color:var(--muted);font-size:12px;">相关:<a href="architecture/license-design.md">授权体系设计(兑换券模型)</a> · <a href="db-schema.html">数据库 Schema</a> · pay 侧设计见 pay 仓 <code>docs/pay接入jiu授权续费对接方案.html</code></p>
<p style="margin-top:26px;color:var(--muted);font-size:12px;">相关:<a href="architecture/license-design.md">授权体系设计(兑换券模型)</a> · <a href="db-schema.html">数据库 Schema</a> · <a href="plans/pay-v2-integration.html">pay v2 改造计划(阅读版)</a> · pay 侧设计见 pay 仓 <code>docs/pay接入jiu授权续费对接方案.html</code></p>
<h2 style="margin-top:36px;">附录 Av1 契约(已弃用,2026-07-11 起停用,仅供历史参考)</h2>
<div class="callout warn">以下为 v1.02026-07-03)原始内容,<b>不再是当前实现</b>,保留仅为历史留痕/排障对照。新对接一律看上面 v2 主体内容。</div>
<div class="card">
<h3 style="margin-top:0;">v1 名词与地址</h3>
<table>
<tr><th></th><th></th></tr>
<tr><td>pay 下单接口</td><td class="mono">POST /api/v1/orders</td></tr>
<tr><td>pay 查单接口</td><td class="mono">GET /api/v1/orders/{out_trade_no}</td></tr>
<tr><td>pay 套餐列表</td><td class="mono">GET /api/v1/products(含 biz_code</td></tr>
</table>
<h3>v1 下单 <span class="tag pay">pay</span> <code>POST /api/v1/orders</code></h3>
<pre><span class="c">// Body</span>
{
"product_id": 3, <span class="c">// pay 套餐 id,需先 GET /products 按 biz_code 查</span>
"biz_system": "jiu",
"biz_ref": "&lt;purchase_id&gt;",
"return_url": "https://jiu.51yanmei.com/license/result"
}
<span class="c">// 成功响应:</span>
{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }</pre>
<h3>v1 回调 <span class="tag pay">pay</span><span class="tag jiu">jiu</span></h3>
<pre>{
"out_trade_no": "yanmei-20260703...-xxxx",
"biz_system": "jiu",
"biz_ref": "&lt;purchase_id&gt;",
"product_biz_code": "annual_standard",
"amount": "2999.00", <span class="c">// v1:元字符串,v2 改为 amount_minor(int64,分)</span>
"trade_no": "2026070322001...", <span class="c">// v2 已去掉此字段</span>
"channel": "alipay",
"paid_at": "2026-07-03T14:36:30+08:00"
}
<span class="c">// 回:HTTP 200 + {"code":"SUCCESS"}(否则 pay 每 60s 重试,24h 内——v2 改为固定无退避无死信)</span></pre>
<p style="margin-bottom:0;">v1 建议表结构 <span class="mono">id · shop_id · product_biz_code · amount · out_trade_no · status(pending/paid/failed) · paid_at · created_at</span> 已实际落库为 <code>license_purchases</code>v2 在此基础上新增 <code>amount_minor/currency/pay_url/renewed_to</code> 四列,旧 <code>amount</code> 列保留只读兼容(观察一版后 DROP),详见 <a href="db-schema.html#license_purchases">db-schema.html</a></p>
</div>
</body>
</html>
+2 -2
View File
@@ -318,8 +318,8 @@ waiting 态:`renderType=='qr'` 时嵌 `QrImageView(data: order.qrContent, size
**Files:** Modify: `docs/pay支付对接开发指南.html`(端点/签名示例/payload/金额口径/事件模型全节 v2 化,标注 v1 历史断代)、`docs/db-schema.html`license_purchases 两新列)、`docs/index.html`(登记本计划 HTML);Create: `docs/plans/pay-v2-integration.html`(本计划同内容 HTML 阅读版,沿用既有深色主题样式块)
- [ ] **8.1** 按本计划契约表更新开发指南;**8.2** db-schema 列级同步(含 pay_url/renewed_to);**8.3** CONTRACT 台账回填(授权管理屏实现完成、fidelity/golden 结果);**8.4** 用户手册补「授权管理/订单管理」节(`docs/manual/user-manual.html``web/content/docs.md` 两侧同步)
- [ ] **8.5 提交**`docs: pay 对接指南升 v2 契约 + 授权管理屏文档同步`
- [x] **8.1** 按本计划契约表更新开发指南;**8.2** db-schema 列级同步(含 pay_url/renewed_to);**8.3** CONTRACT 台账回填(授权管理屏实现完成、fidelity/golden 结果);**8.4** 用户手册补「授权管理/订单管理」节(`docs/manual/user-manual.html``web/content/docs.md` 两侧同步)
- [x] **8.5 提交**`docs: pay 对接指南升 v2 契约 + 授权管理屏文档同步`
> 执行顺序:Task 1→2→3→4→5→9(后端链)与 Task 6→7→10(前端链,依赖对应后端任务接口形状)交错推进,Task 8 收尾。
+1 -1
View File
@@ -29,7 +29,7 @@
.wrap{overflow-x:auto;}
</style></head><body>
<h1>jiu 授权续费对接 pay v2 改造计划</h1>
<div class="sub">2026-07-10 · 状态:<b>已批准,执行中</b> · 执行真相源 = <code>docs/plans/2026-07-10-pay-v2-integration.md</code>(checkbox),本页为阅读版 · pay v2 契约读自 <code>design/pay-v2</code> 分支代码(非文档口径)· 已并入授权管理独立屏三 tab 实现(原型评审通过 13b7274)</div>
<div class="sub">2026-07-10 · 状态:<b>10 个任务全部完成</b>(本地提交态,DoD 全绿;不部署不发版,等用户验收)· 执行真相源 = <code>docs/plans/2026-07-10-pay-v2-integration.md</code>(checkbox),本页为阅读版 · pay v2 契约读自 <code>design/pay-v2</code> 分支代码(非文档口径)· 已并入授权管理独立屏三 tab 实现(原型评审通过 13b7274)· Task 8 文档同步已收尾</div>
<h2>Context:调研结论改变了任务定位</h2><div class="card">
<p style="margin:4px 0"><b>jiu 两侧的 pay v1 对接已经完整上线</b>,本任务不是从零接入,而是<b>契约升级(v1 → v2</b></p>
+1 -1
View File
@@ -5,7 +5,7 @@ const md = require('markdown-it')({ html: false, breaks: false, linkify: false }
const TOC_GROUPS = [
{ group: '快速开始', nums: [1, 2] },
{ group: '业务操作', nums: [3, 4, 5, 6, 7, 8] },
{ group: '设置与答疑', nums: [9, 10] },
{ group: '设置与答疑', nums: [9, 10, 11] },
];
module.exports = function() {
+17 -6
View File
@@ -6,7 +6,7 @@
## 1. 快速上手
**注册新门店**:登录页点「注册新门店」,三步开通——①填门店信息(编号自动分配)②创建管理员账号(密码至少 6 位)③激活授权。新门店自带 **30 天免费试用**,有兑换券登录后在「系统设置 → 授权兑换券」输入短码续期。
**注册新门店**:登录页点「注册新门店」,三步开通——①填门店信息(编号自动分配)②创建管理员账号(密码至少 6 位)③激活授权。新门店自带 **30 天免费试用**,有兑换券登录后在「授权管理 → 授权信息」输入短码续期(第 10 章)
**登录**:输入门店编号、登录账号、密码。「记住我」会自动填入最近登录的门店和账号;账号框带历史下拉,多人共用电脑时点箭头切换。忘记密码请找本店管理员在「用户管理」中重置。
@@ -126,23 +126,34 @@
**设备管理**:登录设备(本店所有在线会话,管理员可「强制下线」)、外设登记(打印机、扫码枪存档)、打印模板。
**系统设置**个面板:
**系统设置**个面板:
| 面板 | 说明 |
|------|------|
| 门店信息 | 店名/地址/电话/负责人,管理员可编辑,编号不可改 |
| 用户管理 | 新增/编辑/重置密码/启停用户,仅管理员 |
| 编号规则 | 单号前缀与序号;序号切勿调小,会撞号 |
| 授权兑换券 | 输入 `JIUKU-XXXX-XXXX` 短码兑换续期,时长叠加;新店 30 天试用;管理员也可点「在线购买 / 续费」直接支付宝购买,到账自动续期 |
| 偏好设置 | 主题切换 + 默认仓库 |
授权过期:7 天内宽限照常用,7–15 天只读,满 15 天锁定;兑换新券立即恢复,数据不丢
授权相关(兑换券续期/在线购买/订单查询)已迁出系统设置,见第 10 章「授权管理」独立入口
「关于我们」页可查版本更新、授权状态、更新日志,并提交反馈。
---
## 10. 常见问题
## 10. 授权管理
**入口**:左侧导航「授权管理」(手机端「我的」→「授权管理」)。三个 tab:
- **授权信息**(人人可看):三格卡片显示授权类型/到期日/剩余天数;输入 `JIUKU-XXXX-XXXX` 短码点「兑换续期」,时长直接叠加到现有到期日;宽限/只读/锁定规则说明。
- **在线购买 / 续费**(仅管理员):选套餐(月付/年付 × 标准/高级)支付宝支付,到账自动续期,页面自动刷新。iOS App 内该 tab 不展示(应用商店合规要求)。
- **订单管理**(仅管理员):购买流水列表(套餐/金额/状态/下单人/时间),顶部四格 KPI(当前到期日/累计购买/订单数/待支付)。待支付单可「继续支付」或「取消订单」;已支付单显示「授权续期至」;已关闭单可「重新购买」。
授权过期:7 天内宽限照常用,7–15 天只读,满 15 天锁定;兑换新券或续费成功立即恢复,数据不丢。
---
## 11. 常见问题
**忘记密码?** 找管理员在「用户管理」重置;管理员本人忘记请联系技术支持。
@@ -156,7 +167,7 @@
**扫码打不开商品页?** 早期版本的扫码报错已修复;确认手机联网,很旧的标签可重打。
**授权到期变只读?**系统设置 → 授权兑换券」输入 JIUKU 兑换码,或管理员「在线购买 / 续费」支付宝支付,到账立即恢复;过期 15 天内数据都在。
**授权到期变只读?**授权管理 → 授权信息」输入 JIUKU 兑换码,或管理员切到「在线购买 / 续费」tab 支付宝支付,到账立即恢复;过期 15 天内数据都在。
**进价/售价还没谈好?** 价格留空即「待定价」,事后在单据详情「确认进价 / 确认售价」补填,账自动补齐。