9.8 KiB
9.8 KiB
支付功能接入文档
本文用于指导后续接入微信、支付宝或其他聚合支付渠道。当前项目尚未实现支付功能,开发时应先把订单、支付单、回调日志和履约流程的边界预留好,避免后续重构订单主链路。
接入目标
- 订单创建后先进入待支付状态。
- 用户或管理员发起支付,后端创建支付单。
- 支付渠道异步回调后,后端验签并更新支付状态。
- 支付成功后触发订单履约:
- 第三方商品:服务端调用第三方下单接口。
- 自营商品:只写入本地数据库,生成本地交付记录。
- 支持后续退款、支付状态主动查询、回调补偿和支付审计。
推荐订单状态
订单主状态建议拆成支付状态和履约状态,不要只用一个 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_party 或 self_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 |
支付渠道,如 wechat、alipay、manual |
amount |
支付金额 |
currency |
默认 CNY |
status |
created、paying、paid、failed、closed、refunded |
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 |
notify、query、refund_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 |
created、processing、succeeded、failed |
channel_refund_no |
渠道退款单号 |
refunded_at |
退款完成时间 |
后端接口设计
创建订单
POST /api/orders
职责:
- 校验商品、价格、库存、上下架状态。
- 创建
orders和order_items。 - 设置
payment_status=unpaid。 - 设置
fulfillment_status=pending。 - 返回订单号和应付金额。
注意:
- 第三方商品在订单创建阶段不要把
uid、key返回给前端。 - 学生账号和密码等敏感信息必须由服务端保存,并在展示时脱敏。
创建支付单
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 和响应摘要。
- 将履约状态改为
submitted或processing。 - 失败时改为
failed,保留失败原因,后台可手动重试。
自营商品:
- 将订单履约状态改为
submitting。 - 生成本地交付记录或待处理任务。
- 自动交付商品可直接改为
completed。 - 人工处理商品改为
processing。
幂等与事务
必须使用幂等键:
- 本地订单号
order_no唯一。 - 本地支付单号
payment_no唯一。 - 渠道交易号
channel_trade_no唯一。 - 支付事件
event_no或原始回调哈希唯一。
建议处理顺序:
- 回调进入后先验签。
- 写入
payment_events,重复事件直接返回成功。 - 开启数据库事务。
- 锁定支付单和订单。
- 校验金额和状态。
- 更新支付单和订单。
- 创建履约任务或直接履约。
- 提交事务。
第三方下单如果耗时较长,建议支付事务内只创建 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。
前端页面建议
- 订单确认页:展示商品、来源、价格、账号信息摘要、应付金额。
- 支付页:展示支付方式、二维码/跳转按钮、倒计时、支付状态轮询。
- 订单详情页:展示支付状态、履约状态、第三方提交状态或自营交付状态。
- 后台支付管理:支付单列表、回调日志、主动查询、异常标记。
- 后台退款管理:退款申请、退款结果、退款回调日志。
测试清单
- 创建订单后状态为
unpaid和pending。 - 创建支付单后状态为
paying。 - 正常回调可把支付单和订单标记为已支付。
- 重复回调不会重复履约。
- 金额不一致不会标记成功。
- 验签失败不会更新订单。
- 第三方商品支付成功后只提交一次第三方订单。
- 自营商品支付成功后不调用第三方接口。
- 支付超时后订单可关闭。
- 退款成功后订单状态正确更新。
与第三方商品的关系
支付渠道和第三方课程接口是两套外部系统,不要混在一个服务里:
- 支付服务只负责收钱、验签、退款和支付事件。
- 第三方课程服务只负责
getcate、getclass、get、add、orders等接口。 - 订单服务负责协调支付成功后的履约分流。
这样后续新增支付渠道或新增自营商品时,不需要改动第三方接口调用核心逻辑。