349 lines
9.8 KiB
Markdown
349 lines
9.8 KiB
Markdown
# 支付功能接入文档
|
||
|
||
本文用于指导后续接入微信、支付宝或其他聚合支付渠道。当前项目尚未实现支付功能,开发时应先把订单、支付单、回调日志和履约流程的边界预留好,避免后续重构订单主链路。
|
||
|
||
## 接入目标
|
||
|
||
- 订单创建后先进入待支付状态。
|
||
- 用户或管理员发起支付,后端创建支付单。
|
||
- 支付渠道异步回调后,后端验签并更新支付状态。
|
||
- 支付成功后触发订单履约:
|
||
- 第三方商品:服务端调用第三方下单接口。
|
||
- 自营商品:只写入本地数据库,生成本地交付记录。
|
||
- 支持后续退款、支付状态主动查询、回调补偿和支付审计。
|
||
|
||
## 推荐订单状态
|
||
|
||
订单主状态建议拆成支付状态和履约状态,不要只用一个 `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` 等接口。
|
||
- 订单服务负责协调支付成功后的履约分流。
|
||
|
||
这样后续新增支付渠道或新增自营商品时,不需要改动第三方接口调用核心逻辑。
|