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