# 支付中心 (RuoYi-Pay) ## 模块简介 `ruoyi-pay` 是 PoJie 项目的**聚合支付中台模块**,提供多支付渠道的统一接入、订单管理、退款管理、回调通知等功能。 ### 定位 `ruoyi-pay` 是一个独立于业务系统的**聚合支付中台**。它向下屏蔽微信、支付宝、银联等渠道的协议差异,向上为电商、会员、充值等业务提供统一的支付、退款、查单接口。 ### 核心能力 - ✅ **多商户/多应用**: 支持多个业务线(如商城App、门店小程序)配置独立的支付参数 - ✅ **多渠道支持**: 微信支付 (Native/JSAPI/App/H5/小程序)、支付宝 (PC/Wap/App/QR) - ✅ **统一下单**: 一套接口适配所有渠道,支持收银台模式 - ✅ **可靠回调**: 包含验签、防重、重试机制,确保业务方能收到通知 - ✅ **防掉单机制**: 自动轮询"支付中"订单,主动同步三方状态 - ✅ **退款管理**: 支持退款申请、退款查询、退款回调处理 - ⏳ **钱包系统**: 支持用户余额充值、提现、消费(待实现) ## 功能特性 ### 1. 支付应用管理 ✅ - ✅ 支付应用CRUD操作 - ✅ 应用状态管理(开启/关闭) - ✅ 回调地址配置(支付回调和退款回调) ### 2. 支付渠道管理 ✅ - ✅ 支付渠道CRUD操作 - ✅ 渠道配置管理(JSON格式,支持动态配置) - ✅ 渠道状态管理 - ✅ 渠道费率配置 - ✅ 支持支付宝、微信支付等多种渠道 ### 3. 支付订单管理 ✅ - ✅ 支付订单创建 - ✅ 订单状态查询 - ✅ 订单同步(防掉单) - ✅ 订单过期处理 - ✅ 订单关闭 - ✅ 商户订单号唯一性校验 ### 4. 退款管理 ✅ - ✅ 退款申请 - ✅ 退款状态查询 - ✅ 退款回调处理 - ✅ 部分退款支持 - ✅ 退款金额校验 ### 5. 回调通知 ✅ - ✅ 支付回调处理 - ✅ 退款回调处理 - ✅ 异步通知重试机制 - ✅ 指数退避重试策略(15s → 30s → 1m → 2m → 5m) - ✅ 回调验签和幂等性处理 ### 6. 定时任务 ✅ - ✅ 支付订单过期处理(每分钟) - ✅ 支付订单状态同步(每2分钟,防掉单) - ✅ 支付回调通知重试(每30秒) ## 支持的支付渠道 ### 支付宝 | 渠道编码 | 名称 | 说明 | 状态 | |---------|------|------|------| | `alipay_pc` | 支付宝 PC 支付 | 电脑网站支付 | ✅ | | `alipay_wap` | 支付宝 Wap 支付 | 手机网站支付 | ✅ | | `alipay_app` | 支付宝 App 支付 | 手机 App 支付 | ✅ | | `alipay_qr` | 支付宝扫码支付 | 当面付 | ✅ | ### 微信支付 | 渠道编码 | 名称 | 说明 | 状态 | |---------|------|------|------| | `wx_pub` | 微信 JSAPI 支付 | 公众号、小程序支付 | ✅ | | `wx_lite` | 微信小程序支付 | 独立小程序支付 | ✅ | | `wx_app` | 微信 App 支付 | 手机 App 支付 | ✅ | | `wx_native` | 微信 Native 支付 | 扫码支付 | ✅ | | `wx_h5` | 微信 H5 支付 | 手机浏览器支付 | ✅ | ### 其他渠道 | 渠道编码 | 名称 | 说明 | 状态 | |---------|------|------|------| | `mock` | 模拟支付 | 开发测试用 | ⏳ 待实现 | | `wallet` | 钱包支付 | 余额支付 | ⏳ 待实现 | ## 核心架构设计 ### 实体关系图 (ERD) ``` ┌──────────┐ │ PayApp │ (支付应用) └────┬─────┘ │ 1 │ │ N ┌────▼──────────┐ │ PayChannel │ (支付渠道) └────┬──────────┘ │ │ N ┌────▼──────────┐ │ PayOrder │ (支付订单) └────┬──────────┘ │ 1 │ │ N ┌────▼──────────┐ ┌──────────────┐ │ PayRefund │ │ PayNotifyTask│ (回调任务) └───────────────┘ └──────────────┘ ``` ### 核心流程 1. **商户入驻**: 在管理后台创建 `应用(App)`,并在该应用下配置 `渠道(Channel)` 参数(如微信AppID、支付宝公钥) 2. **业务下单**: 业务方调用 `pay/order/submit`,传入 `appId` 和 `channelCode` 3. **支付路由**: 支付中心根据 `appId + channelCode` 读取动态配置,实例化对应的支付客户端策略 ### 设计模式 #### 策略模式 (Strategy Pattern) 使用策略模式实现不同支付渠道的统一接口: - `PayStrategy` - 支付策略接口,定义统一的操作方法 - `AlipayStrategy` - 支付宝策略实现 - `WechatPayStrategy` - 微信支付策略实现 - `PayStrategyFactory` - 策略工厂,负责策略的注册和获取 #### 事件驱动 (Event-Driven) - `PaySuccessEvent` - 支付成功事件 - `PayOrderListener` - 监听支付成功事件,创建回调任务 ## 数据库设计 ### 1. 支付应用表 (`pay_app`) 用于隔离不同业务线的支付配置。 | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 应用ID(主键) | | name | VARCHAR(64) | 应用名 | | status | TINYINT | 状态(0开启 1关闭) | | remark | VARCHAR(255) | 备注 | | pay_notify_url | VARCHAR(1024) | 支付结果回调地址 | | refund_notify_url | VARCHAR(1024) | 退款结果回调地址 | | create_by | VARCHAR(64) | 创建者 | | create_time | DATETIME | 创建时间 | | update_by | VARCHAR(64) | 更新者 | | update_time | DATETIME | 更新时间 | **索引**: `PRIMARY KEY (id)` ### 2. 支付渠道表 (`pay_channel`) 存储具体的支付参数(JSON格式),实现无需重启即可修改密钥。 | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 渠道ID(主键) | | code | VARCHAR(32) | 渠道编码(alipay_pc, wx_pub) | | status | TINYINT | 状态(0开启 1关闭) | | remark | VARCHAR(255) | 备注 | | fee_rate | DOUBLE | 渠道费率(%) | | app_id | BIGINT | 应用ID | | config | TEXT | 支付渠道配置(JSON) | | create_by | VARCHAR(64) | 创建者 | | create_time | DATETIME | 创建时间 | | update_by | VARCHAR(64) | 更新者 | | update_time | DATETIME | 更新时间 | **索引**: `PRIMARY KEY (id)` ### 3. 支付订单表 (`pay_order`) | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 订单ID(主键) | | app_id | BIGINT | 应用ID | | channel_id | BIGINT | 渠道ID | | channel_code | VARCHAR(32) | 渠道编码 | | merchant_order_id | VARCHAR(64) | 商户订单编号 | | subject | VARCHAR(32) | 商品标题 | | body | VARCHAR(128) | 商品描述 | | notify_url | VARCHAR(1024) | 异步通知地址 | | price | INT | 支付金额(分) | | channel_fee_rate | DOUBLE | 渠道费率 | | channel_fee_price | INT | 渠道手续费(分) | | status | TINYINT | 状态(0未支付 10已支付 20已退款 30已关闭) | | user_ip | VARCHAR(50) | 用户IP | | expire_time | DATETIME | 订单失效时间 | | success_time | DATETIME | 支付成功时间 | | extension_id | BIGINT | 拓展业务ID | | no | VARCHAR(32) | 支付单号(内部生成) | | channel_order_no | VARCHAR(64) | 渠道订单号(微信/支付宝) | | channel_user_id | VARCHAR(64) | 渠道用户OpenId | | channel_extras | VARCHAR(1024) | 渠道额外参数(JSON) | | create_by | VARCHAR(64) | 创建者 | | create_time | DATETIME | 创建时间 | | update_by | VARCHAR(64) | 更新者 | | update_time | DATETIME | 更新时间 | **索引**: - `PRIMARY KEY (id)` - `KEY idx_merchant_order_id (merchant_order_id)` ### 4. 退款订单表 (`pay_refund`) | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 退款ID(主键) | | no | VARCHAR(32) | 退款单号 | | app_id | BIGINT | 应用ID | | channel_id | BIGINT | 渠道ID | | channel_code | VARCHAR(32) | 渠道编码 | | order_id | BIGINT | 支付订单ID | | order_no | VARCHAR(32) | 支付订单no | | merchant_order_id | VARCHAR(64) | 商户订单编号 | | merchant_refund_id | VARCHAR(64) | 商户退款编号 | | notify_url | VARCHAR(1024) | 回调地址 | | status | TINYINT | 状态(0等待 10成功 20失败) | | pay_price | INT | 支付金额(分) | | refund_price | INT | 退款金额(分) | | reason | VARCHAR(256) | 退款原因 | | user_ip | VARCHAR(50) | 用户IP | | channel_order_no | VARCHAR(64) | 渠道订单号 | | channel_refund_no | VARCHAR(64) | 渠道退款单号 | | success_time | DATETIME | 退款成功时间 | | error_code | VARCHAR(32) | 渠道错误码 | | error_msg | VARCHAR(128) | 渠道错误描述 | | create_by | VARCHAR(64) | 创建者 | | create_time | DATETIME | 创建时间 | | update_by | VARCHAR(64) | 更新者 | | update_time | DATETIME | 更新时间 | **索引**: `PRIMARY KEY (id)` ### 5. 支付通知任务表 (`pay_notify_task`) 用于记录支付/退款回调通知任务,支持重试机制。 | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 任务ID(主键) | | order_id | BIGINT | 订单ID | | order_no | VARCHAR(32) | 订单号 | | notify_url | VARCHAR(1024) | 通知地址 | | notify_body | TEXT | 通知内容(JSON) | | retry_count | INT | 重试次数(默认0) | | max_retry | INT | 最大重试次数(默认5) | | status | TINYINT | 状态(0进行中 1成功 2失败) | | next_notify_time | DATETIME | 下次通知时间 | | create_by | VARCHAR(64) | 创建者 | | create_time | DATETIME | 创建时间 | | update_by | VARCHAR(64) | 更新者 | | update_time | DATETIME | 更新时间 | **索引**: - `PRIMARY KEY (id)` - `KEY idx_next_notify_time (next_notify_time)` ### 6. 支付通知日志表 (`pay_notify_log`) 用于记录回调通知的请求和响应日志(可选)。 | 字段名 | 类型 | 说明 | |--------|------|------| | id | BIGINT | 日志ID(主键) | | app_id | BIGINT | 应用ID | | channel_id | BIGINT | 渠道ID | | order_id | BIGINT | 支付订单ID | | notify_type | VARCHAR(32) | 通知类型(pay, refund) | | status | TINYINT | 处理状态(0处理中 10成功 20失败) | | request_params | TEXT | 请求参数 | | response_body | TEXT | 响应结果 | | create_time | DATETIME | 创建时间 | **索引**: - `PRIMARY KEY (id)` - `KEY idx_order_id (order_id)` **数据库脚本位置**: `script/sql/ry_pay.sql` ## 关键枚举定义 ### 渠道编码 (PayChannelEnum) 见 [支持的支付渠道](#支持的支付渠道) 章节。 ### 订单状态 (PayOrderStatusEnum) | 状态码 | 描述 | |:---|:---| | `0` | **WAITING** (未支付) | | `10` | **SUCCESS** (支付成功) | | `20` | **REFUND** (已退款) | | `30` | **CLOSED** (已关闭) | ### 退款状态 (RefundStatusEnum) | 状态码 | 描述 | |:---|:---| | `0` | **WAITING** (等待处理) | | `10` | **SUCCESS** (退款成功) | | `20` | **FAIL** (退款失败) | ## 模块结构 ``` org.dromara.pay/ ├── config/ # 配置类 │ └── PayProperties.java ├── controller/ # 控制器 │ ├── admin/ # 管理端 │ │ ├── PayAppController.java │ │ ├── PayChannelController.java │ │ ├── PayOrderController.java │ │ └── PayRefundController.java │ ├── app/ # 移动端 │ │ ├── AppPayOrderController.java │ │ └── AppPayRefundController.java │ └── PayNotifyController.java # 回调接口 ├── core/ # 核心策略 │ ├── PayStrategy.java │ ├── PayStrategyFactory.java │ └── impl/ │ ├── AlipayStrategy.java │ ├── WechatPayStrategy.java │ └── WechatPayUtil.java ├── domain/ # 领域模型 │ ├── PayApp.java │ ├── PayChannel.java │ ├── PayOrder.java │ ├── PayRefund.java │ ├── PayNotifyTask.java │ ├── bo/ # 业务对象 │ └── vo/ # 视图对象 ├── enums/ # 枚举 │ ├── PayChannelEnum.java │ ├── PayOrderStatusEnum.java │ └── RefundStatusEnum.java ├── event/ # 事件 │ └── PaySuccessEvent.java ├── job/ # 定时任务 │ ├── PayNotifyJob.java │ ├── PayOrderExpireJob.java │ └── PayOrderSyncJob.java ├── listener/ # 监听器 │ └── PayOrderListener.java ├── mapper/ # 数据访问 │ ├── PayAppMapper.java │ ├── PayChannelMapper.java │ ├── PayOrderMapper.java │ ├── PayRefundMapper.java │ └── PayNotifyTaskMapper.java ├── service/ # 服务层 │ ├── PayAppService.java │ ├── PayChannelService.java │ ├── PayOrderService.java │ ├── PayRefundService.java │ ├── PayNotifyService.java │ └── impl/ └── util/ # 工具类 └── WechatPayUtil.java ``` ## 核心业务流程 ### 1. 统一下单流程 ``` 业务方 → 支付中心 → 策略工厂 → 三方渠道 ↓ ↓ ↓ ↓ 提交订单 创建订单 获取策略 调用下单 ↑ ↑ ↑ ↑ 返回参数 返回参数 返回参数 返回参数 ``` **详细流程**: 1. 业务方调用 `/app/pay/order/submit` 提交支付订单 2. 支付中心校验 App 状态和参数 3. 创建 `pay_order` 记录(状态为 WAITING) 4. 通过策略工厂获取对应的支付策略(Alipay/Wechat) 5. 读取 `pay_channel` 配置,实例化支付客户端 6. 调用三方渠道的下单接口 7. 封装标准响应 `PayOrderSubmitRespVO` 返回给业务方 8. 业务方使用返回的支付参数调起支付 ### 2. 支付回调流程 ``` 三方渠道 → 支付中心 → 业务方 ↓ ↓ ↓ 异步回调 验签更新 接收通知 ↑ ↑ ↑ 返回成功 创建任务 返回success ``` **详细流程**: 1. 支付成功后,三方渠道异步回调 `/pay/notify/{channelCode}/{channelId}` 2. 支付中心解析参数并进行验签 3. 加分布式锁(`lock:order:{id}`)确保幂等性 4. 幂等校验(若已支付则忽略) 5. 更新 `pay_order` 状态为 SUCCESS 6. 发布支付成功事件(`PaySuccessEvent`) 7. 监听器创建 `pay_notify_task` 回调任务 8. 立即执行一次 HTTP 回调通知业务方 9. 业务方返回 `"success"` 或 `{"code": 200}` 10. 如果失败,定时任务按指数退避策略重试 ### 3. 退款流程 ``` 业务方 → 支付中心 → 三方渠道 ↓ ↓ ↓ 申请退款 创建退款 调用退款 ↑ ↑ ↑ 回调通知 更新状态 返回结果 ``` **详细流程**: 1. 业务方调用 `/app/pay/refund/apply` 申请退款 2. 支付中心校验订单(是否已支付、金额是否足够) 3. 创建 `pay_refund` 记录(状态为 WAITING) 4. 调用三方渠道的退款接口 5. **如果三方同步返回退款成功**: - 更新 `pay_refund` 状态为 SUCCESS - 更新 `pay_order` 状态(REFUND/部分退款) - 发送退款回调通知 6. **如果三方异步处理中**: - 保持 WAITING 状态等待回调 - 接收三方异步退款回调 - 更新 `pay_refund` 和 `pay_order` 状态 - 发送退款回调通知 ### 4. 防掉单轮询流程 ``` 定时任务 → 支付中心 → 三方渠道 → 业务方 ↓ ↓ ↓ ↓ 扫描订单 查询状态 返回状态 补发通知 ↓ ↓ 更新状态 触发逻辑 ``` **详细流程**: 1. 定时任务(每2分钟)扫描状态为 `WAITING`、创建时间超过2分钟、未过期的订单 2. 调用三方渠道的查单接口 3. **如果已支付(掉单补救)**: - 更新 `pay_order` 状态为 SUCCESS - 触发支付成功逻辑 - 补发支付成功通知给业务方 4. **如果支付关闭/超时**: - 更新 `pay_order` 状态为 CLOSED - (可选)调用三方关单接口 5. **如果等待支付**: - 不做处理,继续等待 ## API 接口文档 ### 管理端接口 #### 支付应用管理 (`/pay/app`) | 方法 | 路径 | 说明 | 权限 | |:---|:---|:---|:---| | GET | `/pay/app/list` | 查询支付应用列表 | `pay:app:list` | | GET | `/pay/app/{id}` | 查询支付应用详情 | `pay:app:query` | | POST | `/pay/app` | 新增支付应用 | `pay:app:add` | | PUT | `/pay/app` | 修改支付应用 | `pay:app:edit` | | DELETE | `/pay/app/{ids}` | 删除支付应用 | `pay:app:remove` | | POST | `/pay/app/export` | 导出支付应用列表 | `pay:app:export` | #### 支付渠道管理 (`/pay/channel`) | 方法 | 路径 | 说明 | 权限 | |:---|:---|:---|:---| | GET | `/pay/channel/list` | 查询支付渠道列表 | `pay:channel:list` | | GET | `/pay/channel/{id}` | 查询支付渠道详情 | `pay:channel:query` | | POST | `/pay/channel` | 新增支付渠道 | `pay:channel:add` | | PUT | `/pay/channel` | 修改支付渠道 | `pay:channel:edit` | | DELETE | `/pay/channel/{ids}` | 删除支付渠道 | `pay:channel:remove` | | POST | `/pay/channel/export` | 导出支付渠道列表 | `pay:channel:export` | #### 支付订单管理 (`/pay/order`) | 方法 | 路径 | 说明 | 权限 | |:---|:---|:---|:---| | GET | `/pay/order/list` | 查询支付订单列表 | `pay:order:list` | | GET | `/pay/order/{id}` | 查询支付订单详情 | `pay:order:query` | | POST | `/pay/order` | 新增支付订单 | `pay:order:add` | | POST | `/pay/order/submit` | 提交支付订单 | `pay:order:submit` | | PUT | `/pay/order` | 修改支付订单 | `pay:order:edit` | | DELETE | `/pay/order/{ids}` | 删除支付订单 | `pay:order:remove` | | POST | `/pay/order/export` | 导出支付订单列表 | `pay:order:export` | #### 退款订单管理 (`/pay/refund`) | 方法 | 路径 | 说明 | 权限 | |:---|:---|:---|:---| | GET | `/pay/refund/list` | 查询退款订单列表 | `pay:refund:list` | | GET | `/pay/refund/{id}` | 查询退款订单详情 | `pay:refund:query` | | POST | `/pay/refund` | 新增退款订单 | `pay:refund:add` | | POST | `/pay/refund/apply` | 申请退款 | `pay:refund:apply` | | PUT | `/pay/refund` | 修改退款订单 | `pay:refund:edit` | | DELETE | `/pay/refund/{ids}` | 删除退款订单 | `pay:refund:remove` | | POST | `/pay/refund/export` | 导出退款订单列表 | `pay:refund:export` | ### 移动端接口 #### 支付订单 (`/app/pay/order`) | 方法 | 路径 | 说明 | |:---|:---|:---| | POST | `/app/pay/order/submit` | 提交支付订单 | | GET | `/app/pay/order/list` | 查询支付订单列表 | | GET | `/app/pay/order/{id}` | 查询支付订单详情 | | GET | `/app/pay/order/query` | 根据商户订单号查询 | **提交支付订单请求示例**: ```json { "appId": 1, "channelCode": "wx_pub", "channelId": 1, "merchantOrderId": "ORDER20240101001", "subject": "商品订单", "body": "商品订单-ORDER20240101001", "price": 10000, "notifyUrl": "https://your-domain.com/app/mall/pay/notify", "userIp": "127.0.0.1", "channelUserId": "openid_xxx" } ``` **响应示例**: ```json { "code": 200, "msg": "操作成功", "data": { "payOrderNo": "PAY20240101001", "payData": "{\"appId\":\"wx123\",\"timeStamp\":\"1234567890\",\"nonceStr\":\"abc\",\"package\":\"prepay_id=xxx\",\"signType\":\"RSA\",\"paySign\":\"xxx\"}", "status": 0 } } ``` #### 退款 (`/app/pay/refund`) | 方法 | 路径 | 说明 | |:---|:---|:---| | POST | `/app/pay/refund/apply` | 申请退款 | | GET | `/app/pay/refund/list` | 查询退款订单列表 | | GET | `/app/pay/refund/{id}` | 查询退款订单详情 | | GET | `/app/pay/refund/query` | 根据商户退款编号查询 | **申请退款请求示例**: ```json { "orderNo": "PAY20240101001", "merchantRefundId": "REFUND20240101001", "refundPrice": 10000, "reason": "用户申请退款", "notifyUrl": "https://your-domain.com/app/mall/refund/notify", "userIp": "127.0.0.1" } ``` ### 回调接口 #### 支付回调 (`/pay/notify`) | 方法 | 路径 | 说明 | |:---|:---|:---| | POST | `/pay/notify/{channelCode}/{channelId}` | 接收支付回调 | **回调通知内容**: ```json { "orderNo": "PAY20240101001", "merchantOrderId": "ORDER20240101001", "status": 10, "price": 10000, "channelUserId": "openid_xxx", "successTime": "2024-01-01 12:00:00", "channelOrderNo": "wx20240101001" } ``` **业务方响应**: 返回 `"success"` 或 `{"code": 200}` 表示成功 #### 退款回调 (`/pay/notify/refund`) | 方法 | 路径 | 说明 | |:---|:---|:---| | POST | `/pay/notify/refund/{channelCode}/{channelId}` | 接收退款回调 | ## 配置说明 ### 支付应用配置 ```json { "name": "电商应用", "status": 0, "remark": "电商业务支付应用", "payNotifyUrl": "https://your-domain.com/pay/notify", "refundNotifyUrl": "https://your-domain.com/pay/notify/refund" } ``` ### 支付宝渠道配置 ```json { "serverUrl": "https://openapi.alipay.com/gateway.do", "appId": "your_app_id", "privateKey": "your_private_key", "publicKey": "your_public_key", "signType": "RSA2" } ``` **说明**: - `serverUrl`: 支付宝网关地址(生产环境/沙箱环境) - `appId`: 支付宝应用ID - `privateKey`: 应用私钥(RSA2格式) - `publicKey`: 支付宝公钥 - `signType`: 签名类型,固定为 `RSA2` ### 微信支付渠道配置 ```json { "appId": "your_app_id", "mchId": "your_mch_id", "privateKey": "your_private_key", "mchSerialNo": "your_serial_no", "apiV3Key": "your_api_v3_key" } ``` **说明**: - `appId`: 微信应用ID(公众号/小程序/App) - `mchId`: 商户号 - `privateKey`: 商户私钥(PEM格式) - `mchSerialNo`: 商户证书序列号 - `apiV3Key`: APIv3密钥 ## 定时任务 ### 支付订单过期处理 (PayOrderExpireJob) - **执行频率**: 每分钟 (`0 0/1 * * * ?`) - **功能**: - 扫描状态为 `WAITING` 且已过期的订单 - 调用支付渠道的关单接口 - 更新订单状态为 `CLOSED` - **限制**: 每次处理最多100条,防止积压 ### 支付订单状态同步 (PayOrderSyncJob) - **执行频率**: 每2分钟 (`0 0/2 * * * ?`) - **功能**: - 扫描状态为 `WAITING`、创建时间超过2分钟、未过期的订单 - 调用支付渠道的查单接口 - 如果已支付,更新订单状态并触发支付成功逻辑 - 如果已关闭,更新订单状态 - **目的**: 防止掉单,主动同步三方状态 ### 支付回调通知重试 (PayNotifyJob) - **执行频率**: 每30秒 (`0/30 * * * * ?`) - **功能**: - 扫描状态为 `进行中` 且到达通知时间的任务 - 执行HTTP回调通知 - 根据响应判断成功或失败 - 失败时按指数退避策略计算下次通知时间 - **重试策略**: 15s → 30s → 1m → 2m → 5m - **最大重试次数**: 5次 ## 数据校验规则 ### 支付订单校验 - ✅ 商户订单号唯一性校验 - ✅ 支付单号唯一性校验 - ✅ 删除时校验:已支付的订单不能删除 ### 退款订单校验 - ✅ 退款单号唯一性校验 - ✅ 商户退款编号唯一性校验 - ✅ 退款金额不能超过支付金额 - ✅ 删除时校验:处理中的退款不能删除 ### 支付应用校验 - ✅ 应用名称不能为空 - ✅ 回调地址格式校验(必须以 http:// 或 https:// 开头) ### 支付渠道校验 - ✅ 渠道编码不能为空 - ✅ 同一应用下渠道编码唯一性校验 - ✅ 渠道配置JSON格式校验 ## 接入指南 ### Maven 依赖 ```xml org.dromara ruoyi-pay ${revision} ``` ### 快速开始 #### 步骤1: 创建支付应用 在管理后台创建支付应用,配置回调地址。 #### 步骤2: 配置支付渠道 在应用下配置支付渠道,填写渠道参数(JSON格式)。 #### 步骤3: 调用统一下单接口 ```java PayOrderBo bo = new PayOrderBo(); bo.setAppId(1L); bo.setChannelCode("wx_pub"); bo.setMerchantOrderId("ORDER001"); bo.setSubject("商品订单"); bo.setPrice(10000); bo.setNotifyUrl("https://your-domain.com/notify"); PayOrderSubmitRespVO resp = payOrderService.submitOrder(bo); ``` #### 步骤4: 处理支付回调 实现回调接口,接收支付成功通知: ```java @PostMapping("/notify") public String notify(@RequestBody Map data) { String orderNo = (String) data.get("merchantOrderId"); // 处理业务逻辑 return "success"; } ``` ## 扩展开发 ### 添加新的支付渠道 1. 实现 `PayStrategy` 接口 2. 在 `@PostConstruct` 方法中注册到 `PayStrategyFactory` 3. 在 `PayChannelEnum` 中添加新的渠道枚举 4. 配置渠道参数 示例: ```java @Component @RequiredArgsConstructor public class NewPayStrategy implements PayStrategy { private final PayStrategyFactory payStrategyFactory; @PostConstruct public void init() { payStrategyFactory.register("new_pay", this); } @Override public PayOrderSubmitRespVO submitOrder(PayOrderBo bo, PayChannel channel) { // 实现下单逻辑 } // 实现其他接口方法... } ``` ## 错误码定义 | 错误信息 | 说明 | |:---|:---| | `应用不存在或已禁用` | 支付应用不存在或状态为关闭 | | `支付渠道不存在或已禁用` | 支付渠道不存在或状态为关闭 | | `商户订单号已存在` | 商户订单号重复 | | `支付单号已存在` | 支付单号重复 | | `退款单号已存在` | 退款单号重复 | | `商户退款编号已存在` | 商户退款编号重复 | | `退款金额不能超过支付金额` | 退款金额校验失败 | | `订单不存在` | 支付订单不存在 | | `订单未支付或已退款` | 订单状态不允许退款 | | `不支持的支付渠道` | 渠道编码不在支持列表中 | | `未找到支付策略` | 策略未注册 | ## 扩展功能规划 ### 收银台模式 ⏳ - 提供统一的 H5 收银台页面 - 业务方只需传入订单号,跳转至收银台 - 用户在收银台选择微信/支付宝/余额支付 - **状态**: ⏳ 待实现 ### 钱包系统 (Wallet) ⏳ - **充值**: 创建充值订单 -> 调起支付 -> 回调增加余额 - **消费**: 余额扣减 -> 生成消费记录 - **提现**: 申请提现 -> 管理员审核 -> 企业付款到零钱/银行卡 - **状态**: ⏳ 待实现 ### 模拟支付 (Mock) ⏳ - 开发环境专用 - 下单直接返回成功,不调用真实三方接口 - 用于前端联调和流程测试 - **状态**: ⏳ 待实现 ### 对账功能 ⏳ - 每日对账任务,对比支付中心订单与三方渠道订单 - 生成对账报表,标记差异订单 - **状态**: ⏳ 待实现 ### 支付统计报表 ⏳ - 按日/月统计支付金额、订单数、成功率等 - 按渠道统计支付数据 - **状态**: ⏳ 待实现 ## 注意事项 ### 幂等性 - 支付回调需要保证幂等处理,避免重复处理 - 通过订单状态判断,已支付的订单直接返回成功 - 使用分布式锁确保并发安全 ### 安全性 - 回调接口需要验签,确保请求来自支付渠道 - 使用 HTTPS 传输敏感信息 - 渠道配置中的密钥需要妥善保管 - 不要在日志中输出敏感信息(密钥、密码等) ### 超时处理 - 支付订单默认30分钟过期,可通过配置调整 - 过期订单会自动关闭,调用渠道关单接口 - 业务方需要及时处理订单,避免过期 ### 重试机制 - 回调通知采用指数退避策略,最多重试5次 - 重试间隔:15s → 30s → 1m → 2m → 5m - 业务方需要快速响应回调,返回 `"success"` 或 `{"code": 200}` - 回调响应时间建议控制在3秒内 ### 防掉单 - 定时任务主动查询订单状态,防止掉单 - 查询创建时间超过2分钟且未过期的待支付订单 - 如果发现已支付但未收到回调,主动触发支付成功逻辑 ### 金额单位 - 所有金额以"分"为单位存储和传递 - 前端展示时需要转换为"元" - 计算时注意单位转换,避免精度问题 ### 回调地址 - 回调地址必须是公网可访问的 HTTPS 地址 - 回调地址需要支持 POST 请求 - 回调响应需要快速返回,避免超时 - 建议使用内网地址 + 反向代理的方式处理回调 ## 依赖模块 - `ruoyi-common-core` - 核心工具类 - `ruoyi-common-mybatis` - 数据库操作 - `ruoyi-common-web` - Web框架支持 - `ruoyi-common-security` - 安全认证(Sa-Token) - `ruoyi-common-log` - 日志记录 - `ruoyi-common-idempotent` - 幂等性控制 - `ruoyi-common-redis` - Redis缓存 - `alipay-sdk-java` - 支付宝SDK - `wechatpay-java` - 微信支付SDK - `weixin-java-mp` - 微信公众号SDK(用于JSAPI支付)