Files
study_work/docs/payment-integration.md

349 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 支付功能接入文档
本文用于指导后续接入微信、支付宝或其他聚合支付渠道。当前项目尚未实现支付功能,开发时应先把订单、支付单、回调日志和履约流程的边界预留好,避免后续重构订单主链路。
## 接入目标
- 订单创建后先进入待支付状态。
- 用户或管理员发起支付,后端创建支付单。
- 支付渠道异步回调后,后端验签并更新支付状态。
- 支付成功后触发订单履约:
- 第三方商品:服务端调用第三方下单接口。
- 自营商品:只写入本地数据库,生成本地交付记录。
- 支持后续退款、支付状态主动查询、回调补偿和支付审计。
## 推荐订单状态
订单主状态建议拆成支付状态和履约状态,不要只用一个 `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`
请求示例:
```json
{
"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`
- 幂等更新退款单和订单支付状态。
## 支付成功后的履约流程
支付成功处理函数建议抽成后端服务方法,例如:
```ts
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` 或原始回调哈希唯一。
建议处理顺序:
1. 回调进入后先验签。
2. 写入 `payment_events`,重复事件直接返回成功。
3. 开启数据库事务。
4. 锁定支付单和订单。
5. 校验金额和状态。
6. 更新支付单和订单。
7. 创建履约任务或直接履约。
8. 提交事务。
第三方下单如果耗时较长,建议支付事务内只创建 `order_fulfillments` 任务,事务外由 BullMQ 异步提交第三方,避免支付回调超时。
## 环境变量建议
```env
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` 等接口。
- 订单服务负责协调支付成功后的履约分流。
这样后续新增支付渠道或新增自营商品时,不需要改动第三方接口调用核心逻辑。