From 3b5d84a7e3b40702930d76642a5e89d43c2b9041 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 11 Jul 2026 00:44:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20pay=20=E5=AF=B9=E6=8E=A5=E6=8C=87?= =?UTF-8?q?=E5=8D=97=E5=8D=87=20v2=20=E5=A5=91=E7=BA=A6=20+=20=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=AE=A1=E7=90=86=E5=B1=8F=E6=96=87=E6=A1=A3=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20+=20db-schema=20=E5=9B=9B=E6=96=B0=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- design/CONTRACT.md | 2 +- docs/db-schema.html | 8 +- docs/index.html | 4 +- docs/manual/user-manual.html | 41 ++++- docs/pay支付对接开发指南.html | 189 ++++++++++++++------ docs/plans/2026-07-10-pay-v2-integration.md | 4 +- docs/plans/pay-v2-integration.html | 2 +- web/_data/docs.js | 2 +- web/content/docs.md | 23 ++- 9 files changed, 195 insertions(+), 80 deletions(-) diff --git a/design/CONTRACT.md b/design/CONTRACT.md index f208d96..b530815 100644 --- a/design/CONTRACT.md +++ b/design/CONTRACT.md @@ -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.0–4.2%≤8% | ✅ fidelity | ✅ fidelity | | 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity | -| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.3%≤8%;**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好(真实端同步待实现) | ✅ fidelity | ✅ fidelity | +| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.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.3–2.1%≤8% | ✅ fidelity | ✅ fidelity | | 关于我们(桌/移) | `about.html` | `about_screen.dart` | 同步 | ✅ **Phase2 重建**:Hero/产品信息/更新日志 timeline(/public/release 同源);fidelity 2.2–2.7%≤8%;授权信息卡 2026-07-04 迁至 设置→授权管理 | ✅ fidelity | ✅ fidelity | | 登录 | `login.html` | `login_screen.dart` | 同步 | ✅ **Phase2 重建(2026-07-03)**:两栏卡片(品牌渐变面板+表单)+右上主题切换器+记住我/忘记密码+lg 主按钮;fidelity 1.4–2.1%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity | diff --git a/docs/db-schema.html b/docs/db-schema.html index fedfb90..8b61c78 100644 --- a/docs/db-schema.html +++ b/docs/db-schema.html @@ -214,14 +214,18 @@
PRIMARY (`id`)
UNIQUE uk_code (`code`)
INDEX idx_status (`status`)
INDEX idx_redeemed_shop (`redeemed_shop_id`)

license_purchases 在线购买/续费记录

-
pay 收款中枢:契约见 ~/code/pay-contract(v1.0.0)。out_trade_no = pay 订单号,兼作对账键与幂等键(同一单只续期一次);amount 为 pay 下单响应回传的权威金额,webhook 回调时逐分核对。
+
pay 收款中枢:契约见 pay 支付对接开发指南(v2.0,2026-07-11 起 jiu↔pay 走 /api/v2/orders)。out_trade_no = pay 订单号,兼作对账键与幂等键(同一单只续期一次);amount_minor(v2 口径,int64 分)为查单/webhook 回传的权威金额,webhook 回调时逐分核对;旧 amount 列(v1 元字符串)保留只读兼容,观察一版后 DROP。
- + + + + + diff --git a/docs/index.html b/docs/index.html index a69dd25..3bcc933 100644 --- a/docs/index.html +++ b/docs/index.html @@ -37,7 +37,7 @@

🏗 设计方案

-

9.2 系统设置(五个面板)

+

9.2 系统设置(四个面板)

列名类型可空默认说明
id PKBIGINT UNSIGNEDAUTO_INC
shop_id 租户BIGINT UNSIGNED
user_idBIGINT UNSIGNED下单管理员
product_biz_codeVARCHAR(64)套餐稳定码:monthly_standard / annual_standard / monthly_pro / annual_pro
amountVARCHAR(16)NULLpay 下单回传金额,如 2999.00
amount_minorBIGINT0v2 口径:最小单位金额(CNY=分),如 299900=¥2999.00;下单 best-effort 查单回填,webhook 结算前核对
currencyVARCHAR(8)''v2 口径:币种码,如 CNY
pay_urlVARCHAR(512)NULLv2 下单 session payload.url(render_type=redirect 时),订单管理「继续支付」复用
renewed_toDATETIMENULLsettle 续期成功后的授权到期日,订单流水「授权续期至」展示
amountVARCHAR(16)NULLDeprecated:v1 元字符串(如 2999.00),只读兼容输出,观察一版后 DROP
out_trade_noVARCHAR(64)NULLpay 订单号(对账+幂等键)
statusENUM('pending','paid','failed')pending
trade_noVARCHAR(64)NULL渠道交易号(支付宝/微信)
@@ -413,11 +414,10 @@ -
面板说明
门店信息店名、地址、电话、负责人,管理员可编辑;门店编号不可改。这些信息会印在标签和单据上。
用户管理新增用户(账号+初始密码+角色)、编辑、重置密码、启用/停用。四级角色见第 2 章。仅管理员可操作。
编号规则入库单、出库单等单号的前缀与序号。不要把序号调小到已用过的范围,会撞号。
授权兑换券本系统按「时长兑换券」授权:输入形如 JIUKU-XXXX-XXXX 的短码点「兑换续期」,时长直接叠加到现有到期日上,无需订阅。新门店自带 30 天试用。管理员也可点「在线购买 / 续费」选套餐后支付宝支付,到账自动续期。
偏好设置三套主题切换 + 默认仓库(新建单据时自动选中的仓库)。
-
授权到期会怎样?过期 7 天内是宽限期,一切照常;过期 7–15 天进入只读,只能看不能录;满 15 天锁定登录。任何阶段兑换新券立即恢复,数据不会丢。
+
授权相关(兑换券续期 / 在线购买 / 订单查询)已从系统设置迁出,见第 10 章「授权管理」独立入口。

9.3 关于我们

@@ -425,8 +425,33 @@

版本信息(有新版可一键更新)、授权状态与到期日、更新日志时间线,以及「反馈 Bug / 功能建议」入口。

- -

10. 常见问题 FAQ

+ +

10. 授权管理

+
谁用:授权信息人人可看,续费/购买/订单仅管理员 · 入口:左侧导航「授权管理」(手机端「我的」→「授权管理」)
+ +

10.1 授权信息

+
+

三格卡片看清当前状态:授权类型(月度/年度/永久/试用)、到期日剩余天数。下方是兑换券输入框:输入形如 JIUKU-XXXX-XXXX 的短码点「兑换续期」,时长直接叠加到现有到期日上,无需订阅;新门店自带 30 天试用。再下方是宽限/只读/锁定规则说明卡,iOS 端另有「联系客服」引导卡(应用内购买合规限制,iOS 上不展示在线购买入口)。

+
授权到期会怎样?过期 7 天内是宽限期,一切照常;过期 7–15 天进入只读,只能看不能录;满 15 天锁定登录。任何阶段兑换新券或续费成功立即恢复,数据不会丢。
+
+ +

10.2 在线购买 / 续费

+
+

管理员可在此选套餐(月付/年付 × 标准/高级)后点「立即购买」,跳转支付宝收银台完成付款;到账后系统自动续期,页面轮询到状态变化即时刷新,无需手动确认。金额以下单时套餐表为准。iOS 应用内该 tab 不展示(App Store 合规要求,需购买请在网页版或联系客服)。

+
+ +

10.3 订单管理

+
+

仅管理员可见。列出本店全部购买/续费流水:套餐、金额、状态(待支付/已支付/已关闭)、下单人、下单与支付时间。顶部四格显示当前授权到期日、累计购买金额、订单总数、待支付笔数(点击可快速筛选)。

+ +
+ + +

11. 常见问题 FAQ

Q1:忘记密码了怎么办?

找本店管理员:「系统设置 → 用户管理」→ 你的账号 →「重置密码」。管理员自己忘了,联系技术支持处理。

@@ -447,7 +472,7 @@

早期版本的扫码报错(502 / 页面不存在)已修复。请确认手机有网络;很旧的标签可以在库存页重打一张新标签。

Q7:系统提示授权过期、变成只读了,怎么续?

-

「系统设置 → 授权兑换券」输入 JIUKU- 开头的兑换码点「兑换续期」,或管理员点「在线购买 / 续费」支付宝支付,到账立即恢复。过期 15 天内数据都还在,别慌。

+

「授权管理 → 授权信息」输入 JIUKU- 开头的兑换码点「兑换续期」,或管理员切到「在线购买 / 续费」tab 选套餐支付宝支付,到账立即恢复。过期 15 天内数据都还在,别慌。

Q8:进货时价格还没谈好,能先入库吗?

能。进价留空提交即可(显示「待定价」),价格定了以后在入库单详情点「确认进价」补上,成本和应付自动补齐。出库同理,售价留空、事后「确认售价」。

diff --git a/docs/pay支付对接开发指南.html b/docs/pay支付对接开发指南.html index 27efdc6..21110be 100644 --- a/docs/pay支付对接开发指南.html +++ b/docs/pay支付对接开发指南.html @@ -44,34 +44,40 @@

← 文档索引

pay 支付对接开发指南

jiu 门店在应用内购买/续费授权:付款走 pay 收款服务,付成功后 pay 回调 jiu,jiu 直接给门店续期。
-
v1.0 · 2026-07-03 · 面向 jiu 后端开发 · pay 侧已就绪(下单+签名+webhook 推送已实现验证)· 本文档 = jiu 侧要实现的部分
+
v2.0 · 2026-07-11 · 面向 jiu 后端开发 · pay 侧 v2 契约已就绪(下单+签名+webhook 推送+查单+取消均已联调验证)· 本文档 = jiu 侧要实现的部分
-
给开发者:pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)已开发并联调验证完毕。你只需实现 jiu 这一侧的 4 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底。接口契约、签名算法、权益映射见下,照着写即可。
+
给开发者:pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)已开发并联调验证完毕。jiu 这一侧已实现 6 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底 ⑤ 取消透传 ⑥ 订单列表(授权管理·订单管理 tab)。接口契约、签名算法、权益映射见下。
+
v1→v2 断代(2026-07-11):jiu 对接 pay 服务的契约已从 /api/v1/orders 全面切到 /api/v2/orders——下单请求体、下单/查单响应形状、金额口径(string 元 → int64 最小单位)、webhook payload(新增 event_type 事件模型)均已变更。v1 契约历史内容保留在文末「附录 A」仅供留痕参考,新对接一律照本文档主体(v2)实现。 jiu 自己对外的 4 个业务接口路径不变(仍是 /api/v1/license/...,这是 jiu 自己的 API 版本号,与 pay 服务的 /api/v2 无关,两者独立编号,未来变化各走各的)。

1. 名词与地址

- - - - - + + + + + + +
pay 服务地址https://pay.51yanmei.com
pay 下单接口POST /api/v1/orders
pay 查单接口GET /api/v1/orders/{out_trade_no}
pay 套餐列表GET /api/v1/products(含 biz_code)
jiu 回调接收器(你要实现POST /api/v1/pay/callback
共享密钥双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: BIZ_JIU_SECRET;jiu 侧自定,同值)
pay 下单接口POST /api/v2/orders
pay 查单接口GET /api/v2/orders/{order_no}
pay 取消接口POST /api/v2/orders/{order_no}/cancel
pay retry 接口POST /api/v2/orders/{order_no}/retry
jiu 回调接收器(已实现POST /api/v1/pay/callback
jiu 购买/查单/取消/订单列表(已实现见 §4,均挂 /api/v1/license/...
共享密钥双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: BIZ_JIU_SECRET;jiu 侧 PAY_SECRET,同值)
+
jiu 不接 retry(门店场景单渠道支付宝,无换支付方式需求;POST /orders/:no/retry 409 currency_mismatch 按约定等价于「新建订单」,客户端「重新选择」语义已覆盖,见 §8)。接 cancel:客户端「重新选择」时透传取消,防 pending 单堆积挤占查单兜底每轮 50 条限额。

2. 整体流程

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 / 刷新 → 已续期 ✓│ │ │
+ │ │◄ ⑤pay 签名 webhook(event_type=payment.succeeded) ─│ + │ ⑥验签→时间窗→nonce 防重放→按 event_type 分发→settle:金额核对→续期→回 SUCCESS│ + ④客户端跳回 return_url / 3s轮询+30s心跳兜底 → 已续期 ✓│ │ + │ (若「重新选择」放弃本单 → POST /orders/:no/cancel 透传取消,防 pending 堆积) │

3. 签名算法(两个方向都用它)

@@ -86,7 +92,13 @@ X-Pay-Sign上面算出的签名

rawBody = HTTP 请求体的原始字节(验签/签名都对同一份原始 body,勿先反序列化再重拼)。

-
方向:jiu→pay 下单 由 jiu 签名、pay 验签;pay→jiu 回调 由 pay 签名、jiu 验签。两边同一把密钥、同一算法。
+
方向:jiu→pay 下单 由 jiu 签名、pay 验签;pay→jiu 回调 由 pay 签名、jiu 验签。两边同一把密钥、同一算法,v1→v2 零改动
+
v2 新增两点(jiu 侧已实现): +
    +
  • X-Pay-Event 头(webhook 请求另带的事件类型提示)不参与签名,只作日志辅助;事件类型判定以 body 里的 event_type 字段为准(见 §5.2)。
  • +
  • nonce 防重放:pay v2 无退避无死信、60s 固定重投,重投时会带新的 X-Pay-Nonce;jiu 侧自建内存去重表(10 分钟滚动窗口),同一 nonce 原样重发直接拒签(401),合法重投(新 nonce)不受影响——真正的幂等仍靠 §4②的 out_trade_no 短路。
  • +
+

Go 参考实现(与 pay 侧 util.HMACSign 完全一致)

// 签名(下单时调用)/ 验签(收 webhook 时对比)
@@ -97,31 +109,34 @@ func hmacSign(secret string, parts ...string) string {
 }
 // 验签:hmac.Equal(([]byte)hmacSign(secret, "jiu", ts, nonce, rawBody), got)
-

4. jiu 要实现的四块

+

4. jiu 已实现的六块

1购买接口 jiu POST /api/v1/license/purchase

-

鉴权 = jiu 自己的登录态(当前用户/门店)。步骤:

+

鉴权 = jiu 自己的登录态(仅管理员/超管,handler 内判权)。步骤:

    -
  1. 校验登录态,拿到当前 shop_id;入参为套餐(前端传 biz_code 或 plan 标识)。
  2. -
  3. Purchase 记录(status=pending,存 shop_id / product_biz_code / amount)。
  4. -
  5. 调 pay 下单(见 §5.1),biz_ref = purchase_idreturn_url = jiu 结果页。
  6. -
  7. 把 pay 返回的 pay_url(和 out_trade_no,回写 Purchase)返给客户端。
  8. +
  9. 校验登录态,拿到当前 shop_id;入参 {"biz_code": "annual_standard"}(前端传套餐稳定码)。
  10. +
  11. LicensePurchase 记录(status=pending,存 shop_id / user_id / product_biz_code)。
  12. +
  13. 调 pay v2 下单(见 §5.1):sku=biz_codebiz_ref=purchase_idreturn_url=jiu 结果页。
  14. +
  15. 解析响应拿到 order_no + session:{render_type,payload},回写 out_trade_noredirect 态额外把 payload.url 存进 pay_url 列(订单管理「继续支付」复用同一收银台链接,避免二次下单)。
  16. +
  17. best-effort 查单回填:v2 下单响应不回传金额(D1:价格权威永远在 pay),下单成功后立即调一次查单(§5.3)拿 amount_minor/currency/subject 回填;查单失败不阻断下单(金额留 0,等 webhook/查单兜底时再补)。
  18. +
  19. render_type/payload/amount_minor/currency 连同兼容字段 pay_url(redirect 态=payload.url)、amount(分转元字符串,官网/旧客户端读这两个)一并返给客户端。
-
建议新增表 license_purchasesid · shop_id · product_biz_code · amount · out_trade_no(index) · status(pending/paid/failed) · paid_at · created_atout_trade_no 兼作对账键与幂等键。
+
license_purchases 已落库(详见 db-schema.html):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_atout_trade_no 兼作对账键与幂等键;jiu 自身的三态 status(区别于 pay 的八态,见 §5.3)不再新增枚举,pay 侧 canceled/expired 统一映射本地 failed。

2webhook 接收器 jiu POST /api/v1/pay/callback(核心)

    -
  1. 读原始 body(勿被框架提前解析掉),取 4 个签名头。
  2. -
  3. 验签hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)校验时间戳 ±5 分钟。任一不过 → 401,不处理。
  4. -
  5. 幂等:按 out_trade_no 查 Purchase;若已 paid → 直接回 {"code":"SUCCESS"}(pay 会重发,必须幂等)。
  6. -
  7. 金额校验:payload.amount 与 Purchase.amount 一致(分级比较,防篡改)。
  8. -
  9. 续期:按 product_biz_code 映射权益(§6),给该 shop 的 License 直接叠加 ExpiresAt(§7)。
  10. -
  11. 标记 Purchase=paid;回 HTTP 200 + {"code":"SUCCESS"}(pay 认响应体含 SUCCESS 才算成功,否则退避重试)。
  12. +
  13. 读原始 body(勿被框架提前解析掉),取 3 个签名头(X-Pay-Timestamp/Nonce/SignX-Pay-Event 不参与验签)。
  14. +
  15. 验签hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)校验时间戳 ±5 分钟;nonce 防重放(§3)。任一不过 → 401,不处理。
  16. +
  17. event_type 分发payment.succeeded → 走入账(settle);其余事件(refund.* 等,本期不接,另起任务)→ 记 ALERT 日志后照样回 SUCCESS ack(v2 无退避无死信,回非 SUCCESS 会被 60s 永久重投)。
  18. +
  19. 入账(settle)幂等:按 out_trade_no 查 Purchase(FOR UPDATE 行锁);若已 paid → 直接短路成功(pay 会重发,必须幂等)。
  20. +
  21. 金额校验:payload 的 amount_minor+currency 与 Purchase 一致(int64 精确比较,防篡改);若购买单金额仍为 0(下单回填失败的残单),先补查一次(§5.3)再核对,仍拿不到则 ErrPayAmount 拒绝入账(fail-closed,等 pay 重投)。
  22. +
  23. 续期:按 product_biz_code 映射权益(§6),给该 shop 的 License 直接叠加 ExpiresAt(§7),并把续期后的到期日回写 Purchase.renewed_to(订单管理「授权续期至」展示用)。
  24. +
  25. 标记 Purchase=paid;回 HTTP 200 + {"code":"SUCCESS"}(pay 认响应体前 4096 字节大小写不敏感含 SUCCESS 才算成功,否则 60s 重投)。
-
这是「钱已到账」的入口——验签 + 幂等 + 金额校验三者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。
+
这是「钱已到账」的入口——验签 + nonce 防重放 + 幂等 + 金额核对四者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。

3续期逻辑 jiu

-

复用/参照现有 license 服务(见 授权体系设计)。方式 = 直接叠加(不生成兑换码):

+

复用/参照现有 license 服务(见 授权体系设计)。方式 = 直接叠加(不生成兑换码),v1→v2 逻辑零改动,仅多一步回写 Purchase.renewed_to

base := license.ExpiresAt
 if base == nil || base.Before(now) { base = now }   // 已过期从现在起算
 license.ExpiresAt = base + duration(bizCode)         // +30 或 +365 天
@@ -129,39 +144,60 @@ license.Tier       = tier(bizCode)                   // standard
 license.Type       = billType(bizCode)               // monthly / annual
 license.MaxDevices = maxDevices(bizCode)             // 2 / 5
 license.Features    = features(bizCode)              // 仓库数/图片额度/AI 分析
-license.IsActive   = true
+license.IsActive = true +purchase.RenewedTo = license.ExpiresAt // v2 新增:订单流水「授权续期至」

4查单兜底 jiu

-

防 webhook 全丢:对 pending 超时(如 >5 分钟)的 Purchase,定时主动查 pay GET /api/v1/orders/{out_trade_no},若返回 status=paid 则走与 webhook 相同的续期入账(同样幂等)。

+

防 webhook 全丢:后台每 60s 扫一次 pending 超 5 分钟的 Purchase,主动查 pay §5.3。按 v2 订单八态映射:paid → 走与 webhook 相同的 settle 入账(同样幂等);canceled/expired → 本地标 failedrefunding/partially_refunded/refunded → no-op 记日志(退款接入另起任务);created/pending → 继续等下一轮。

-

5. 与 pay 的接口契约

+

5取消透传 jiu POST /api/v1/license/purchase/:out_trade_no/cancel

+

仅管理员;只对本店 status=pending 的单生效(非 pending 幂等无害不外呼 pay)。调 pay §5.4,canceled=true 时本地条件更新 WHERE id=? AND status='pending'failedcanceled=false(取消请求到达 pay 时单已被支付的竞态)时本地保持 pending,等 webhook/查单兜底正常入账——绝不能因为收到 canceled:false 就误标失败,否则钱已收但门店权益丢失。客户端「重新选择」套餐时 fire-and-forget 调用本接口(失败静默,查单兜底兜底)。

-

5.1 下单 pay POST https://pay.51yanmei.com/api/v1/orders

+

6订单列表 jiu GET /api/v1/license/purchases?page=&page_size=&status=

+

仅管理员;授权管理「订单管理」tab 数据源。按 created_at DESC 分页,可选 status 筛选;返回每笔购买流水(含 amount_minor/amount/pay_url/renewed_topay_url 仅 pending 单非空)+ summary(累计已付金额/已付笔数/待支付笔数/总笔数,独立于分页/筛选,只看店内全量)+ user_name(下单人显示名,real_name 为空回退 username)。

+ +

5. 与 pay 的接口契约(v2)

+ +

5.1 下单 pay POST https://pay.51yanmei.com/api/v2/orders

// Headers: Content-Type: application/json + 4 个签名头(§3)
 // Body(对这份原始 body 签名):
 {
-  "product_id": 3,                       // pay 套餐 id(见 §6,或先 GET /products 按 biz_code 查 id)
+  "sku": "annual_standard",              // 直接用 biz_code 当 sku,无需再查 product_id(v1 的 productID 缓存整段已删除)
+  "method": "alipay",
   "biz_system": "jiu",
   "biz_ref": "<purchase_id>",           // jiu 的购买记录 id,回调原样带回
   "return_url": "https://jiu.51yanmei.com/license/result"
 }
-// 成功响应:
-{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }
-

data.pay_url 交给客户端打开;out_trade_no 回写 Purchase。金额以 pay 套餐表为准,不要自己传金额。

+// 成功响应(不回传金额——D1:价格权威在 pay,下单后需另调 §5.3 查单拿金额): +{ "data": { "order_no": "pay-x1...", "session": { "render_type": "redirect", "payload": { "url": "https://openapi.alipay.com/..." } } } } +// render_type 可能的值:redirect(外跳收银台,今天唯一实装) | qr(卡内嵌二维码,payload.qr_content,pay 侧文档有口径但暂无实现) | 未知值(客户端 toast 提示升级) +

data.order_no 回写 Purchase.out_trade_norender_type=="redirect" 时把 payload.url 存进 pay_url 兼容列。金额以 pay 为权威,下单响应不带金额,紧接着调 §5.3 查单回填。

-

5.2 回调 payjiu(pay 主动 POST 到你的接收器)

-
// Headers: 4 个签名头(§3),你要验签
+  

5.2 回调 payjiu(pay 主动 POST 到你的接收器,路径不变)

+
// Headers: X-Pay-Timestamp/Nonce/Sign 3 个签名头(§3),你要验签;X-Pay-Event 头带事件类型提示但不参与签名
 {
-  "out_trade_no": "yanmei-20260703...-xxxx",
+  "event_type": "payment.succeeded",     // v2 新增:事件分发以此字段为准;未来会有 refund.* 等事件(本期 jiu 只 ack 不处理)
+  "out_trade_no": "pay-x1...",
   "biz_system": "jiu",
   "biz_ref": "<purchase_id>",
   "product_biz_code": "annual_standard",
-  "amount": "2999.00",
-  "trade_no": "2026070322001...",        // 支付宝交易号
+  "amount_minor": 299900,                // v2:int64 最小单位(CNY=分),不再是 v1 的 "2999.00" 字符串
+  "currency": "CNY",
   "channel": "alipay",
   "paid_at": "2026-07-03T14:36:30+08:00"
+  // 无 trade_no 字段(v1 有,v2 去掉了渠道交易号)
 }
-// 你必须回:HTTP 200 + {"code":"SUCCESS"}(否则 pay 每 60s 重试,24h 内)
+// 你必须回:HTTP 200 + body 前 4096 字节含 "SUCCESS"(大小写不敏感),标准回包 {"code":"SUCCESS"};否则 pay 每 60s 重试,固定无退避无死信
+ +

5.3 查单 pay GET https://pay.51yanmei.com/api/v2/orders/{order_no}(无鉴权)

+
// 成功响应(不回传 biz_ref/trade_no):
+{ "data": { "order_no": "pay-x1...", "status": "paid", "subject": "岩美酒库·标准版年付", "amount_minor": 299900, "currency": "CNY", "paid_at": "2026-07-03T14:36:30+08:00" } }
+// status 八态:created | pending | paid | canceled | expired | refunding | partially_refunded | refunded(见 §4④映射规则)
+

用途两处:①下单后 best-effort 回填金额(§4①);②查单兜底/webhook 残单核对前补查(§4②④)。查单无鉴权是 pay 既定惯例(不可猜 ID + payload 无敏感字段),非设计缺陷。

+ +

5.4 取消 pay POST https://pay.51yanmei.com/api/v2/orders/{order_no}/cancel(无签名无请求体)

+
// 成功响应,恒 200:
+{ "data": { "canceled": true } }  // true=确认取消(钱未扣/未入账);false=已终态或已支付竞态,本地不得跟着标失败(见 §4⑤)

6. 套餐 biz_code → 权益映射

@@ -171,37 +207,76 @@ license.IsActive = true
月付·高级monthly_pro¥599+30 天pro5单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析
年付·高级annual_pro¥5999+365 天pro5单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析
-

建议 Features(License.Features JSON)编码:{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。

+

建议 Features(License.Features JSON)编码:{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。上表价格仍是人类可读展示;线上口径以 pay 查单/webhook 返回的 amount_minor(分)为准,如 ¥2999 ⇔ 299900

jiu 当前 Tier 未做能力差异(仅 standard)。标准/高级的实际功能开关(多仓库限制、图片额度、AI 分析)需 jiu 侧后续按此权益表落地;先把 tier/max_devices/features 正确写入 License,能力 gating 逐步接。

7. 安全红线

8. 客户端(Web + App)

9. 联调 Checklist

    -
  1. 约定并配置共享密钥(pay BIZ_JIU_SECRET = jiu 侧同值);pay 侧配 BIZ_JIU_CALLBACK_URL = jiu 接收器地址。
  2. -
  3. pay 侧 seed 4 个真实套餐(biz_code 见 §6),jiu 记下对应 product_id(或用 GET /products 动态取)。
  4. -
  5. jiu 实现购买接口 → 下单 → 拿 pay_url(先验证签名被 pay 接受)。
  6. -
  7. jiu 实现 webhook 接收器 → 用 pay 真实 1 分钱订单触发(临时把某套餐改 0.01)→ 验签/幂等/续期全走通。
  8. -
  9. 验证幂等(pay 重发不重复续期)、查单兜底(模拟 webhook 丢失)。
  10. -
  11. 客户端 Web + App webview 两端各跑一遍付款→回跳→授权已续。
  12. +
  13. pay v2 合 main 部署;生产 accounts 配置 alipay 且套餐种子落库;共享密钥就位(pay BIZ_JIU_SECRET = jiu PAY_SECRET,同值,走 Bitwarden);pay 侧配 BIZ_JIU_CALLBACK_URL = jiu 接收器地址;jiu 侧 PAY_BASE_URL 指向 v2 部署。
  14. +
  15. ¥0.01/¥1 真单走通:下单 → redirect 收银台 → 实付 → webhook 入账续期 → 客户端轮询转 success → 权限即时恢复。
  16. +
  17. 阻断 webhook(临时改 callback_url)验证查单兜底(reconcile,每 60s 扫 pending>5 分钟单)能补上账。
  18. +
  19. 重投验证:webhook 接收器临时回 FAIL,观察 pay 60s 重投与 jiu 幂等(不重复续期)。
  20. +
  21. 官网 checkout(读 pay_url 兼容字段)+ 旧版客户端购买路径回归,确认零改动仍可用。
  22. +
  23. 取消路径:客户端「重新选择」→ 观察 pay 订单翻 canceled、本地翻 failed
  24. +
  25. 真机验证手机拉起支付宝 App(等 pay 侧端型透传补契约后再测,见 §8)。
-

相关:授权体系设计(兑换券模型) · 数据库 Schema · pay 侧设计见 pay 仓 docs/pay接入jiu授权续费对接方案.html

+

相关:授权体系设计(兑换券模型) · 数据库 Schema · pay v2 改造计划(阅读版) · pay 侧设计见 pay 仓 docs/pay接入jiu授权续费对接方案.html

+ +

附录 A:v1 契约(已弃用,2026-07-11 起停用,仅供历史参考)

+
以下为 v1.0(2026-07-03)原始内容,不再是当前实现,保留仅为历史留痕/排障对照。新对接一律看上面 v2 主体内容。
+
+

v1 名词与地址

+ + + + + +
pay 下单接口POST /api/v1/orders
pay 查单接口GET /api/v1/orders/{out_trade_no}
pay 套餐列表GET /api/v1/products(含 biz_code)
+

v1 下单 pay POST /api/v1/orders

+
// Body:
+{
+  "product_id": 3,                       // pay 套餐 id,需先 GET /products 按 biz_code 查
+  "biz_system": "jiu",
+  "biz_ref": "<purchase_id>",
+  "return_url": "https://jiu.51yanmei.com/license/result"
+}
+// 成功响应:
+{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }
+

v1 回调 payjiu

+
{
+  "out_trade_no": "yanmei-20260703...-xxxx",
+  "biz_system": "jiu",
+  "biz_ref": "<purchase_id>",
+  "product_biz_code": "annual_standard",
+  "amount": "2999.00",                   // v1:元字符串,v2 改为 amount_minor(int64,分)
+  "trade_no": "2026070322001...",        // v2 已去掉此字段
+  "channel": "alipay",
+  "paid_at": "2026-07-03T14:36:30+08:00"
+}
+// 回:HTTP 200 + {"code":"SUCCESS"}(否则 pay 每 60s 重试,24h 内——v2 改为固定无退避无死信)
+

v1 建议表结构 id · shop_id · product_biz_code · amount · out_trade_no · status(pending/paid/failed) · paid_at · created_at 已实际落库为 license_purchases,v2 在此基础上新增 amount_minor/currency/pay_url/renewed_to 四列,旧 amount 列保留只读兼容(观察一版后 DROP),详见 db-schema.html

+
diff --git a/docs/plans/2026-07-10-pay-v2-integration.md b/docs/plans/2026-07-10-pay-v2-integration.md index f91eba1..5cfa662 100644 --- a/docs/plans/2026-07-10-pay-v2-integration.md +++ b/docs/plans/2026-07-10-pay-v2-integration.md @@ -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 收尾。 diff --git a/docs/plans/pay-v2-integration.html b/docs/plans/pay-v2-integration.html index 7bcc6db..3416b3a 100644 --- a/docs/plans/pay-v2-integration.html +++ b/docs/plans/pay-v2-integration.html @@ -29,7 +29,7 @@ .wrap{overflow-x:auto;}

jiu 授权续费对接 pay v2 改造计划

-
2026-07-10 · 状态:已批准,执行中 · 执行真相源 = docs/plans/2026-07-10-pay-v2-integration.md(checkbox),本页为阅读版 · pay v2 契约读自 design/pay-v2 分支代码(非文档口径)· 已并入授权管理独立屏三 tab 实现(原型评审通过 13b7274)
+
2026-07-10 · 状态:10 个任务全部完成(本地提交态,DoD 全绿;不部署不发版,等用户验收)· 执行真相源 = docs/plans/2026-07-10-pay-v2-integration.md(checkbox),本页为阅读版 · pay v2 契约读自 design/pay-v2 分支代码(非文档口径)· 已并入授权管理独立屏三 tab 实现(原型评审通过 13b7274)· Task 8 文档同步已收尾

Context:调研结论改变了任务定位

jiu 两侧的 pay v1 对接已经完整上线,本任务不是从零接入,而是契约升级(v1 → v2)

diff --git a/web/_data/docs.js b/web/_data/docs.js index d2b02be..c456a8c 100644 --- a/web/_data/docs.js +++ b/web/_data/docs.js @@ -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() { diff --git a/web/content/docs.md b/web/content/docs.md index bf52fda..36b8cba 100644 --- a/web/content/docs.md +++ b/web/content/docs.md @@ -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 天内数据都在。 **进价/售价还没谈好?** 价格留空即「待定价」,事后在单据详情「确认进价 / 确认售价」补填,账自动补齐。