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