#微信支付能力规格说明 ## ADDED Requirements ### Requirement: 系统必须支持 JSAPI 支付 系统 MUST 支持在微信内网页发起 JSAPI 支付,用户在微信客户端内完成支付。 #### Scenario: 用户在微信内成功发起支付 - **WHEN** 用户在微信内选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/jsapi` 端点,传入用户 OpenID - **THEN** 系统验证订单状态为 `pending`(待支付) - **THEN** 系统调用 PowerWeChat SDK 的 `Order.JSAPITransaction()` 创建支付订单 - **THEN** 系统生成 JSSDK 支付配置(包含 prepay_id、timestamp、nonceStr、paySign) - **THEN** 系统返回支付配置给前端 - **THEN** 前端调用 `wx.requestPayment()` 唤起微信支付 #### Scenario: 订单不存在或状态不正确 - **WHEN** 用户提交的订单 ID 不存在,或订单状态不是 `pending` - **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"订单不存在或不可支付" #### Scenario: 订单金额为 0 - **WHEN** 订单金额为 0 元 - **THEN** 系统跳过微信支付,直接更新订单状态为 `paid` - **THEN** 系统触发套餐激活和分佣计算 #### Scenario: 微信支付 API 调用失败 - **WHEN** 调用 PowerWeChat SDK 创建支付订单时失败(网络超时、参数错误等) - **THEN** 系统记录详细的错误日志(Request ID、错误码、错误消息) - **THEN** 系统返回错误码 1042(微信支付发起失败)和中文错误消息"支付发起失败,请重试" ### Requirement: 系统必须支持 H5 支付 系统 MUST 支持在移动端浏览器外发起 H5 支付,用户可唤起微信 APP 完成支付。 #### Scenario: 用户在浏览器中成功发起 H5 支付 - **WHEN** 用户在移动端浏览器选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/h5` 端点,传入用户终端 IP 和场景信息 - **THEN** 系统验证订单状态为 `pending` - **THEN** 系统调用 PowerWeChat SDK 的 `Order.TransactionH5()` 创建 H5 支付订单 - **THEN** 系统返回微信支付跳转 URL(h5_url) - **THEN** 前端跳转到该 URL,用户在微信 H5 页面完成支付 #### Scenario: 缺少必填参数 - **WHEN** 请求缺少 `payer_client_ip` 或 `scene_info` 参数 - **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"缺少必填参数" #### Scenario: 订单已支付 - **WHEN** 用户提交的订单状态已是 `paid` - **THEN** 系统返回错误码 1000(参数错误)和中文错误消息"订单已支付" ### Requirement: 系统必须支持微信支付回调 系统 SHALL 接收并处理微信支付成功通知,更新订单状态并触发后续业务逻辑。 #### Scenario: 接收到合法的支付成功通知 - **WHEN** 微信回调 `/api/callback/wechat-pay` 端点,传入支付成功通知 - **THEN** PowerWeChat SDK 自动验证回调签名 - **THEN** 系统解析通知内容,提取商户订单号(out_trade_no) - **THEN** 系统调用 `orderService.HandlePaymentCallback()` 更新订单状态为 `paid`(幂等处理) - **THEN** 系统触发套餐激活和分佣计算 - **THEN** 系统返回 HTTP 200 和 `{"return_code": "SUCCESS"}` 给微信 #### Scenario: 接收到重复的支付通知 - **WHEN** 微信多次发送同一订单的支付成功通知 - **THEN** 系统通过幂等检查识别订单已支付 - **THEN** 系统直接返回成功响应,不重复处理业务逻辑 #### Scenario: 回调签名验证失败 - **WHEN** 微信回调的签名无效或被篡改 - **THEN** PowerWeChat SDK 自动拒绝该请求 - **THEN** 系统记录 ERROR 级别日志(Request ID、签名验证失败详情) - **THEN** 系统返回 HTTP 400 错误 #### Scenario: 订单号不存在 - **WHEN** 微信回调中的商户订单号在系统中不存在 - **THEN** 系统记录 ERROR 级别日志 - **THEN** 系统返回失败响应给微信(让微信稍后重试) #### Scenario: 支付回调处理失败 - **WHEN** 系统在处理支付回调时发生数据库错误或其他异常 - **THEN** 系统记录 ERROR 级别日志(Request ID、错误详情) - **THEN** 系统返回失败响应给微信(让微信稍后重试) ### Requirement: 支付回调处理必须幂等 系统 MUST 确保多次接收到同一支付通知时,业务逻辑只执行一次。 #### Scenario: 订单状态条件更新 - **WHEN** 系统更新订单状态为 `paid` - **THEN** 系统使用条件更新:`UPDATE ... WHERE id = ? AND payment_status = ?`(只更新状态为 pending 的订单) - **THEN** 如果更新影响行数为 0,系统检查当前订单状态: - 如果已支付,返回成功(幂等) - 如果已取消/已退款,返回错误 #### Scenario: 套餐激活幂等性 - **WHEN** 订单支付成功后触发套餐激活 - **THEN** 系统检查 `tb_package_usage` 表是否已存在该订单的激活记录 - **THEN** 如果已存在,跳过激活逻辑(幂等) ### Requirement: 系统必须支持查询微信支付订单 系统 SHALL 支持根据商户订单号查询微信支付订单状态。 #### Scenario: 查询到支付成功的订单 - **WHEN** 调用 `PaymentService.Order.QueryByOutTradeNumber()` 查询订单 - **THEN** 系统返回订单详情,包含: - 订单号(out_trade_no) - 微信支付单号(transaction_id) - 支付状态(trade_state: SUCCESS) - 支付时间(success_time) - 支付金额(total) #### Scenario: 查询到待支付的订单 - **WHEN** 查询的订单尚未支付 - **THEN** 系统返回订单详情,支付状态为 `NOTPAY` #### Scenario: 查询不存在的订单 - **WHEN** 查询的商户订单号在微信侧不存在 - **THEN** PowerWeChat SDK 返回错误 - **THEN** 系统记录日志并返回错误码 1042 ### Requirement: 系统必须支持关闭未支付订单 系统 SHALL 支持关闭超时未支付的微信订单。 #### Scenario: 成功关闭未支付订单 - **WHEN** 调用 `PaymentService.Order.Close()` 关闭订单,传入商户订单号 - **THEN** 系统调用微信 API 关闭订单 - **THEN** 系统返回成功响应 #### Scenario: 尝试关闭已支付订单 - **WHEN** 调用关闭接口,但订单已支付 - **THEN** 微信 API 返回错误(订单已支付,无法关闭) - **THEN** 系统记录日志并返回错误 #### Scenario: 订单创建后 5 分钟内关闭 - **WHEN** 订单创建后不足 5 分钟就调用关闭接口 - **THEN** 系统可能因订单状态同步不及时而关闭失败 - **THEN** 系统建议在创建 5 分钟后再关闭 ### Requirement: 系统必须支持配置管理 微信支付相关配置 MUST 通过 Viper + 环境变量管理。 #### Scenario: 从环境变量读取配置 - **WHEN** 系统启动时 - **THEN** 系统从环境变量读取以下配置: - `JUNHONG_WECHAT_PAYMENT_APP_ID`(支付 AppID) - `JUNHONG_WECHAT_PAYMENT_MCH_ID`(商户号) - `JUNHONG_WECHAT_PAYMENT_API_V3_KEY`(API V3 密钥) - `JUNHONG_WECHAT_PAYMENT_API_V2_KEY`(API V2 密钥) - `JUNHONG_WECHAT_PAYMENT_CERT_PATH`(商户证书路径) - `JUNHONG_WECHAT_PAYMENT_KEY_PATH`(商户私钥路径) - `JUNHONG_WECHAT_PAYMENT_SERIAL_NO`(证书序列号) - `JUNHONG_WECHAT_PAYMENT_NOTIFY_URL`(支付回调地址) #### Scenario: 证书文件不存在时启动失败 - **WHEN** 配置的证书路径指向的文件不存在或无读取权限 - **THEN** 系统记录 FATAL 级别日志 - **THEN** 系统启动失败并退出 #### Scenario: 必填配置缺失时启动失败 - **WHEN** 必填配置项(AppID、商户号、API 密钥)缺失 - **THEN** 系统记录 FATAL 级别日志 - **THEN** 系统启动失败并退出 ### Requirement: API 必须遵循统一响应格式 所有微信支付相关 API MUST 返回统一的 JSON 响应格式(同微信公众号规范)。 #### Scenario: 支付发起成功响应 - **WHEN** JSAPI 支付发起成功 - **THEN** 系统返回 HTTP 200 和以下格式: ```json { "code": 0, "message": "success", "data": { "prepay_id": "wx...", "pay_config": { "appId": "...", "timeStamp": "...", "nonceStr": "...", "package": "prepay_id=...", "signType": "RSA", "paySign": "..." } }, "timestamp": 1706789012345 } ``` #### Scenario: H5 支付发起成功响应 - **WHEN** H5 支付发起成功 - **THEN** 系统返回 HTTP 200 和以下格式: ```json { "code": 0, "message": "success", "data": { "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?..." }, "timestamp": 1706789012345 } ``` ### Requirement: 系统必须记录完整的日志 所有微信支付 API 调用 MUST 记录完整的日志。 #### Scenario: 记录支付发起日志 - **WHEN** 系统调用微信支付 API 创建订单 - **THEN** 系统记录 INFO 级别日志,包含:Request ID、订单号、支付类型(JSAPI/H5)、订单金额 #### Scenario: 记录支付回调日志 - **WHEN** 系统收到微信支付回调 - **THEN** 系统记录 INFO 级别日志,包含:Request ID、订单号、微信支付单号、支付时间 #### Scenario: 记录支付错误日志 - **WHEN** 微信支付 API 调用失败 - **THEN** 系统记录 ERROR 级别日志,包含:Request ID、订单号、错误码、错误消息、完整的错误详情 ### Requirement: 系统必须支持 Redis 缓存 微信支付的 Access Token MUST 使用 Redis 缓存(与微信公众号共享同一缓存机制)。 #### Scenario: Token 缓存与公众号共享 - **WHEN** 微信支付和公众号使用相同的 AppID - **THEN** 系统复用同一个 Redis Cache 实例 - **THEN** Token 缓存 Key 相同,避免重复获取 --- ## MODIFIED Requirements (from: add-payment-config-management) ### Requirement: 微信支付配置动态加载 微信支付配置 MUST 从数据库动态加载(通过 `tb_wechat_config` 表),替代原有的环境变量静态配置。Payment 实例按需创建,支持请求级 AppID 覆盖(区分公众号和小程序)。 #### Scenario: 从数据库加载配置创建 Payment 实例 - **WHEN** 支付流程需要使用微信支付 - **THEN** 系统从 Redis 缓存或数据库加载当前生效的微信参数配置(`is_active=true` 且 `provider_type=wechat`) - **THEN** 系统使用配置中的 `wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no` 创建 `payment.Payment` 实例 - **THEN** 证书内容从 Base64 解码后写入临时文件供 PowerWeChat SDK 使用 > **本次留桩**:WechatPayJSAPI 和 WechatPayH5 方法保留现有 wechatPayment 单例调用,添加 TODO 注释标记后续替换点。 #### Scenario: 无生效微信支付配置时拒绝支付 - **WHEN** 系统查询不到 `is_active=true` 的微信参数配置,或生效配置的 `provider_type` 非 `wechat` - **THEN** 微信支付相关接口返回错误 ```json { "code": 1175, "data": null, "msg": "当前无可用的支付渠道", "timestamp": "2026-03-16T10:00:00+08:00" } ``` #### Scenario: 公众号 JSAPI 支付使用公众号 AppID ``` POST /api/h5/orders/:id/wechat-pay/jsapi Authorization: Bearer {token} Content-Type: application/json ``` **请求体** ```json { "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `openid` | string | ✅ | 用户在公众号下的 OpenID | - **THEN** 系统使用配置中的 `oa_app_id`(公众号 AppID)创建支付订单 - **THEN** Payer OpenID 为用户在该公众号下的 OpenID **成功响应 `200 OK`**(本次留桩,返回结构不变) ```json { "code": 0, "data": { "prepay_id": "wx26112221580621e9b071c00d9e093b0000", "pay_config": { "appId": "wx1234567890abcdef", "timeStamp": "1711411341", "nonceStr": "abc123", "package": "prepay_id=wx26112221580621e9b071c00d9e093b0000", "signType": "RSA", "paySign": "..." } }, "msg": "success", "timestamp": "2026-03-16T10:00:00+08:00" } ``` #### Scenario: 小程序支付使用小程序 AppID - **WHEN** 用户在小程序中发起支付 - **THEN** 系统在调用 `JSAPITransaction` 时将 AppID 覆盖为配置中的 `miniapp_app_id` - **THEN** Payer OpenID 为用户在该小程序下的 OpenID #### Scenario: 微信 H5 支付 ``` POST /api/h5/orders/:id/wechat-pay/h5 Authorization: Bearer {token} Content-Type: application/json ``` **请求体** ```json { "scene_info": { "payer_client_ip": "14.23.150.211", "h5_info": { "type": "Wap" } } } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `scene_info.payer_client_ip` | string | ✅ | 用户终端 IP | | `scene_info.h5_info.type` | string | ❌ | 场景类型:`iOS` / `Android` / `Wap` | **成功响应 `200 OK`** ```json { "code": 0, "data": { "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx..." }, "msg": "success", "timestamp": "2026-03-16T10:00:00+08:00" } ``` #### Scenario: 配置缺失时系统正常启动 - **WHEN** 系统启动时数据库中无微信参数配置或配置不完整 - **THEN** 系统正常启动,支付功能降级为仅支持钱包/线下 - **THEN** 系统记录 WARN 日志"无可用微信参数配置,第三方支付功能不可用" --- ### Requirement: 微信支付回调按配置验签 系统 SHALL 接收并处理微信支付成功通知。回调验签 MUST 使用订单关联的支付配置(而非当前生效配置)。 #### Scenario: 接收到合法的支付成功通知 ``` POST /api/callback/wechat-pay Content-Type: 由微信服务器决定 无需认证 ``` - **WHEN** 微信回调端点收到支付成功通知 - **THEN** 系统解析通知中的商户订单号(`out_trade_no`) - **THEN** 按订单号前缀分发(`ORD` → 套餐订单,`CRCH` → 资产充值,`ARCH` → 代理充值) - **THEN** 查询对应表记录,通过 `payment_config_id` 加载对应的微信参数配置 - **THEN** 使用该配置的凭证通过 PowerWeChat SDK 验证回调签名 - **THEN** 调用对应 Service 的 HandlePaymentCallback - **THEN** 返回成功响应 **成功响应** ```json { "code": 0, "data": { "return_code": "SUCCESS" }, "msg": "success", "timestamp": "2026-03-16T10:00:00+08:00" } ``` #### Scenario: 订单关联的配置已被软删除 - **WHEN** 回调到达,但 `payment_config_id` 对应的配置已被软删除 - **THEN** 系统使用 `GetByIDUnscoped` 加载该配置(软删除不影响回调处理) - **THEN** 正常完成验签和订单处理 #### Scenario: 重复回调幂等处理 - **WHEN** 微信多次发送同一订单的支付成功通知 - **THEN** 系统通过幂等检查识别已支付,直接返回成功响应 #### Scenario: 回调签名验证失败 - **WHEN** 签名无效或被篡改 - **THEN** PowerWeChat SDK 自动拒绝,系统记录 ERROR 日志,返回 HTTP 400 #### Scenario: 订单号不存在 - **WHEN** 回调中的商户订单号在系统中不存在 - **THEN** 系统记录 ERROR 日志,返回失败响应