Files
sundynix-agentix/PAYMENT_DESIGN.md
T
Blizzard 039e7a2f06 docs(payment): 渠道拍板微信支付 Native + 两层汇率解耦写明
- P5.2 = wechatpay-go(Native 下单 code_url→二维码,APIv3 验签解密);商户号/证书
  env 注入,未配置渠道自动隐藏只剩兑换码,半配置状态不许把下单路由搞出 5xx。
- 用户强调积分↔token 要可动态调:第二层 tokens_per_credit 是 P2 期现成的
  (admin 计费页,DB 热生效);本期只新增第一层(积分包定价,admin 配包)。
  订单锁定下单当时的包价,改包不影响已付订单。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:55:02 +08:00

7.6 KiB
Raw Blame History

支付设计(SaaS P5)—— 充值积分包,不做订阅

2026-07-17。前置已就绪:P2 计量(usage_event/credit_ledger/物化余额/rollup)、 P4 积分环(GrantCredits + 余额硬拦截)、薄 Web 面(账单页已有充值占位)。 本文一页纸定路线,增量照此施工;渠道选择(§3)留用户拍板。

1. 结论先行

  • 做「预付积分包」充值,不做订阅。理由:积分体系是现成的——支付入账只是给 credit_ledger 多一个自动化的 grant 来源,改动集中在「订单 + 回调」;订阅制 (周期扣费/升降级/按比例退差)是另一个量级的状态机,等有付费用户再议。 Tenant.Planfree/pro/enterprise)字段留着,将来订阅直接长在上面。
  • 渠道做成适配器接口,先上一个「零资质渠道」跑通闭环(兑换码/人工转账核销), 真渠道(支付宝/微信/Stripe)接进来只是多一个 adapter,不动骨架。
  • 金额一律服务端说了算:前端只传「包 ID」,价格/积分数由服务端订单锁定, 回调按订单校验金额,不信任任何客户端传值。

2. 与现有账本的对接点(都是现成的)

现有件 位置 支付怎么用
credit_ledgerappend-onlykind: grant/usage/adjustRef 字段) store/credit.go 支付入账 = kind=grant, ref=order_id
GrantCredits(ctx, tenant, kind, micro, ref, memo)(事务内:分录+物化余额) store/credit.go:151 回调确认后调用;内部已 WithoutTenant(跨租户写)
余额硬拦截 credit_enforce store/setting.go 不动;充值到账即自然解封
薄 Web 面「用量与账单」页 sundynix-web/src/pages/Usage.tsx 占位文案换成真充值入口
admin 计费页 + /admin/usage 观测侧 加支付订单流观测(P5.3

⚠️ 必须先补的闸:GrantCreditsref 无唯一约束。 admin 手工充值无所谓; 支付回调会重复推送(渠道明文保证 at-least-once),不闸就是重复入账事故。 双保险:① 订单状态机 CAS 是主闸(见 §5);② credit_ledger(kind, ref) 部分唯一索引(WHERE kind='grant' AND ref<>'')兜底,两道闸缺一不可。

3. 渠道决策(用户拍板,技术侧全兼容)

已拍板(2026-07-17):真渠道用微信支付 Native(P5.2)。 需企业主体 + 微信商户号 mchid + APIv3 密钥 + 商户证书);形态 = 下单得 code_url → Web 面渲染二维码 → 用户扫码付 → 回调(APIv3 签名验签 + AES-GCM 解密)。SDK 用官方 github.com/wechatpay-apiv3/wechatpay-go。其余渠道留 adapter 位,不做。

渠道 前提 形态 状态
兑换码/人工核销P5.1 内置) admin 生成码 → 用户在 Web 面输码入账 先做,零资质跑通闭环
微信支付 NativeP5.2 企业主体 + 商户号 二维码(code_url 已拍板
支付宝 / Stripe 不做,留 adapter 位

Adapter 接口(internal/payment/channel.go):

type Channel interface {
    Name() string
    // CreatePay 依据订单生成支付凭据(二维码内容/跳转 URL/兑换码提示)
    CreatePay(ctx, o *PaymentOrder) (PayIntent, error)
    // VerifyCallback 验签并解析回调 → (orderID, channelTxnID, paidAmountFen, error)
    VerifyCallback(req *http.Request) (CallbackResult, error)
    // QueryOrder 主动查单(掉单补偿用)
    QueryOrder(ctx, orderID string) (OrderStatus, error)
}

3b. 两层汇率,各管各的(用户 2026-07-17 强调:积分↔token 必须可动态调)

人民币 ──(第一层: 积分包定价 price_fen→credits_micro, admin 配包/上下架)──> 积分
积分  ──(第二层: tokens_per_credit, admin 计费页动态调, DB 热生效 ✅已存在)──> token
  • 第二层是现成的SettingTokensPerCreditDB 优先→env→1000),admin「计费 & 用量」 页可改,对后续任务实时生效(P2 期 live 验证过:改 500 → 新任务 credits=tok/500)。 支付不碰它。
  • 第一层是本期新增sundynix_credit_pack 表,admin 可改价/加量/上下架。
  • 解耦收益:促销只动包;模型成本变了要调积分购买力只动汇率;互不牵连。 订单锁定的是下单当时的包价与积分数(写死在订单行),之后改包不影响已付订单。

4. 数据模型(新增两件)

sundynix_payment_order            # 订单:支付侧事实源(与 credit_ledger 对账的另一条腿)
  id            雪花 (= 对外 order_id, ledger.ref)
  tenant_id     计费租户(下单时用 ResolveBillingTenantID 解析并锁定)
  user_id       操作人(审计)
  pack_id       积分包 ID
  amount_fen    应付金额(分)      # 服务端按包锁定
  credits_micro 到账积分(micro)   # 服务端按包锁定
  channel       redeem / alipay / wechat / stripe
  status        pending → paid | failed | expired ; paid → refunded(人工)
  channel_txn   渠道流水号(回调带回)
  paid_at
sundynix_credit_pack              # 积分包配置(admin 可改,别硬编码)
  id / name / credits_micro / price_fen / active / sort

5. 流程与幂等(核心就这一张图)

Web面账单页 → GET /billing/packs → 选包 → POST /billing/orders {pack_id}
  → 服务端建 pending 订单(锁价) → 返回 PayIntent(二维码/跳转/输码框)
用户支付 → 渠道回调 POST /billing/callback/:channel(公开路由,验签是唯一门)
  → VerifyCallback 验签 + 金额与订单核对
  → 一个事务内:UPDATE payment_order SET status='paid' WHERE id=? AND status='pending'
      RowsAffected==0 → 已处理过,直接 200(幂等闸①)
      ==1 → GrantCredits(kind=grant, ref=order_id)(唯一索引兜底,幂等闸②)
  → 前端轮询 GET /billing/orders/:id 到 paid → 刷余额
掉单补偿:pending 超 15min 的订单定时 QueryOrder 补态;过期置 expired
  • 回调路由是公开的(渠道服务器打不了 Bearer),安全完全靠验签 + 金额核对 + 订单状态机;这与 /reports/:id/export 公开的先例同构,但多了签名门。
  • 退款先只做人工:admin 发起 → adjust 负分录 + 订单置 refunded;自动退款不做。
  • 对账三条腿:渠道账单 ↔ payment_order(paid)ledger(kind=grant), 日终脚本比对(P5.3,先出 admin 页面人肉看,再自动化)。

6. 分增量(每步「机制→单测→live」)

  • P5.1 订单骨架 + 兑换码渠道(不依赖任何外部资质,全链路即刻可 live 验证): 两张表 + ledger 唯一索引 + Channel 接口 + redeem adapter + /billing/* 路由 (下单≥billing_admin? 不——充值谁都该能充,挂 ≥memberviewer 只读仍拦)+ Web 面账单页真充值 UI + admin 生成兑换码。
  • P5.2 微信支付 Native(已拍板)wechatpay-go adapterNative 下单 code_url → Web 面二维码)+ APIv3 回调验签解密 + 轮询查单。商户号/证书经 env 注入, 未配置时渠道自动隐藏(只剩兑换码),别让半配置状态把下单路由搞出 5xx。
  • P5.3 对账与观测:admin「支付订单」流 + 日终对账 + 掉单补偿定时器。
  • P5.4 按需:退款流程化、发票、微信/Stripe 并列。

7. 明确不做(本期)

订阅/自动续费、套餐权益(plan 仍是展示字段)、多币种、自动退款、发票自动化、 渠道分账。等真实付费流量拉动。