Files
2026-02-22 12:12:02 +08:00

913 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 支付中心 (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
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-pay</artifactId>
<version>${revision}</version>
</dependency>
```
### 快速开始
#### 步骤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<String, Object> 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支付)