# 支付功能接入文档

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

## 接入目标

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

## 推荐订单状态

订单主状态建议拆成支付状态和履约状态，不要只用一个 `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` 等接口。
- 订单服务负责协调支付成功后的履约分流。

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