← 文档索引

pay 支付对接开发指南

jiu 门店在应用内购买/续费授权:付款走 pay 收款服务,付成功后 pay 回调 jiu,jiu 直接给门店续期。
v2.0 · 2026-07-11 · 面向 jiu 后端开发 · pay 侧 v2 契约已就绪(下单+签名+webhook 推送+查单+取消均已联调验证)· 本文档 = jiu 侧要实现的部分
给开发者: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/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 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(event_type=payment.succeeded) ─│ │ ⑥验签→时间窗→nonce 防重放→按 event_type 分发→settle:金额核对→续期→回 SUCCESS│ ④客户端跳回 return_url / 3s轮询+30s心跳兜底 → 已续期 ✓│ │ │ (若「重新选择」放弃本单 → POST /orders/:no/cancel 透传取消,防 pending 堆积) │

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

签名串(4 段用换行 \n 连接,再 HMAC-SHA256,最后 base64):

sign = base64( HMAC_SHA256( secret, system + "\n" + timestamp + "\n" + nonce + "\n" + rawBody ) )

请求头

Header说明
X-Pay-System固定 jiu
X-Pay-TimestampUnix 秒(服务端校验 ±5 分钟窗口)
X-Pay-Nonce随机串(如 uuid)
X-Pay-Sign上面算出的签名

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

方向:jiu→pay 下单 由 jiu 签名、pay 验签;pay→jiu 回调 由 pay 签名、jiu 验签。两边同一把密钥、同一算法,v1→v2 零改动
v2 新增两点(jiu 侧已实现):

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

// 签名(下单时调用)/ 验签(收 webhook 时对比)
func hmacSign(secret string, parts ...string) string {
    m := hmac.New(sha256.New, []byte(secret))
    m.Write([]byte(strings.Join(parts, "\n")))
    return base64.StdEncoding.EncodeToString(m.Sum(nil))
}
// 验签:hmac.Equal(([]byte)hmacSign(secret, "jiu", ts, nonce, rawBody), got)

4. jiu 已实现的六块

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

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

  1. 校验登录态,拿到当前 shop_id;入参 {"biz_code": "annual_standard"}(前端传套餐稳定码)。
  2. LicensePurchase 记录(status=pending,存 shop_id / user_id / product_biz_code)。
  3. 调 pay v2 下单(见 §5.1):sku=biz_codebiz_ref=purchase_idreturn_url=jiu 结果页。
  4. 解析响应拿到 order_no + session:{render_type,payload},回写 out_trade_noredirect 态额外把 payload.url 存进 pay_url 列(订单管理「继续支付」复用同一收银台链接,避免二次下单)。
  5. best-effort 查单回填:v2 下单响应不回传金额(D1:价格权威永远在 pay),下单成功后立即调一次查单(§5.3)拿 amount_minor/currency/subject 回填;查单失败不阻断下单(金额留 0,等 webhook/查单兜底时再补)。
  6. render_type/payload/amount_minor/currency 连同兼容字段 pay_url(redirect 态=payload.url)、amount(分转元字符串,官网/旧客户端读这两个)一并返给客户端。
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(勿被框架提前解析掉),取 3 个签名头(X-Pay-Timestamp/Nonce/SignX-Pay-Event 不参与验签)。
  2. 验签hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)校验时间戳 ±5 分钟;nonce 防重放(§3)。任一不过 → 401,不处理。
  3. event_type 分发payment.succeeded → 走入账(settle);其余事件(refund.* 等,本期不接,另起任务)→ 记 ALERT 日志后照样回 SUCCESS ack(v2 无退避无死信,回非 SUCCESS 会被 60s 永久重投)。
  4. 入账(settle)幂等:按 out_trade_no 查 Purchase(FOR UPDATE 行锁);若已 paid → 直接短路成功(pay 会重发,必须幂等)。
  5. 金额校验:payload 的 amount_minor+currency 与 Purchase 一致(int64 精确比较,防篡改);若购买单金额仍为 0(下单回填失败的残单),先补查一次(§5.3)再核对,仍拿不到则 ErrPayAmount 拒绝入账(fail-closed,等 pay 重投)。
  6. 续期:按 product_biz_code 映射权益(§6),给该 shop 的 License 直接叠加 ExpiresAt(§7),并把续期后的到期日回写 Purchase.renewed_to(订单管理「授权续期至」展示用)。
  7. 标记 Purchase=paid;回 HTTP 200 + {"code":"SUCCESS"}(pay 认响应体前 4096 字节大小写不敏感含 SUCCESS 才算成功,否则 60s 重投)。
这是「钱已到账」的入口——验签 + nonce 防重放 + 幂等 + 金额核对四者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。

3续期逻辑 jiu

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

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

4查单兜底 jiu

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

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 调用本接口(失败静默,查单兜底兜底)。

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 签名):
{
  "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"
}
// 成功响应(不回传金额——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: X-Pay-Timestamp/Nonce/Sign 3 个签名头(§3),你要验签;X-Pay-Event 头带事件类型提示但不参与签名
{
  "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_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 + 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 → 权益映射

套餐biz_code价格时长tier客户端(MaxDevices)权益(Features)
月付·标准monthly_standard¥299+30 天standard2单店/单仓库 · 千张图片分享
年付·标准annual_standard¥2999+365 天standard2单店/单仓库 · 千张图片分享
月付·高级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 侧按现有能力开关定义,本表给权益语义。上表价格仍是人类可读展示;线上口径以 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 v2 合 main 部署;生产 accounts 配置 alipay 且套餐种子落库;共享密钥就位(pay BIZ_JIU_SECRET = jiu PAY_SECRET,同值,走 Bitwarden);pay 侧配 BIZ_JIU_CALLBACK_URL = jiu 接收器地址;jiu 侧 PAY_BASE_URL 指向 v2 部署。
  2. ¥0.01/¥1 真单走通:下单 → redirect 收银台 → 实付 → webhook 入账续期 → 客户端轮询转 success → 权限即时恢复。
  3. 阻断 webhook(临时改 callback_url)验证查单兜底(reconcile,每 60s 扫 pending>5 分钟单)能补上账。
  4. 重投验证:webhook 接收器临时回 FAIL,观察 pay 60s 重投与 jiu 幂等(不重复续期)。
  5. 官网 checkout(读 pay_url 兼容字段)+ 旧版客户端购买路径回归,确认零改动仍可用。
  6. 取消路径:客户端「重新选择」→ 观察 pay 订单翻 canceled、本地翻 failed
  7. 真机验证手机拉起支付宝 App(等 pay 侧端型透传补契约后再测,见 §8)。

相关:授权体系设计(兑换券模型) · 数据库 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