# 支付中心 (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支付)