Files
study_work/docs/payment-integration.md

9.8 KiB
Raw Permalink Blame History

支付功能接入文档

本文用于指导后续接入微信、支付宝或其他聚合支付渠道。当前项目尚未实现支付功能,开发时应先把订单、支付单、回调日志和履约流程的边界预留好,避免后续重构订单主链路。

接入目标

  • 订单创建后先进入待支付状态。
  • 用户或管理员发起支付,后端创建支付单。
  • 支付渠道异步回调后,后端验签并更新支付状态。
  • 支付成功后触发订单履约:
    • 第三方商品:服务端调用第三方下单接口。
    • 自营商品:只写入本地数据库,生成本地交付记录。
  • 支持后续退款、支付状态主动查询、回调补偿和支付审计。

推荐订单状态

订单主状态建议拆成支付状态和履约状态,不要只用一个 status 混合表达全部含义。

支付状态 payment_status

状态 说明
unpaid 未支付,订单刚创建
paying 已创建支付单,等待渠道结果
paid 已支付成功
pay_failed 支付失败
closed 支付超时或订单关闭
refunding 退款中
refunded 已退款
partial_refunded 部分退款

履约状态 fulfillment_status

状态 说明
pending 等待支付或等待履约
submitting 正在提交第三方或生成本地交付
submitted 已提交第三方或已生成交付
processing 第三方或本地处理中
completed 已完成
failed 履约失败
canceled 已取消

推荐数据表

orders

订单主表,保存业务订单。

建议字段:

字段 说明
id 本地订单 ID
order_no 本地订单号,展示和支付使用
user_id 下单用户
source_type third_partyself_owned
provider 第三方供应商标识,自营可为空
total_amount 订单金额
payable_amount 应付金额
paid_amount 实付金额
payment_status 支付状态
fulfillment_status 履约状态
remark 备注
created_at 创建时间
updated_at 更新时间

order_items

订单明细表,保存商品快照,避免商品改价影响历史订单。

建议字段:

字段 说明
id 明细 ID
order_id 订单 ID
product_id 商品 ID
product_name 商品名称快照
source_type 商品来源
external_product_id 第三方商品 ID,自营为空
unit_price 单价
quantity 数量
total_amount 明细金额
payload 下单扩展信息,敏感字段入库前加密或脱敏

payments

支付单表。一个订单可以有多次支付尝试,但同一时间只允许一个有效待支付支付单。

建议字段:

字段 说明
id 支付单 ID
payment_no 本地支付单号
order_id 订单 ID
order_no 本地订单号冗余
channel 支付渠道,如 wechatalipaymanual
amount 支付金额
currency 默认 CNY
status createdpayingpaidfailedclosedrefunded
channel_trade_no 渠道交易号
channel_payload 渠道创建支付返回,敏感字段脱敏
paid_at 支付成功时间
expired_at 支付过期时间
created_at 创建时间
updated_at 更新时间

payment_events

支付事件表,保存支付回调、主动查询、退款回调等原始事件。

建议字段:

字段 说明
id 事件 ID
payment_id 支付单 ID
order_id 订单 ID
channel 支付渠道
event_type notifyqueryrefund_notify
event_no 渠道事件号或渠道交易号,用于幂等
status 处理状态
raw_payload 原始回调内容,注意脱敏或加密
verify_result 验签结果
error_message 处理失败原因
created_at 创建时间

refunds

退款单表,后续退款功能使用。

建议字段:

字段 说明
id 退款单 ID
refund_no 本地退款单号
payment_id 支付单 ID
order_id 订单 ID
amount 退款金额
reason 退款原因
status createdprocessingsucceededfailed
channel_refund_no 渠道退款单号
refunded_at 退款完成时间

后端接口设计

创建订单

POST /api/orders

职责:

  • 校验商品、价格、库存、上下架状态。
  • 创建 ordersorder_items
  • 设置 payment_status=unpaid
  • 设置 fulfillment_status=pending
  • 返回订单号和应付金额。

注意:

  • 第三方商品在订单创建阶段不要把 uidkey 返回给前端。
  • 学生账号和密码等敏感信息必须由服务端保存,并在展示时脱敏。

创建支付单

POST /api/payments

请求示例:

{
  "orderNo": "ORD202606060001",
  "channel": "wechat",
  "returnUrl": "https://example.com/orders/ORD202606060001"
}

职责:

  • 校验订单存在且未支付。
  • 校验订单金额和当前商品金额是否允许支付。
  • 创建 payments
  • 调用支付渠道统一下单接口。
  • 返回前端支付参数,例如二维码、跳转链接或小程序支付参数。

支付回调

POST /api/payments/notify/:channel

职责:

  • 获取原始请求体。
  • 按渠道验签。
  • 写入 payment_events
  • 校验本地支付单号、金额、币种、渠道交易号。
  • 幂等更新 payments.status=paid
  • 幂等更新 orders.payment_status=paid
  • 触发履约流程。
  • 返回渠道要求的成功响应。

必须保证:

  • 重复回调不会重复提交第三方订单。
  • 金额不一致时不能标记为已支付。
  • 验签失败只记录事件,不更新支付成功。

主动查询支付状态

GET /api/payments/:paymentNo/query

职责:

  • 调用渠道查询接口。
  • 写入 payment_events
  • 如果渠道显示已支付,走同一套支付成功处理逻辑。

申请退款

POST /api/refunds

职责:

  • 校验订单已支付且允许退款。
  • 创建 refunds
  • 调用渠道退款接口。
  • 更新订单为 refunding

退款回调

POST /api/payments/refund-notify/:channel

职责:

  • 验签。
  • 写入 payment_events
  • 幂等更新退款单和订单支付状态。

支付成功后的履约流程

支付成功处理函数建议抽成后端服务方法,例如:

async function handlePaymentSucceeded(paymentNo: string) {
  // 1. 开启事务
  // 2. 锁定 payment 和 order
  // 3. 如果 payment/order 已处理过,直接返回
  // 4. 标记 payment=paid、order.payment_status=paid
  // 5. 根据 order.source_type 触发履约
  // 6. 提交事务
}

第三方商品:

  • 将订单履约状态改为 submitting
  • 调用第三方 act=add
  • 成功后保存第三方订单 ID 和响应摘要。
  • 将履约状态改为 submittedprocessing
  • 失败时改为 failed,保留失败原因,后台可手动重试。

自营商品:

  • 将订单履约状态改为 submitting
  • 生成本地交付记录或待处理任务。
  • 自动交付商品可直接改为 completed
  • 人工处理商品改为 processing

幂等与事务

必须使用幂等键:

  • 本地订单号 order_no 唯一。
  • 本地支付单号 payment_no 唯一。
  • 渠道交易号 channel_trade_no 唯一。
  • 支付事件 event_no 或原始回调哈希唯一。

建议处理顺序:

  1. 回调进入后先验签。
  2. 写入 payment_events,重复事件直接返回成功。
  3. 开启数据库事务。
  4. 锁定支付单和订单。
  5. 校验金额和状态。
  6. 更新支付单和订单。
  7. 创建履约任务或直接履约。
  8. 提交事务。

第三方下单如果耗时较长,建议支付事务内只创建 order_fulfillments 任务,事务外由 BullMQ 异步提交第三方,避免支付回调超时。

环境变量建议

PAYMENT_DEFAULT_CHANNEL=wechat

WECHAT_PAY_MCH_ID=
WECHAT_PAY_APP_ID=
WECHAT_PAY_API_V3_KEY=
WECHAT_PAY_PRIVATE_KEY=
WECHAT_PAY_CERT_SERIAL_NO=
WECHAT_PAY_NOTIFY_URL=

ALIPAY_APP_ID=
ALIPAY_PRIVATE_KEY=
ALIPAY_PUBLIC_KEY=
ALIPAY_NOTIFY_URL=
ALIPAY_RETURN_URL=

生产环境要求:

  • 支付私钥和商户密钥不得提交仓库。
  • .env 不提交。
  • 私钥建议使用部署平台 Secret 或 KMS。
  • 回调地址必须是 HTTPS。

前端页面建议

  • 订单确认页:展示商品、来源、价格、账号信息摘要、应付金额。
  • 支付页:展示支付方式、二维码/跳转按钮、倒计时、支付状态轮询。
  • 订单详情页:展示支付状态、履约状态、第三方提交状态或自营交付状态。
  • 后台支付管理:支付单列表、回调日志、主动查询、异常标记。
  • 后台退款管理:退款申请、退款结果、退款回调日志。

测试清单

  • 创建订单后状态为 unpaidpending
  • 创建支付单后状态为 paying
  • 正常回调可把支付单和订单标记为已支付。
  • 重复回调不会重复履约。
  • 金额不一致不会标记成功。
  • 验签失败不会更新订单。
  • 第三方商品支付成功后只提交一次第三方订单。
  • 自营商品支付成功后不调用第三方接口。
  • 支付超时后订单可关闭。
  • 退款成功后订单状态正确更新。

与第三方商品的关系

支付渠道和第三方课程接口是两套外部系统,不要混在一个服务里:

  • 支付服务只负责收钱、验签、退款和支付事件。
  • 第三方课程服务只负责 getcategetclassgetaddorders 等接口。
  • 订单服务负责协调支付成功后的履约分流。

这样后续新增支付渠道或新增自营商品时,不需要改动第三方接口调用核心逻辑。