first commit
This commit is contained in:
@@ -0,0 +1,912 @@
|
||||
# 支付中心 (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支付)
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-pay</artifactId>
|
||||
|
||||
<description>
|
||||
ruoyi-pay支付模块
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-doc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-log</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-idempotent</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付宝SDK -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
</dependency>
|
||||
<!-- 微信支付 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.github.wechatpay-apiv3</groupId>
|
||||
<artifactId>wechatpay-java</artifactId>
|
||||
</dependency>
|
||||
<!-- 微信公众号 SDK (for JSAPI) -->
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-mp</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.dromara.pay.config;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* 支付配置属性
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "pay")
|
||||
public class PayProperties {
|
||||
|
||||
/**
|
||||
* 支付宝配置
|
||||
*/
|
||||
@Valid
|
||||
private AlipayProperties alipay = new AlipayProperties();
|
||||
|
||||
/**
|
||||
* 微信支付配置
|
||||
*/
|
||||
@Valid
|
||||
private WechatProperties wechat = new WechatProperties();
|
||||
|
||||
@Data
|
||||
public static class AlipayProperties {
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotBlank(message = "支付宝AppId不能为空")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@NotBlank(message = "支付宝商户号不能为空")
|
||||
private String mchId;
|
||||
|
||||
/**
|
||||
* 支付宝网关地址
|
||||
*/
|
||||
@NotBlank(message = "支付宝网关地址不能为空")
|
||||
private String serverUrl;
|
||||
|
||||
/**
|
||||
* 支付宝公钥
|
||||
*/
|
||||
@NotBlank(message = "支付宝公钥不能为空")
|
||||
private String alipayPublicKey;
|
||||
|
||||
/**
|
||||
* 应用私钥
|
||||
*/
|
||||
@NotBlank(message = "应用私钥不能为空")
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
@NotBlank(message = "支付宝异步通知地址不能为空")
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 同步跳转地址
|
||||
*/
|
||||
@NotBlank(message = "支付宝同步跳转地址不能为空")
|
||||
private String returnUrl;
|
||||
|
||||
/**
|
||||
* 签名类型
|
||||
*/
|
||||
private String signType = "RSA2";
|
||||
|
||||
/**
|
||||
* 字符编码
|
||||
*/
|
||||
private String charset = "UTF-8";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class WechatProperties {
|
||||
/**
|
||||
* 公众号AppId 1
|
||||
*/
|
||||
@NotBlank(message = "微信公众号AppId不能为空")
|
||||
private String appMp1Id;
|
||||
|
||||
/**
|
||||
* 公众号AppId 2
|
||||
*/
|
||||
private String appMp2Id;
|
||||
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@NotBlank(message = "微信商户号不能为空")
|
||||
private String mchId;
|
||||
|
||||
/**
|
||||
* 商户证书序列号
|
||||
*/
|
||||
@NotBlank(message = "微信商户证书序列号不能为空")
|
||||
private String mchSerialNo;
|
||||
|
||||
/**
|
||||
* 商户API私钥
|
||||
*/
|
||||
@NotBlank(message = "微信商户API私钥不能为空")
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* API V3 密钥
|
||||
*/
|
||||
@NotBlank(message = "微信API V3密钥不能为空")
|
||||
private String apiV3Key;
|
||||
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
@NotBlank(message = "微信异步通知地址不能为空")
|
||||
private String notifyUrl;
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package org.dromara.pay.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.vo.PayNotifyResult;
|
||||
import org.dromara.pay.domain.vo.PayChannelVo;
|
||||
import org.dromara.pay.service.PayChannelService;
|
||||
import org.dromara.pay.service.PayOrderService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 支付回调通知
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/pay/notify")
|
||||
public class PayNotifyController {
|
||||
|
||||
private final PayOrderService payOrderService;
|
||||
private final PayChannelService payChannelService;
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
private final org.dromara.pay.service.PayRefundService payRefundService;
|
||||
|
||||
/**
|
||||
* 接收支付回调
|
||||
*
|
||||
* @param channelCode 渠道编码
|
||||
* @param channelId 渠道ID
|
||||
* @param request 请求对象
|
||||
*/
|
||||
@SaIgnore
|
||||
@PostMapping("/{channelCode}/{channelId}")
|
||||
public String notify(@PathVariable String channelCode, @PathVariable Long channelId, HttpServletRequest request) {
|
||||
log.info("收到支付回调: channelCode={}, channelId={}", channelCode, channelId);
|
||||
|
||||
// 1. 获取渠道配置
|
||||
PayChannelVo channelVo = payChannelService.queryById(channelId);
|
||||
if (channelVo == null) {
|
||||
log.error("支付回调失败,渠道不存在: {}", channelId);
|
||||
return "failure";
|
||||
}
|
||||
if (!channelVo.getCode().equals(channelCode)) {
|
||||
log.error("支付回调失败,渠道编码不匹配: {} != {}", channelVo.getCode(), channelCode);
|
||||
return "failure";
|
||||
}
|
||||
PayChannel channel = MapstructUtils.convert(channelVo, PayChannel.class);
|
||||
|
||||
// 2. 获取策略处理回调
|
||||
try {
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channelCode);
|
||||
PayNotifyResult result = strategy.processNotify(request, channel);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
// 3. 更新订单状态
|
||||
// 这里需要根据 orderNo 查询订单,确保存在且未支付
|
||||
// 暂时假设 PayOrderService 有方法处理状态更新,或者我们直接在这里处理
|
||||
// 由于 PayOrderService 主要是 CRUD,这里我们可能需要扩展它或直接操作
|
||||
|
||||
// 简单实现:查询订单 -> 更新状态
|
||||
// 注意:需要幂等处理 (result.isSuccess() 只是验签通过,业务状态可能已经更新)
|
||||
handleOrderSuccess(result.getOrderNo(), result.getTradeNo(), channelCode);
|
||||
|
||||
return result.getResponseBody();
|
||||
} else {
|
||||
log.error("支付回调验签失败: {}", channelCode);
|
||||
return result.getResponseBody(); // 通常是 failure
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("支付回调处理异常", e);
|
||||
return "failure";
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOrderSuccess(String orderNo, String channelOrderNo, String channelCode) {
|
||||
payOrderService.notifyOrderSuccess(orderNo, channelOrderNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收退款回调
|
||||
*
|
||||
* @param channelCode 渠道编码
|
||||
* @param channelId 渠道ID
|
||||
* @param request 请求对象
|
||||
*/
|
||||
@SaIgnore
|
||||
@PostMapping("/refund/{channelCode}/{channelId}")
|
||||
public String refundNotify(@PathVariable String channelCode, @PathVariable Long channelId, HttpServletRequest request) {
|
||||
log.info("收到退款回调: channelCode={}, channelId={}", channelCode, channelId);
|
||||
|
||||
// 1. 获取渠道配置
|
||||
PayChannelVo channelVo = payChannelService.queryById(channelId);
|
||||
if (channelVo == null) {
|
||||
log.error("退款回调失败,渠道不存在: {}", channelId);
|
||||
return "failure";
|
||||
}
|
||||
if (!channelVo.getCode().equals(channelCode)) {
|
||||
log.error("退款回调失败,渠道编码不匹配: {} != {}", channelVo.getCode(), channelCode);
|
||||
return "failure";
|
||||
}
|
||||
PayChannel channel = MapstructUtils.convert(channelVo, PayChannel.class);
|
||||
|
||||
// 2. 获取策略处理回调(退款回调通常与支付回调类似,但需要解析退款单号)
|
||||
try {
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channelCode);
|
||||
PayNotifyResult result = strategy.processNotify(request, channel);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
// 3. 更新退款状态
|
||||
// 注意:退款回调中,orderNo可能是退款单号,需要根据实际情况调整
|
||||
// 这里假设result.getOrderNo()是退款单号
|
||||
handleRefundSuccess(result.getOrderNo(), result.getTradeNo(), channelCode);
|
||||
|
||||
return result.getResponseBody();
|
||||
} else {
|
||||
log.error("退款回调验签失败: {}", channelCode);
|
||||
return result.getResponseBody();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("退款回调处理异常", e);
|
||||
return "failure";
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRefundSuccess(String refundNo, String channelRefundNo, String channelCode) {
|
||||
payRefundService.notifyRefundSuccess(refundNo, channelRefundNo);
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package org.dromara.pay.controller.admin;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayAppBo;
|
||||
import org.dromara.pay.domain.vo.PayAppVo;
|
||||
import org.dromara.pay.service.PayAppService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 支付应用
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/pay/app")
|
||||
public class PayAppController extends BaseController {
|
||||
|
||||
private final PayAppService payAppService;
|
||||
|
||||
/**
|
||||
* 查询支付应用列表
|
||||
*/
|
||||
@SaCheckPermission("pay:app:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayAppVo> list(PayAppBo bo, PageQuery pageQuery) {
|
||||
return payAppService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付应用详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("pay:app:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<PayAppVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(payAppService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付应用
|
||||
*/
|
||||
@SaCheckPermission("pay:app:add")
|
||||
@Log(title = "支付应用", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody PayAppBo bo) {
|
||||
return toAjax(payAppService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付应用
|
||||
*/
|
||||
@SaCheckPermission("pay:app:edit")
|
||||
@Log(title = "支付应用", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody PayAppBo bo) {
|
||||
return toAjax(payAppService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付应用
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("pay:app:remove")
|
||||
@Log(title = "支付应用", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(payAppService.deleteWithValidByIds(Arrays.asList(ids), true));
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package org.dromara.pay.controller.admin;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayChannelBo;
|
||||
import org.dromara.pay.domain.vo.PayChannelVo;
|
||||
import org.dromara.pay.service.PayChannelService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付渠道
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/pay/channel")
|
||||
public class PayChannelController extends BaseController {
|
||||
|
||||
private final PayChannelService payChannelService;
|
||||
|
||||
/**
|
||||
* 查询支付渠道列表
|
||||
*/
|
||||
@SaCheckPermission("pay:channel:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayChannelVo> list(PayChannelBo bo, PageQuery pageQuery) {
|
||||
return payChannelService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付渠道详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("pay:channel:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<PayChannelVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(payChannelService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付渠道
|
||||
*/
|
||||
@SaCheckPermission("pay:channel:add")
|
||||
@Log(title = "支付渠道", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody PayChannelBo bo) {
|
||||
return toAjax(payChannelService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付渠道
|
||||
*/
|
||||
@SaCheckPermission("pay:channel:edit")
|
||||
@Log(title = "支付渠道", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody PayChannelBo bo) {
|
||||
return toAjax(payChannelService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付渠道
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("pay:channel:remove")
|
||||
@Log(title = "支付渠道", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(payChannelService.deleteWithValidByIds(Arrays.asList(ids), true));
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package org.dromara.pay.controller.admin;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayOrderBo;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.domain.vo.PayOrderVo;
|
||||
import org.dromara.pay.service.PayOrderService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/pay/order")
|
||||
public class PayOrderController extends BaseController {
|
||||
|
||||
private final PayOrderService payOrderService;
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
@SaCheckPermission("pay:order:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayOrderVo> list(PayOrderBo bo, PageQuery pageQuery) {
|
||||
return payOrderService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付订单详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("pay:order:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<PayOrderVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(payOrderService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付订单
|
||||
*/
|
||||
@SaCheckPermission("pay:order:add")
|
||||
@Log(title = "支付订单", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody PayOrderBo bo) {
|
||||
return toAjax(payOrderService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交支付订单
|
||||
*/
|
||||
@SaCheckPermission("pay:order:submit")
|
||||
@Log(title = "支付订单", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/submit")
|
||||
public R<PayOrderSubmitRespVO> submit(@Validated @RequestBody PayOrderBo bo) {
|
||||
return R.ok(payOrderService.submitOrder(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付订单
|
||||
*/
|
||||
@SaCheckPermission("pay:order:edit")
|
||||
@Log(title = "支付订单", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody PayOrderBo bo) {
|
||||
return toAjax(payOrderService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付订单
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("pay:order:remove")
|
||||
@Log(title = "支付订单", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(payOrderService.deleteWithValidByIds(Arrays.asList(ids), true));
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package org.dromara.pay.controller.admin;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayRefundBo;
|
||||
import org.dromara.pay.domain.vo.PayRefundVo;
|
||||
import org.dromara.pay.service.PayRefundService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 退款订单
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/pay/refund")
|
||||
public class PayRefundController extends BaseController {
|
||||
|
||||
private final PayRefundService payRefundService;
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
@SaCheckPermission("pay:refund:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayRefundVo> list(PayRefundBo bo, PageQuery pageQuery) {
|
||||
return payRefundService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退款订单详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("pay:refund:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<PayRefundVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(payRefundService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增退款订单
|
||||
*/
|
||||
@SaCheckPermission("pay:refund:add")
|
||||
@Log(title = "退款订单", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody PayRefundBo bo) {
|
||||
return toAjax(payRefundService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改退款订单
|
||||
*/
|
||||
@SaCheckPermission("pay:refund:edit")
|
||||
@Log(title = "退款订单", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody PayRefundBo bo) {
|
||||
return toAjax(payRefundService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除退款订单
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("pay:refund:remove")
|
||||
@Log(title = "退款订单", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(payRefundService.deleteWithValidByIds(Arrays.asList(ids), true));
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package org.dromara.pay.controller.app;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayOrderBo;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.domain.vo.PayOrderVo;
|
||||
import org.dromara.pay.service.PayOrderService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 移动端支付订单Controller
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/pay/order")
|
||||
public class AppPayOrderController extends BaseController {
|
||||
|
||||
private final PayOrderService payOrderService;
|
||||
|
||||
/**
|
||||
* 提交支付订单
|
||||
*/
|
||||
@PostMapping("/submit")
|
||||
public R<PayOrderSubmitRespVO> submitOrder(@Validated @RequestBody PayOrderBo bo) {
|
||||
PayOrderSubmitRespVO resp = payOrderService.submitOrder(bo);
|
||||
return R.ok(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayOrderVo> list(PayOrderBo bo, PageQuery pageQuery) {
|
||||
// 可以根据业务需要过滤当前用户的订单
|
||||
return payOrderService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R<PayOrderVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(payOrderService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户订单号查询支付订单
|
||||
*/
|
||||
@GetMapping("/query")
|
||||
public R<PayOrderVo> queryByMerchantOrderId(@RequestParam String merchantOrderId) {
|
||||
PayOrderBo bo = new PayOrderBo();
|
||||
bo.setMerchantOrderId(merchantOrderId);
|
||||
var list = payOrderService.queryList(bo);
|
||||
if (list.isEmpty()) {
|
||||
return R.fail("支付订单不存在");
|
||||
}
|
||||
return R.ok(list.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package org.dromara.pay.controller.app;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.pay.domain.bo.PayRefundBo;
|
||||
import org.dromara.pay.domain.vo.PayRefundVo;
|
||||
import org.dromara.pay.service.PayRefundService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 移动端退款Controller
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/pay/refund")
|
||||
public class AppPayRefundController extends BaseController {
|
||||
|
||||
private final PayRefundService refundService;
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
*/
|
||||
@PostMapping("/apply")
|
||||
@Log(title = "申请退款", businessType = BusinessType.INSERT)
|
||||
public R<Void> applyRefund(@Validated @RequestBody PayRefundBo bo) {
|
||||
return toAjax(refundService.createRefund(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<PayRefundVo> list(PayRefundBo bo, PageQuery pageQuery) {
|
||||
return refundService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R<PayRefundVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(refundService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据商户退款编号查询
|
||||
*/
|
||||
@GetMapping("/query")
|
||||
public R<PayRefundVo> queryByMerchantRefundId(@RequestParam String merchantRefundId) {
|
||||
PayRefundBo bo = new PayRefundBo();
|
||||
bo.setMerchantRefundId(merchantRefundId);
|
||||
List<PayRefundVo> list = refundService.queryList(bo);
|
||||
if (list.isEmpty()) {
|
||||
return R.fail("退款订单不存在");
|
||||
}
|
||||
return R.ok(list.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.dromara.pay.core;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import org.dromara.pay.domain.vo.PayNotifyResult;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
|
||||
/**
|
||||
* 支付策略接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayStrategy {
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*
|
||||
* @param order 支付订单
|
||||
* @param channel 支付渠道
|
||||
* @return 提交响应
|
||||
*/
|
||||
PayOrderSubmitRespVO unifiedOrder(PayOrder order, PayChannel channel);
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*
|
||||
* @param refund 退款订单
|
||||
* @param channel 支付渠道
|
||||
* @return 是否提交成功
|
||||
*/
|
||||
Boolean refund(PayRefund refund, PayChannel channel);
|
||||
|
||||
/**
|
||||
* 查询订单状态
|
||||
*
|
||||
* @param order 支付订单
|
||||
* @param channel 支付渠道
|
||||
* @return 渠道订单状态 (映射到 PayOrderStatusEnum)
|
||||
*/
|
||||
Integer queryOrder(PayOrder order, PayChannel channel);
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*
|
||||
* @param order 支付订单
|
||||
* @param channel 支付渠道
|
||||
* @return 是否关闭成功
|
||||
*/
|
||||
Boolean closeOrder(PayOrder order, PayChannel channel);
|
||||
|
||||
/**
|
||||
* 查询退款状态
|
||||
*
|
||||
* @param refund 退款订单
|
||||
* @param channel 支付渠道
|
||||
* @return 退款状态
|
||||
*/
|
||||
Integer queryRefund(PayRefund refund, PayChannel channel);
|
||||
|
||||
/**
|
||||
* 处理回调通知
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param channel 支付渠道
|
||||
* @return 处理结果
|
||||
*/
|
||||
PayNotifyResult processNotify(HttpServletRequest request, PayChannel channel);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.dromara.pay.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.pay.enums.PayChannelEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 支付策略工厂
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PayStrategyFactory {
|
||||
|
||||
private final Map<String, PayStrategy> strategyMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册策略
|
||||
*/
|
||||
public void register(String channelCode, PayStrategy strategy) {
|
||||
strategyMap.put(channelCode, strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取策略
|
||||
*/
|
||||
public PayStrategy getStrategy(String channelCode) {
|
||||
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
|
||||
if (channelEnum == null) {
|
||||
throw new ServiceException("不支持的支付渠道: " + channelCode);
|
||||
}
|
||||
// 简单映射:假设 code 前缀决定策略,例如 wx_pub -> WechatPayStrategy
|
||||
// 或者直接根据 code 获取 bean
|
||||
// 这里为了简单,假设实现类自己注册,或者通过 BeanName 获取
|
||||
// 比如 WechatPayStrategy 处理所有 wx_ 开头的
|
||||
|
||||
// 更好的方式是策略实现类在 init 时注册自己
|
||||
PayStrategy strategy = strategyMap.get(channelCode);
|
||||
if (strategy == null) {
|
||||
// 尝试根据前缀查找 (例如 wx_pub, wx_lite 都用 WechatPayStrategy)
|
||||
if (channelCode.startsWith("wx_")) {
|
||||
strategy = strategyMap.get("wx");
|
||||
} else if (channelCode.startsWith("alipay_")) {
|
||||
strategy = strategyMap.get("alipay");
|
||||
}
|
||||
}
|
||||
|
||||
if (strategy == null) {
|
||||
throw new ServiceException("未找到支付策略: " + channelCode);
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package org.dromara.pay.core.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.*;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.*;
|
||||
import com.alipay.api.response.*;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import org.dromara.pay.domain.vo.PayNotifyResult;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.enums.PayChannelEnum;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付宝支付策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class AlipayStrategy implements PayStrategy {
|
||||
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
payStrategyFactory.register(PayChannelEnum.ALIPAY_PC.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.ALIPAY_WAP.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.ALIPAY_APP.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.ALIPAY_QR.getCode(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderSubmitRespVO unifiedOrder(PayOrder order, PayChannel channel) {
|
||||
AlipayClient client = getClient(channel.getConfig());
|
||||
String code = channel.getCode();
|
||||
PayOrderSubmitRespVO resp = new PayOrderSubmitRespVO();
|
||||
resp.setPayOrderNo(order.getNo());
|
||||
resp.setStatus(0); // WAITING
|
||||
|
||||
try {
|
||||
if (PayChannelEnum.ALIPAY_PC.getCode().equals(code)) {
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
request.setReturnUrl(order.getNotifyUrl()); // 暂时复用
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
model.setSubject(order.getSubject());
|
||||
model.setTotalAmount(String.valueOf(order.getPrice() / 100.0));
|
||||
model.setProductCode("FAST_INSTANT_TRADE_PAY");
|
||||
request.setBizModel(model);
|
||||
AlipayTradePagePayResponse response = client.pageExecute(request);
|
||||
resp.setPayData(response.getBody());
|
||||
} else if (PayChannelEnum.ALIPAY_WAP.getCode().equals(code)) {
|
||||
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
model.setSubject(order.getSubject());
|
||||
model.setTotalAmount(String.valueOf(order.getPrice() / 100.0));
|
||||
model.setProductCode("QUICK_WAP_WAY");
|
||||
request.setBizModel(model);
|
||||
AlipayTradeWapPayResponse response = client.pageExecute(request);
|
||||
resp.setPayData(response.getBody());
|
||||
} else if (PayChannelEnum.ALIPAY_APP.getCode().equals(code)) {
|
||||
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
model.setSubject(order.getSubject());
|
||||
model.setTotalAmount(String.valueOf(order.getPrice() / 100.0));
|
||||
model.setProductCode("QUICK_MSECURITY_PAY");
|
||||
request.setBizModel(model);
|
||||
AlipayTradeAppPayResponse response = client.sdkExecute(request);
|
||||
resp.setPayData(response.getBody());
|
||||
} else if (PayChannelEnum.ALIPAY_QR.getCode().equals(code)) {
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
model.setSubject(order.getSubject());
|
||||
model.setTotalAmount(String.valueOf(order.getPrice() / 100.0));
|
||||
request.setBizModel(model);
|
||||
AlipayTradePrecreateResponse response = client.execute(request);
|
||||
resp.setPayData(response.getQrCode());
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝下单失败", e);
|
||||
throw new ServiceException("支付宝下单失败: " + e.getMessage());
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean refund(PayRefund refund, PayChannel channel) {
|
||||
AlipayClient client = getClient(channel.getConfig());
|
||||
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
||||
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
||||
model.setOutTradeNo(refund.getOrderNo());
|
||||
model.setRefundAmount(String.valueOf(refund.getRefundPrice() / 100.0));
|
||||
model.setRefundReason(refund.getReason());
|
||||
model.setOutRequestNo(refund.getNo());
|
||||
request.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeRefundResponse response = client.execute(request);
|
||||
if (response.isSuccess()) {
|
||||
refund.setChannelRefundNo(response.getTradeNo()); // 支付宝可能不返回退款单号,用TradeNo暂代
|
||||
return true;
|
||||
} else {
|
||||
refund.setErrorMsg(response.getSubMsg());
|
||||
refund.setErrorCode(response.getSubCode());
|
||||
return false;
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝退款失败", e);
|
||||
throw new ServiceException("支付宝退款失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer queryOrder(PayOrder order, PayChannel channel) {
|
||||
AlipayClient client = getClient(channel.getConfig());
|
||||
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
||||
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
request.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeQueryResponse response = client.execute(request);
|
||||
if (response.isSuccess()) {
|
||||
if ("TRADE_SUCCESS".equals(response.getTradeStatus()) || "TRADE_FINISHED".equals(response.getTradeStatus())) {
|
||||
return PayOrderStatusEnum.SUCCESS.getStatus();
|
||||
} else if ("TRADE_CLOSED".equals(response.getTradeStatus())) {
|
||||
return PayOrderStatusEnum.CLOSED.getStatus();
|
||||
}
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝查单失败", e);
|
||||
}
|
||||
return PayOrderStatusEnum.WAITING.getStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean closeOrder(PayOrder order, PayChannel channel) {
|
||||
AlipayClient client = getClient(channel.getConfig());
|
||||
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
|
||||
AlipayTradeCloseModel model = new AlipayTradeCloseModel();
|
||||
model.setOutTradeNo(order.getNo());
|
||||
request.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeCloseResponse response = client.execute(request);
|
||||
if (response.isSuccess()) {
|
||||
log.info("支付宝关单成功: {}", order.getNo());
|
||||
return true;
|
||||
} else {
|
||||
log.error("支付宝关单失败: {}", response.getSubMsg());
|
||||
return false;
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝关单异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer queryRefund(PayRefund refund, PayChannel channel) {
|
||||
AlipayClient client = getClient(channel.getConfig());
|
||||
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
|
||||
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
|
||||
model.setOutTradeNo(refund.getOrderNo());
|
||||
model.setOutRequestNo(refund.getNo());
|
||||
request.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeFastpayRefundQueryResponse response = client.execute(request);
|
||||
if (response.isSuccess()) {
|
||||
// 支付宝查询退款成功,一般表示退款成功,因为如果退款不存在会报错或返回空
|
||||
if (StrUtil.isNotBlank(response.getRefundAmount())) {
|
||||
return 10; // SUCCESS
|
||||
}
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝退款查询失败", e);
|
||||
}
|
||||
return 0; // WAITING or UNKNOWN
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayNotifyResult processNotify(HttpServletRequest request, PayChannel channel) {
|
||||
PayNotifyResult result = new PayNotifyResult();
|
||||
Map<String, String> params = new HashMap<>();
|
||||
Map<String, String[]> requestParams = request.getParameterMap();
|
||||
for (String name : requestParams.keySet()) {
|
||||
String[] values = requestParams.get(name);
|
||||
String valueStr = "";
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
|
||||
}
|
||||
params.put(name, valueStr);
|
||||
}
|
||||
|
||||
Map<String, Object> config = JsonUtils.parseMap(channel.getConfig());
|
||||
String alipayPublicKey = (String) config.get("publicKey");
|
||||
String signType = (String) config.get("signType");
|
||||
String charset = "UTF-8"; // 默认UTF-8
|
||||
|
||||
try {
|
||||
boolean verify = AlipaySignature.rsaCheckV1(params, alipayPublicKey, charset, signType);
|
||||
result.setSuccess(verify);
|
||||
if (verify) {
|
||||
result.setOrderNo(params.get("out_trade_no"));
|
||||
result.setTradeNo(params.get("trade_no"));
|
||||
result.setResponseBody("success");
|
||||
} else {
|
||||
result.setResponseBody("failure");
|
||||
}
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝回调验签异常", e);
|
||||
result.setSuccess(false);
|
||||
result.setResponseBody("failure");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AlipayClient getClient(String configJson) {
|
||||
Map<String, Object> config = JsonUtils.parseMap(configJson);
|
||||
return new DefaultAlipayClient(
|
||||
(String) config.get("serverUrl"),
|
||||
(String) config.get("appId"),
|
||||
(String) config.get("privateKey"),
|
||||
"json",
|
||||
"UTF-8",
|
||||
(String) config.get("publicKey"),
|
||||
(String) config.get("signType")
|
||||
);
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
package org.dromara.pay.core.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.wechat.pay.java.core.Config;
|
||||
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
||||
import com.wechat.pay.java.core.notification.NotificationConfig;
|
||||
import com.wechat.pay.java.core.notification.NotificationParser;
|
||||
import com.wechat.pay.java.core.notification.RequestParam;
|
||||
import com.wechat.pay.java.core.util.PemUtil;
|
||||
import com.wechat.pay.java.service.payments.app.AppService;
|
||||
import com.wechat.pay.java.service.payments.app.model.PrepayRequest;
|
||||
import com.wechat.pay.java.service.payments.app.model.PrepayResponse;
|
||||
import com.wechat.pay.java.service.payments.h5.H5Service;
|
||||
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
|
||||
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
||||
import com.wechat.pay.java.service.payments.model.Transaction;
|
||||
import com.wechat.pay.java.service.refund.RefundService;
|
||||
import com.wechat.pay.java.service.refund.model.AmountReq;
|
||||
import com.wechat.pay.java.service.refund.model.CreateRequest;
|
||||
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
|
||||
import com.wechat.pay.java.service.refund.model.Refund;
|
||||
import com.wechat.pay.java.service.refund.model.Status;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import org.dromara.pay.domain.vo.PayNotifyResult;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.enums.PayChannelEnum;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.wechat.pay.java.core.http.Constant.*;
|
||||
|
||||
/**
|
||||
* 微信支付策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class WechatPayStrategy implements PayStrategy {
|
||||
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
payStrategyFactory.register(PayChannelEnum.WX_PUB.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.WX_LITE.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.WX_APP.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.WX_NATIVE.getCode(), this);
|
||||
payStrategyFactory.register(PayChannelEnum.WX_H5.getCode(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderSubmitRespVO unifiedOrder(PayOrder order, PayChannel channel) {
|
||||
Config config = getConfig(channel.getConfig());
|
||||
String code = channel.getCode();
|
||||
PayOrderSubmitRespVO resp = new PayOrderSubmitRespVO();
|
||||
resp.setPayOrderNo(order.getNo());
|
||||
resp.setStatus(0); // WAITING
|
||||
|
||||
Map<String, Object> channelConfig = JsonUtils.parseMap(channel.getConfig());
|
||||
String appId = (String) channelConfig.get("appId");
|
||||
String mchId = (String) channelConfig.get("mchId");
|
||||
String privateKeyStr = (String) channelConfig.get("privateKey");
|
||||
String mchSerialNo = (String) channelConfig.get("mchSerialNo");
|
||||
|
||||
try {
|
||||
if (PayChannelEnum.WX_NATIVE.getCode().equals(code)) {
|
||||
NativePayService service = new NativePayService.Builder().config(config).build();
|
||||
com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest request = new com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest();
|
||||
request.setAppid(appId);
|
||||
request.setMchid(mchId);
|
||||
request.setDescription(order.getSubject());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
com.wechat.pay.java.service.payments.nativepay.model.Amount amount = new com.wechat.pay.java.service.payments.nativepay.model.Amount();
|
||||
amount.setTotal(order.getPrice());
|
||||
request.setAmount(amount);
|
||||
|
||||
com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse response = service.prepay(request);
|
||||
resp.setPayData(response.getCodeUrl());
|
||||
|
||||
} else if (PayChannelEnum.WX_APP.getCode().equals(code)) {
|
||||
AppService service = new AppService.Builder().config(config).build();
|
||||
PrepayRequest request = new PrepayRequest();
|
||||
request.setAppid(appId);
|
||||
request.setMchid(mchId);
|
||||
request.setDescription(order.getSubject());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
com.wechat.pay.java.service.payments.app.model.Amount amount = new com.wechat.pay.java.service.payments.app.model.Amount();
|
||||
amount.setTotal(order.getPrice());
|
||||
request.setAmount(amount);
|
||||
|
||||
PrepayResponse response = service.prepay(request);
|
||||
|
||||
// 二次签名
|
||||
PrivateKey privateKey = PemUtil.loadPrivateKeyFromString(privateKeyStr);
|
||||
Map<String, String> signMap = WechatPayUtil.signApp(appId, mchId, response.getPrepayId(), privateKey, mchSerialNo);
|
||||
resp.setPayData(JsonUtils.toJsonString(signMap));
|
||||
|
||||
} else if (PayChannelEnum.WX_PUB.getCode().equals(code) || PayChannelEnum.WX_LITE.getCode().equals(code)) {
|
||||
JsapiService service = new JsapiService.Builder().config(config).build();
|
||||
com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest request = new com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest();
|
||||
request.setAppid(appId);
|
||||
request.setMchid(mchId);
|
||||
request.setDescription(order.getSubject());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
com.wechat.pay.java.service.payments.jsapi.model.Amount amount = new com.wechat.pay.java.service.payments.jsapi.model.Amount();
|
||||
amount.setTotal(order.getPrice());
|
||||
request.setAmount(amount);
|
||||
|
||||
Payer payer = new Payer();
|
||||
payer.setOpenid(order.getChannelUserId()); // 必须传入 openid
|
||||
request.setPayer(payer);
|
||||
|
||||
com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse response = service.prepay(request);
|
||||
|
||||
// 二次签名
|
||||
PrivateKey privateKey = PemUtil.loadPrivateKeyFromString(privateKeyStr);
|
||||
Map<String, String> signMap = WechatPayUtil.signJsapi(appId, response.getPrepayId(), privateKey, mchSerialNo);
|
||||
resp.setPayData(JsonUtils.toJsonString(signMap));
|
||||
|
||||
} else if (PayChannelEnum.WX_H5.getCode().equals(code)) {
|
||||
H5Service service = new H5Service.Builder().config(config).build();
|
||||
com.wechat.pay.java.service.payments.h5.model.PrepayRequest request = new com.wechat.pay.java.service.payments.h5.model.PrepayRequest();
|
||||
request.setAppid(appId);
|
||||
request.setMchid(mchId);
|
||||
request.setDescription(order.getSubject());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
request.setNotifyUrl(order.getNotifyUrl());
|
||||
com.wechat.pay.java.service.payments.h5.model.Amount amount = new com.wechat.pay.java.service.payments.h5.model.Amount();
|
||||
amount.setTotal(order.getPrice());
|
||||
request.setAmount(amount);
|
||||
|
||||
com.wechat.pay.java.service.payments.h5.model.PrepayResponse response = service.prepay(request);
|
||||
resp.setPayData(response.getH5Url());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("微信下单失败", e);
|
||||
throw new ServiceException("微信下单失败: " + e.getMessage());
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean refund(PayRefund refund, PayChannel channel) {
|
||||
Config config = getConfig(channel.getConfig());
|
||||
RefundService service = new RefundService.Builder().config(config).build();
|
||||
|
||||
CreateRequest request = new CreateRequest();
|
||||
request.setOutTradeNo(refund.getOrderNo());
|
||||
request.setOutRefundNo(refund.getNo());
|
||||
request.setReason(refund.getReason());
|
||||
request.setNotifyUrl(refund.getNotifyUrl());
|
||||
|
||||
AmountReq amount = new AmountReq();
|
||||
amount.setRefund(Long.valueOf(refund.getRefundPrice()));
|
||||
amount.setTotal(Long.valueOf(refund.getPayPrice()));
|
||||
amount.setCurrency("CNY");
|
||||
request.setAmount(amount);
|
||||
|
||||
try {
|
||||
Refund response = service.create(request);
|
||||
if (response.getStatus() == Status.SUCCESS || response.getStatus() == Status.PROCESSING) {
|
||||
refund.setChannelRefundNo(response.getRefundId());
|
||||
return true;
|
||||
} else {
|
||||
refund.setErrorMsg(response.getStatus().name());
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("微信退款失败", e);
|
||||
throw new ServiceException("微信退款失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer queryOrder(PayOrder order, PayChannel channel) {
|
||||
Config config = getConfig(channel.getConfig());
|
||||
JsapiService service = new JsapiService.Builder().config(config).build(); // Query is generic across services usually, using JsapiService for access
|
||||
|
||||
com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest request = new com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest();
|
||||
request.setMchid(getConfigMap(channel.getConfig()).get("mchId").toString());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
|
||||
try {
|
||||
Transaction transaction = service.queryOrderByOutTradeNo(request);
|
||||
if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
|
||||
return PayOrderStatusEnum.SUCCESS.getStatus();
|
||||
} else if (transaction.getTradeState() == Transaction.TradeStateEnum.CLOSED || transaction.getTradeState() == Transaction.TradeStateEnum.REVOKED) {
|
||||
return PayOrderStatusEnum.CLOSED.getStatus();
|
||||
} else if (transaction.getTradeState() == Transaction.TradeStateEnum.REFUND) {
|
||||
return PayOrderStatusEnum.REFUND.getStatus();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("微信查单失败", e);
|
||||
}
|
||||
return PayOrderStatusEnum.WAITING.getStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean closeOrder(PayOrder order, PayChannel channel) {
|
||||
Config config = getConfig(channel.getConfig());
|
||||
JsapiService service = new JsapiService.Builder().config(config).build();
|
||||
CloseOrderRequest request = new CloseOrderRequest();
|
||||
request.setMchid(getConfigMap(channel.getConfig()).get("mchId").toString());
|
||||
request.setOutTradeNo(order.getNo());
|
||||
try {
|
||||
service.closeOrder(request);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("微信关单失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer queryRefund(PayRefund refund, PayChannel channel) {
|
||||
Config config = getConfig(channel.getConfig());
|
||||
RefundService service = new RefundService.Builder().config(config).build();
|
||||
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
|
||||
request.setOutRefundNo(refund.getNo());
|
||||
try {
|
||||
Refund response = service.queryByOutRefundNo(request);
|
||||
if (response.getStatus() == Status.SUCCESS) {
|
||||
return 10; // SUCCESS
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("微信退款查询失败", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayNotifyResult processNotify(HttpServletRequest request, PayChannel channel) {
|
||||
PayNotifyResult result = new PayNotifyResult();
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
|
||||
StringBuilder stringBuffer = new StringBuilder();
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
String requestBody = stringBuffer.toString();
|
||||
|
||||
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
|
||||
String nonce = request.getHeader(WECHAT_PAY_NONCE);
|
||||
String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
|
||||
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
|
||||
|
||||
RequestParam requestParam = new RequestParam.Builder()
|
||||
.serialNumber(serialNo)
|
||||
.nonce(nonce)
|
||||
.signature(signature)
|
||||
.timestamp(timestamp)
|
||||
.body(requestBody)
|
||||
.build();
|
||||
|
||||
// Config config = getConfig(channel.getConfig());
|
||||
// NotificationParser parser = new NotificationParser((NotificationConfig) config); // RSAAutoCertificateConfig implements NotificationConfig
|
||||
|
||||
// Re-create config here or cast
|
||||
NotificationConfig config = (NotificationConfig) getConfig(channel.getConfig());
|
||||
NotificationParser parser = new NotificationParser(config);
|
||||
|
||||
Transaction transaction = parser.parse(requestParam, Transaction.class);
|
||||
if (transaction != null && transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
|
||||
result.setSuccess(true);
|
||||
result.setOrderNo(transaction.getOutTradeNo());
|
||||
result.setTradeNo(transaction.getTransactionId());
|
||||
// 微信支付V3回调成功需要返回 HTTP 200 或 204,无特定body内容,但建议返回空JSON或success
|
||||
// Spring MVC controller usually returns body.
|
||||
// We should return a structure that controller can map to ResponseEntity
|
||||
result.setResponseBody("{\"code\": \"SUCCESS\", \"message\": \"SUCCESS\"}");
|
||||
} else {
|
||||
result.setSuccess(false);
|
||||
result.setResponseBody("{\"code\": \"FAIL\", \"message\": \"FAIL\"}");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("微信回调处理异常", e);
|
||||
result.setSuccess(false);
|
||||
result.setResponseBody("{\"code\": \"FAIL\", \"message\": \"EXCEPTION\"}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Config getConfig(String configJson) {
|
||||
Map<String, Object> configMap = JsonUtils.parseMap(configJson);
|
||||
return new RSAAutoCertificateConfig.Builder()
|
||||
.merchantId((String) configMap.get("mchId"))
|
||||
.privateKey((String) configMap.get("privateKey"))
|
||||
.merchantSerialNumber((String) configMap.get("mchSerialNo"))
|
||||
.apiV3Key((String) configMap.get("apiV3Key"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> getConfigMap(String configJson) {
|
||||
return JsonUtils.parseMap(configJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.dromara.pay.core.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.wechat.pay.java.core.cipher.RSASigner;
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付工具类
|
||||
*/
|
||||
public class WechatPayUtil {
|
||||
|
||||
public static Map<String, String> signApp(String appId, String mchId, String prepayId, PrivateKey privateKey, String certificateSerialNo) {
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String nonceStr = RandomUtil.randomString(32);
|
||||
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
|
||||
|
||||
RSASigner signer = new RSASigner(certificateSerialNo, privateKey);
|
||||
String sign = signer.sign(message).getSign();
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("appid", appId);
|
||||
map.put("partnerid", mchId);
|
||||
map.put("prepayid", prepayId);
|
||||
map.put("package", "Sign=WXPay");
|
||||
map.put("noncestr", nonceStr);
|
||||
map.put("timestamp", String.valueOf(timestamp));
|
||||
map.put("sign", sign);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static Map<String, String> signJsapi(String appId, String prepayId, PrivateKey privateKey, String certificateSerialNo) {
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String nonceStr = RandomUtil.randomString(32);
|
||||
String packageStr = "prepay_id=" + prepayId;
|
||||
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + packageStr + "\n";
|
||||
|
||||
RSASigner signer = new RSASigner(certificateSerialNo, privateKey);
|
||||
String sign = signer.sign(message).getSign();
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("appId", appId);
|
||||
map.put("timeStamp", String.valueOf(timestamp));
|
||||
map.put("nonceStr", nonceStr);
|
||||
map.put("package", packageStr);
|
||||
map.put("signType", "RSA");
|
||||
map.put("paySign", sign);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.dromara.pay.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付应用对象 pay_app
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@TableName("pay_app")
|
||||
public class PayApp {
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 支付结果回调地址
|
||||
*/
|
||||
private String payNotifyUrl;
|
||||
|
||||
/**
|
||||
* 退款结果回调地址
|
||||
*/
|
||||
private String refundNotifyUrl;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.dromara.pay.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付渠道对象 pay_channel
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@TableName("pay_channel")
|
||||
public class PayChannel {
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 渠道编码(alipay_pc, wx_pub)
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 渠道费率(%)
|
||||
*/
|
||||
private Double feeRate;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 支付渠道配置(JSON)
|
||||
*/
|
||||
private String config;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.dromara.pay.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付通知任务对象 pay_notify_task
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@TableName("pay_notify_task")
|
||||
public class PayNotifyTask {
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 订单号
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 通知内容
|
||||
*/
|
||||
private String notifyBody;
|
||||
|
||||
/**
|
||||
* 重试次数
|
||||
*/
|
||||
private Integer retryCount;
|
||||
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
private Integer maxRetry;
|
||||
|
||||
/**
|
||||
* 状态(0进行中 1成功 2失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 下次通知时间
|
||||
*/
|
||||
private Date nextNotifyTime;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.dromara.pay.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付订单对象 pay_order
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@TableName(value = "pay_order", autoResultMap = true)
|
||||
public class PayOrder {
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 商品描述
|
||||
*/
|
||||
private String body;
|
||||
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 渠道费率
|
||||
*/
|
||||
private Double channelFeeRate;
|
||||
|
||||
/**
|
||||
* 渠道手续费(分)
|
||||
*/
|
||||
private Integer channelFeePrice;
|
||||
|
||||
/**
|
||||
* 状态(0未支付 10已支付 20已退款 30已关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 订单失效时间
|
||||
*/
|
||||
private Date expireTime;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 拓展业务ID (如扫码支付的记录ID)
|
||||
*/
|
||||
private Long extensionId;
|
||||
|
||||
/**
|
||||
* 支付单号(内部生成)
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 渠道订单号(微信/支付宝)
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道用户OpenId
|
||||
*/
|
||||
private String channelUserId;
|
||||
|
||||
/**
|
||||
* 渠道额外参数 (存储微信openid, 支付宝buyer_id等)
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> channelExtras;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package org.dromara.pay.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 退款订单对象 pay_refund
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@TableName("pay_refund")
|
||||
public class PayRefund {
|
||||
|
||||
/**
|
||||
* 退款ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 退款单号
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 支付订单ID
|
||||
*/
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 支付订单no
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商户退款编号
|
||||
*/
|
||||
private String merchantRefundId;
|
||||
|
||||
/**
|
||||
* 回调地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 状态(0等待 10成功 20失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
private Integer payPrice;
|
||||
|
||||
/**
|
||||
* 退款金额(分)
|
||||
*/
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 渠道订单号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道退款单号
|
||||
*/
|
||||
private String channelRefundNo;
|
||||
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 渠道错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 渠道错误描述
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.dromara.pay.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.pay.domain.PayApp;
|
||||
|
||||
/**
|
||||
* 支付应用业务对象 pay_app
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@AutoMapper(target = PayApp.class, reverseConvertGenerate = false)
|
||||
public class PayAppBo {
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotBlank(message = "应用名不能为空")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 支付结果回调地址
|
||||
*/
|
||||
@NotBlank(message = "支付结果回调地址不能为空")
|
||||
private String payNotifyUrl;
|
||||
|
||||
/**
|
||||
* 退款结果回调地址
|
||||
*/
|
||||
@NotBlank(message = "退款结果回调地址不能为空")
|
||||
private String refundNotifyUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.dromara.pay.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
|
||||
/**
|
||||
* 支付渠道业务对象 pay_channel
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@AutoMapper(target = PayChannel.class, reverseConvertGenerate = false)
|
||||
public class PayChannelBo {
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 渠道编码(alipay_pc, wx_pub)
|
||||
*/
|
||||
@NotBlank(message = "渠道编码不能为空")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 渠道费率(%)
|
||||
*/
|
||||
@NotNull(message = "渠道费率不能为空")
|
||||
private Double feeRate;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotNull(message = "应用ID不能为空")
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 支付渠道配置(JSON)
|
||||
*/
|
||||
@NotBlank(message = "支付渠道配置不能为空")
|
||||
private String config;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.dromara.pay.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付订单业务对象 pay_order
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@AutoMapper(target = PayOrder.class, reverseConvertGenerate = false)
|
||||
public class PayOrderBo {
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotNull(message = "应用ID不能为空")
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 商品描述
|
||||
*/
|
||||
private String body;
|
||||
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 渠道费率
|
||||
*/
|
||||
private Double channelFeeRate;
|
||||
|
||||
/**
|
||||
* 渠道手续费(分)
|
||||
*/
|
||||
private Integer channelFeePrice;
|
||||
|
||||
/**
|
||||
* 状态(0未支付 10已支付 20已退款 30已关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 订单失效时间
|
||||
*/
|
||||
private Date expireTime;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 拓展业务ID (如扫码支付的记录ID)
|
||||
*/
|
||||
private Long extensionId;
|
||||
|
||||
/**
|
||||
* 支付单号(内部生成)
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 渠道订单号(微信/支付宝)
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道用户OpenId
|
||||
*/
|
||||
private String channelUserId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.dromara.pay.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 退款订单业务对象 pay_refund
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@AutoMapper(target = PayRefund.class, reverseConvertGenerate = false)
|
||||
public class PayRefundBo {
|
||||
|
||||
/**
|
||||
* 退款ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 退款单号
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotNull(message = "应用ID不能为空")
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 支付订单ID
|
||||
*/
|
||||
@NotNull(message = "支付订单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 支付订单no
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商户退款编号
|
||||
*/
|
||||
private String merchantRefundId;
|
||||
|
||||
/**
|
||||
* 回调地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 状态(0等待 10成功 20失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
private Integer payPrice;
|
||||
|
||||
/**
|
||||
* 退款金额(分)
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 渠道订单号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道退款单号
|
||||
*/
|
||||
private String channelRefundNo;
|
||||
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 渠道错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 渠道错误描述
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.pay.domain.PayApp;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付应用视图对象 pay_app
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = PayApp.class)
|
||||
public class PayAppVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 支付结果回调地址
|
||||
*/
|
||||
private String payNotifyUrl;
|
||||
|
||||
/**
|
||||
* 退款结果回调地址
|
||||
*/
|
||||
private String refundNotifyUrl;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付渠道视图对象 pay_channel
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = PayChannel.class)
|
||||
public class PayChannelVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 渠道编码(alipay_pc, wx_pub)
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态(0开启 1关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 渠道费率(%)
|
||||
*/
|
||||
private Double feeRate;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 支付渠道配置(JSON)
|
||||
*/
|
||||
private String config;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 支付回调处理结果
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PayNotifyResult {
|
||||
/**
|
||||
* 验签是否通过
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 系统内部订单号
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 渠道交易单号
|
||||
*/
|
||||
private String tradeNo;
|
||||
|
||||
/**
|
||||
* 返回给渠道的响应内容 (例如 "success" 或 XML)
|
||||
*/
|
||||
private String responseBody;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 支付订单提交响应 VO
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
public class PayOrderSubmitRespVO implements Serializable {
|
||||
|
||||
/**
|
||||
* 支付单号
|
||||
*/
|
||||
private String payOrderNo;
|
||||
|
||||
/**
|
||||
* 调起支付的参数 (JSON格式,或URL)
|
||||
*/
|
||||
private String payData;
|
||||
|
||||
/**
|
||||
* 支付状态 (10: 成功, 0: 等待)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 渠道返回的原始数据
|
||||
*/
|
||||
private Object rawData;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付订单视图对象 pay_order
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = PayOrder.class)
|
||||
public class PayOrderVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 商品描述
|
||||
*/
|
||||
private String body;
|
||||
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 渠道费率
|
||||
*/
|
||||
private Double channelFeeRate;
|
||||
|
||||
/**
|
||||
* 渠道手续费(分)
|
||||
*/
|
||||
private Integer channelFeePrice;
|
||||
|
||||
/**
|
||||
* 状态(0未支付 10已支付 20已退款 30已关闭)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 订单失效时间
|
||||
*/
|
||||
private Date expireTime;
|
||||
|
||||
/**
|
||||
* 支付成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 拓展业务ID (如扫码支付的记录ID)
|
||||
*/
|
||||
private Long extensionId;
|
||||
|
||||
/**
|
||||
* 支付单号(内部生成)
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 渠道订单号(微信/支付宝)
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道用户OpenId
|
||||
*/
|
||||
private String channelUserId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.dromara.pay.domain.vo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 退款订单视图对象 pay_refund
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = PayRefund.class)
|
||||
public class PayRefundVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 退款ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 退款单号
|
||||
*/
|
||||
private String no;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 渠道ID
|
||||
*/
|
||||
private Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
private String channelCode;
|
||||
|
||||
/**
|
||||
* 支付订单ID
|
||||
*/
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 支付订单no
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商户退款编号
|
||||
*/
|
||||
private String merchantRefundId;
|
||||
|
||||
/**
|
||||
* 回调地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 状态(0等待 10成功 20失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 支付金额(分)
|
||||
*/
|
||||
private Integer payPrice;
|
||||
|
||||
/**
|
||||
* 退款金额(分)
|
||||
*/
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 渠道订单号
|
||||
*/
|
||||
private String channelOrderNo;
|
||||
|
||||
/**
|
||||
* 渠道退款单号
|
||||
*/
|
||||
private String channelRefundNo;
|
||||
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private Date successTime;
|
||||
|
||||
/**
|
||||
* 渠道错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 渠道错误描述
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.dromara.pay.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付渠道枚举
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayChannelEnum {
|
||||
|
||||
WX_PUB("wx_pub", "微信 JSAPI 支付"),
|
||||
WX_LITE("wx_lite", "微信小程序支付"),
|
||||
WX_APP("wx_app", "微信 App 支付"),
|
||||
WX_NATIVE("wx_native", "微信 Native 支付"),
|
||||
WX_H5("wx_h5", "微信 H5 支付"),
|
||||
ALIPAY_PC("alipay_pc", "支付宝 PC 支付"),
|
||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 支付"),
|
||||
ALIPAY_APP("alipay_app", "支付宝 App 支付"),
|
||||
ALIPAY_QR("alipay_qr", "支付宝扫码支付"),
|
||||
MOCK("mock", "模拟支付"),
|
||||
WALLET("wallet", "钱包支付");
|
||||
|
||||
private final String code;
|
||||
private final String name;
|
||||
|
||||
public static PayChannelEnum getByCode(String code) {
|
||||
for (PayChannelEnum value : values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.dromara.pay.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付订单状态枚举
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayOrderStatusEnum {
|
||||
|
||||
WAITING(0, "未支付"),
|
||||
SUCCESS(10, "支付成功"),
|
||||
REFUND(20, "已退款"),
|
||||
CLOSED(30, "已关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.dromara.pay.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 退款状态枚举
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RefundStatusEnum {
|
||||
|
||||
WAITING(0, "等待"),
|
||||
SUCCESS(10, "成功"),
|
||||
FAIL(20, "失败");
|
||||
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
|
||||
public static RefundStatusEnum getByCode(Integer code) {
|
||||
for (RefundStatusEnum value : values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.dromara.pay.event;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 支付成功事件
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Getter
|
||||
public class PaySuccessEvent extends ApplicationEvent {
|
||||
|
||||
private final PayOrder order;
|
||||
|
||||
public PaySuccessEvent(Object source, PayOrder order) {
|
||||
super(source);
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.dromara.pay.job;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.pay.domain.PayNotifyTask;
|
||||
import org.dromara.pay.service.PayNotifyService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付通知重试任务
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class PayNotifyJob {
|
||||
|
||||
private final PayNotifyService payNotifyService;
|
||||
|
||||
/**
|
||||
* 每30秒检查一次待通知任务
|
||||
*/
|
||||
@Scheduled(cron = "0/30 * * * * ?")
|
||||
public void retryNotify() {
|
||||
List<PayNotifyTask> tasks = payNotifyService.getPendingTasks();
|
||||
if (tasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("扫描到待执行回调任务: {}个", tasks.size());
|
||||
for (PayNotifyTask task : tasks) {
|
||||
payNotifyService.executeTask(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.dromara.pay.job;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.dromara.pay.mapper.PayChannelMapper;
|
||||
import org.dromara.pay.mapper.PayOrderMapper;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付订单过期处理任务
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class PayOrderExpireJob {
|
||||
|
||||
private final PayOrderMapper payOrderMapper;
|
||||
private final PayChannelMapper payChannelMapper;
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
|
||||
/**
|
||||
* 每分钟检查一次过期订单
|
||||
*/
|
||||
@Scheduled(cron = "0 0/1 * * * ?")
|
||||
public void expireOrder() {
|
||||
// 查询未支付且已过期的订单
|
||||
Date now = new Date();
|
||||
List<PayOrder> expiredOrders = payOrderMapper.selectList(new LambdaQueryWrapper<PayOrder>()
|
||||
.eq(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus())
|
||||
.lt(PayOrder::getExpireTime, now)
|
||||
.last("LIMIT 100")); // 每次处理100条,防止积压
|
||||
|
||||
for (PayOrder order : expiredOrders) {
|
||||
try {
|
||||
log.info("处理过期订单: {}", order.getNo());
|
||||
PayChannel channel = payChannelMapper.selectById(order.getChannelId());
|
||||
if (channel != null) {
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channel.getCode());
|
||||
// 调用三方关单
|
||||
strategy.closeOrder(order, channel);
|
||||
}
|
||||
|
||||
// 更新本地状态为已关闭
|
||||
order.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
|
||||
payOrderMapper.updateById(order);
|
||||
} catch (Exception e) {
|
||||
log.error("处理过期订单异常: {}", order.getNo(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.dromara.pay.job;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.dromara.pay.mapper.PayChannelMapper;
|
||||
import org.dromara.pay.mapper.PayOrderMapper;
|
||||
import org.dromara.pay.service.PayOrderService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付订单同步任务 (防掉单)
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class PayOrderSyncJob {
|
||||
|
||||
private final PayOrderMapper payOrderMapper;
|
||||
private final PayChannelMapper payChannelMapper;
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
private final PayOrderService payOrderService;
|
||||
|
||||
/**
|
||||
* 每2分钟检查一次"支付中"的订单 (防掉单)
|
||||
* 针对创建时间超过2分钟,且未过期的订单
|
||||
*/
|
||||
@Scheduled(cron = "0 0/2 * * * ?")
|
||||
public void syncOrder() {
|
||||
Date now = new Date();
|
||||
Date twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000);
|
||||
|
||||
// 查询 WAITING 状态,创建时间 < 2分钟前,过期时间 > 现在的订单
|
||||
List<PayOrder> waitingOrders = payOrderMapper.selectList(new LambdaQueryWrapper<PayOrder>()
|
||||
.eq(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus())
|
||||
.lt(PayOrder::getCreateTime, twoMinutesAgo)
|
||||
.gt(PayOrder::getExpireTime, now)
|
||||
.last("LIMIT 100"));
|
||||
|
||||
for (PayOrder order : waitingOrders) {
|
||||
try {
|
||||
log.info("同步支付订单状态: {}", order.getNo());
|
||||
PayChannel channel = payChannelMapper.selectById(order.getChannelId());
|
||||
if (channel == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channel.getCode());
|
||||
Integer status = strategy.queryOrder(order, channel);
|
||||
|
||||
if (PayOrderStatusEnum.SUCCESS.getStatus().equals(status)) {
|
||||
log.info("同步发现订单已支付: {}", order.getNo());
|
||||
// 触发支付成功逻辑
|
||||
payOrderService.notifyOrderSuccess(order.getNo(), null);
|
||||
} else if (PayOrderStatusEnum.CLOSED.getStatus().equals(status)) {
|
||||
log.info("同步发现订单已关闭: {}", order.getNo());
|
||||
order.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
|
||||
payOrderMapper.updateById(order);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("同步订单异常: {}", order.getNo(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.dromara.pay.listener;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.pay.event.PaySuccessEvent;
|
||||
import org.dromara.pay.service.PayNotifyService;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 支付订单事件监听
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class PayOrderListener {
|
||||
|
||||
private final PayNotifyService payNotifyService;
|
||||
|
||||
/**
|
||||
* 监听支付成功事件,创建回调任务
|
||||
*/
|
||||
@Async
|
||||
@EventListener
|
||||
public void onPaySuccess(PaySuccessEvent event) {
|
||||
log.info("监听到支付成功事件: orderNo={}", event.getOrder().getNo());
|
||||
try {
|
||||
payNotifyService.createTask(event.getOrder());
|
||||
} catch (Exception e) {
|
||||
log.error("创建支付回调任务失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.dromara.pay.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pay.domain.PayApp;
|
||||
import org.dromara.pay.domain.vo.PayAppVo;
|
||||
|
||||
/**
|
||||
* 支付应用Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Mapper
|
||||
public interface PayAppMapper extends BaseMapperPlus<PayApp, PayAppVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.dromara.pay.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.vo.PayChannelVo;
|
||||
|
||||
/**
|
||||
* 支付渠道Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Mapper
|
||||
public interface PayChannelMapper extends BaseMapperPlus<PayChannel, PayChannelVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.dromara.pay.mapper;
|
||||
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pay.domain.PayNotifyTask;
|
||||
|
||||
/**
|
||||
* 支付通知任务Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayNotifyTaskMapper extends BaseMapperPlus<PayNotifyTask, PayNotifyTask> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.dromara.pay.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.vo.PayOrderVo;
|
||||
|
||||
/**
|
||||
* 支付订单Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Mapper
|
||||
public interface PayOrderMapper extends BaseMapperPlus<PayOrder, PayOrderVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.dromara.pay.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import org.dromara.pay.domain.vo.PayRefundVo;
|
||||
|
||||
/**
|
||||
* 退款订单Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Mapper
|
||||
public interface PayRefundMapper extends BaseMapperPlus<PayRefund, PayRefundVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.dromara.pay.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.bo.PayAppBo;
|
||||
import org.dromara.pay.domain.vo.PayAppVo;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付应用Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayAppService {
|
||||
|
||||
/**
|
||||
* 查询支付应用
|
||||
*/
|
||||
PayAppVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询支付应用列表
|
||||
*/
|
||||
TableDataInfo<PayAppVo> queryPageList(PayAppBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询支付应用列表
|
||||
*/
|
||||
List<PayAppVo> queryList(PayAppBo bo);
|
||||
|
||||
/**
|
||||
* 新增支付应用
|
||||
*/
|
||||
Boolean insertByBo(PayAppBo bo);
|
||||
|
||||
/**
|
||||
* 修改支付应用
|
||||
*/
|
||||
Boolean updateByBo(PayAppBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除支付应用信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.dromara.pay.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.bo.PayChannelBo;
|
||||
import org.dromara.pay.domain.vo.PayChannelVo;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付渠道Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayChannelService {
|
||||
|
||||
/**
|
||||
* 查询支付渠道
|
||||
*/
|
||||
PayChannelVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询支付渠道列表
|
||||
*/
|
||||
TableDataInfo<PayChannelVo> queryPageList(PayChannelBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询支付渠道列表
|
||||
*/
|
||||
List<PayChannelVo> queryList(PayChannelBo bo);
|
||||
|
||||
/**
|
||||
* 新增支付渠道
|
||||
*/
|
||||
Boolean insertByBo(PayChannelBo bo);
|
||||
|
||||
/**
|
||||
* 修改支付渠道
|
||||
*/
|
||||
Boolean updateByBo(PayChannelBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除支付渠道信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.dromara.pay.service;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.pay.domain.PayNotifyTask;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.mapper.PayNotifyTaskMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付通知服务
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PayNotifyService {
|
||||
|
||||
private final PayNotifyTaskMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 创建通知任务
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createTask(PayOrder order) {
|
||||
if (StringUtils.isBlank(order.getNotifyUrl())) {
|
||||
return;
|
||||
}
|
||||
PayNotifyTask task = new PayNotifyTask();
|
||||
task.setOrderId(order.getId());
|
||||
task.setOrderNo(order.getNo());
|
||||
task.setNotifyUrl(order.getNotifyUrl());
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("orderNo", order.getNo());
|
||||
body.put("merchantOrderId", order.getMerchantOrderId());
|
||||
body.put("status", order.getStatus());
|
||||
body.put("price", order.getPrice());
|
||||
body.put("channelUserId", order.getChannelUserId());
|
||||
body.put("successTime", order.getSuccessTime());
|
||||
body.put("extensionId", order.getExtensionId());
|
||||
body.put("channelExtras", order.getChannelExtras());
|
||||
|
||||
task.setNotifyBody(JsonUtils.toJsonString(body));
|
||||
task.setRetryCount(0);
|
||||
task.setMaxRetry(5);
|
||||
task.setStatus(0); // 进行中
|
||||
task.setNextNotifyTime(new Date()); // 立即执行
|
||||
|
||||
baseMapper.insert(task);
|
||||
|
||||
// 尝试立即执行一次
|
||||
executeTask(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行通知任务
|
||||
*/
|
||||
public void executeTask(PayNotifyTask task) {
|
||||
try {
|
||||
log.info("开始执行支付回调通知: taskId={}, url={}", task.getId(), task.getNotifyUrl());
|
||||
String result = HttpUtil.post(task.getNotifyUrl(), task.getNotifyBody(), 5000);
|
||||
log.info("支付回调通知响应: taskId={}, result={}", task.getId(), result);
|
||||
|
||||
// 判断成功:返回 "success" 或 JSON格式 {"code": 200}
|
||||
boolean success = false;
|
||||
if (StringUtils.isNotBlank(result)) {
|
||||
String trimmed = result.trim();
|
||||
if ("success".equalsIgnoreCase(trimmed)) {
|
||||
success = true;
|
||||
} else {
|
||||
// 尝试解析JSON
|
||||
try {
|
||||
Map<String, Object> jsonResult = JsonUtils.parseMap(trimmed);
|
||||
Object code = jsonResult.get("code");
|
||||
if (code != null) {
|
||||
if (code instanceof Number && ((Number) code).intValue() == 200) {
|
||||
success = true;
|
||||
} else if ("200".equals(String.valueOf(code))) {
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 不是JSON格式,忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
task.setStatus(1); // 成功
|
||||
baseMapper.updateById(task);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("支付回调通知异常: taskId={}", task.getId(), e);
|
||||
}
|
||||
|
||||
// 失败处理
|
||||
task.setRetryCount(task.getRetryCount() + 1);
|
||||
if (task.getRetryCount() >= task.getMaxRetry()) {
|
||||
task.setStatus(2); // 失败
|
||||
} else {
|
||||
// 计算下次重试时间:指数退避
|
||||
// 15s, 30s, 1m, 2m, 5m
|
||||
int delaySeconds = 15 * (1 << (task.getRetryCount() - 1));
|
||||
task.setNextNotifyTime(new Date(System.currentTimeMillis() + delaySeconds * 1000L));
|
||||
}
|
||||
baseMapper.updateById(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待执行的任务
|
||||
*/
|
||||
public List<PayNotifyTask> getPendingTasks() {
|
||||
return baseMapper.selectList(new LambdaQueryWrapper<PayNotifyTask>()
|
||||
.eq(PayNotifyTask::getStatus, 0)
|
||||
.le(PayNotifyTask::getNextNotifyTime, new Date())
|
||||
.last("LIMIT 100"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.dromara.pay.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.bo.PayOrderBo;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.domain.vo.PayOrderVo;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付订单Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayOrderService {
|
||||
|
||||
/**
|
||||
* 查询支付订单
|
||||
*/
|
||||
PayOrderVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
TableDataInfo<PayOrderVo> queryPageList(PayOrderBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
List<PayOrderVo> queryList(PayOrderBo bo);
|
||||
|
||||
/**
|
||||
* 新增支付订单
|
||||
*/
|
||||
Boolean insertByBo(PayOrderBo bo);
|
||||
|
||||
/**
|
||||
* 修改支付订单
|
||||
*/
|
||||
Boolean updateByBo(PayOrderBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除支付订单信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 提交支付订单
|
||||
*/
|
||||
PayOrderSubmitRespVO submitOrder(PayOrderBo bo);
|
||||
|
||||
/**
|
||||
* 支付成功回调通知
|
||||
*/
|
||||
void notifyOrderSuccess(String orderNo, String channelOrderNo);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.dromara.pay.service;
|
||||
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.bo.PayRefundBo;
|
||||
import org.dromara.pay.domain.vo.PayRefundVo;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 退款订单Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface PayRefundService {
|
||||
|
||||
/**
|
||||
* 查询退款订单
|
||||
*/
|
||||
PayRefundVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
TableDataInfo<PayRefundVo> queryPageList(PayRefundBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
List<PayRefundVo> queryList(PayRefundBo bo);
|
||||
|
||||
/**
|
||||
* 新增退款订单
|
||||
*/
|
||||
Boolean insertByBo(PayRefundBo bo);
|
||||
|
||||
/**
|
||||
* 修改退款订单
|
||||
*/
|
||||
Boolean updateByBo(PayRefundBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除退款订单信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 创建并执行退款
|
||||
*/
|
||||
Boolean createRefund(PayRefundBo bo);
|
||||
|
||||
/**
|
||||
* 处理退款回调成功
|
||||
*/
|
||||
void notifyRefundSuccess(String refundNo, String channelRefundNo);
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package org.dromara.pay.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.PayApp;
|
||||
import org.dromara.pay.domain.bo.PayAppBo;
|
||||
import org.dromara.pay.domain.vo.PayAppVo;
|
||||
import org.dromara.pay.mapper.PayAppMapper;
|
||||
import org.dromara.pay.service.PayAppService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付应用Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PayAppServiceImpl implements PayAppService {
|
||||
|
||||
private final PayAppMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询支付应用
|
||||
*/
|
||||
@Override
|
||||
public PayAppVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付应用列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<PayAppVo> queryPageList(PayAppBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<PayApp> lqw = buildQueryWrapper(bo);
|
||||
Page<PayAppVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付应用列表
|
||||
*/
|
||||
@Override
|
||||
public List<PayAppVo> queryList(PayAppBo bo) {
|
||||
LambdaQueryWrapper<PayApp> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<PayApp> buildQueryWrapper(PayAppBo bo) {
|
||||
LambdaQueryWrapper<PayApp> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getName()), PayApp::getName, bo.getName());
|
||||
lqw.eq(bo.getStatus() != null, PayApp::getStatus, bo.getStatus());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付应用
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(PayAppBo bo) {
|
||||
PayApp add = MapstructUtils.convert(bo, PayApp.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付应用
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(PayAppBo bo) {
|
||||
PayApp update = MapstructUtils.convert(bo, PayApp.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(PayApp entity){
|
||||
// 校验应用名称不能为空
|
||||
if (StringUtils.isBlank(entity.getName())) {
|
||||
throw new ServiceException("应用名称不能为空");
|
||||
}
|
||||
// 校验回调地址格式
|
||||
if (StringUtils.isNotBlank(entity.getPayNotifyUrl())) {
|
||||
if (!entity.getPayNotifyUrl().startsWith("http://") && !entity.getPayNotifyUrl().startsWith("https://")) {
|
||||
throw new ServiceException("支付回调地址格式不正确");
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(entity.getRefundNotifyUrl())) {
|
||||
if (!entity.getRefundNotifyUrl().startsWith("http://") && !entity.getRefundNotifyUrl().startsWith("https://")) {
|
||||
throw new ServiceException("退款回调地址格式不正确");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除支付应用
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
// 校验应用是否有关联的支付渠道
|
||||
for (Long id : ids) {
|
||||
// 这里可以添加检查是否有关联的支付渠道或订单
|
||||
// 暂时只检查应用是否存在
|
||||
PayApp app = baseMapper.selectById(id);
|
||||
if (app == null) {
|
||||
throw new ServiceException("支付应用不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package org.dromara.pay.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.bo.PayChannelBo;
|
||||
import org.dromara.pay.domain.vo.PayChannelVo;
|
||||
import org.dromara.pay.mapper.PayChannelMapper;
|
||||
import org.dromara.pay.service.PayChannelService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付渠道Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PayChannelServiceImpl implements PayChannelService {
|
||||
|
||||
private final PayChannelMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询支付渠道
|
||||
*/
|
||||
@Override
|
||||
public PayChannelVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付渠道列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<PayChannelVo> queryPageList(PayChannelBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<PayChannel> lqw = buildQueryWrapper(bo);
|
||||
Page<PayChannelVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付渠道列表
|
||||
*/
|
||||
@Override
|
||||
public List<PayChannelVo> queryList(PayChannelBo bo) {
|
||||
LambdaQueryWrapper<PayChannel> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<PayChannel> buildQueryWrapper(PayChannelBo bo) {
|
||||
LambdaQueryWrapper<PayChannel> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getCode()), PayChannel::getCode, bo.getCode());
|
||||
lqw.eq(bo.getStatus() != null, PayChannel::getStatus, bo.getStatus());
|
||||
lqw.eq(bo.getAppId() != null, PayChannel::getAppId, bo.getAppId());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付渠道
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(PayChannelBo bo) {
|
||||
PayChannel add = MapstructUtils.convert(bo, PayChannel.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付渠道
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(PayChannelBo bo) {
|
||||
PayChannel update = MapstructUtils.convert(bo, PayChannel.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(PayChannel entity){
|
||||
// 校验渠道编码不能为空
|
||||
if (StringUtils.isBlank(entity.getCode())) {
|
||||
throw new ServiceException("渠道编码不能为空");
|
||||
}
|
||||
// 校验渠道编码唯一性(同一应用下)
|
||||
if (entity.getAppId() != null) {
|
||||
LambdaQueryWrapper<PayChannel> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PayChannel::getAppId, entity.getAppId())
|
||||
.eq(PayChannel::getCode, entity.getCode());
|
||||
if (entity.getId() != null) {
|
||||
wrapper.ne(PayChannel::getId, entity.getId());
|
||||
}
|
||||
Long count = baseMapper.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new ServiceException("该应用下渠道编码已存在");
|
||||
}
|
||||
}
|
||||
// 校验配置JSON格式
|
||||
if (StringUtils.isNotBlank(entity.getConfig())) {
|
||||
try {
|
||||
org.dromara.common.json.utils.JsonUtils.parseMap(entity.getConfig());
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("渠道配置JSON格式不正确");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除支付渠道
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
// 校验渠道是否存在
|
||||
for (Long id : ids) {
|
||||
PayChannel channel = baseMapper.selectById(id);
|
||||
if (channel == null) {
|
||||
throw new ServiceException("支付渠道不存在");
|
||||
}
|
||||
// 可以添加检查是否有关联的订单
|
||||
}
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package org.dromara.pay.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayApp;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.bo.PayOrderBo;
|
||||
import org.dromara.pay.domain.vo.PayOrderSubmitRespVO;
|
||||
import org.dromara.pay.domain.vo.PayOrderVo;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.dromara.pay.event.PaySuccessEvent;
|
||||
import org.dromara.pay.mapper.PayAppMapper;
|
||||
import org.dromara.pay.mapper.PayChannelMapper;
|
||||
import org.dromara.pay.mapper.PayOrderMapper;
|
||||
import org.dromara.pay.service.PayOrderService;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支付订单Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PayOrderServiceImpl implements PayOrderService {
|
||||
|
||||
private final PayOrderMapper baseMapper;
|
||||
private final PayAppMapper payAppMapper;
|
||||
private final PayChannelMapper payChannelMapper;
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/**
|
||||
* 查询支付订单
|
||||
*/
|
||||
@Override
|
||||
public PayOrderVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<PayOrderVo> queryPageList(PayOrderBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<PayOrder> lqw = buildQueryWrapper(bo);
|
||||
Page<PayOrderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单列表
|
||||
*/
|
||||
@Override
|
||||
public List<PayOrderVo> queryList(PayOrderBo bo) {
|
||||
LambdaQueryWrapper<PayOrder> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<PayOrder> buildQueryWrapper(PayOrderBo bo) {
|
||||
LambdaQueryWrapper<PayOrder> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(bo.getAppId() != null, PayOrder::getAppId, bo.getAppId());
|
||||
lqw.eq(bo.getChannelId() != null, PayOrder::getChannelId, bo.getChannelId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getChannelCode()), PayOrder::getChannelCode, bo.getChannelCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMerchantOrderId()), PayOrder::getMerchantOrderId, bo.getMerchantOrderId());
|
||||
lqw.eq(bo.getStatus() != null, PayOrder::getStatus, bo.getStatus());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增支付订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(PayOrderBo bo) {
|
||||
PayOrder add = MapstructUtils.convert(bo, PayOrder.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改支付订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(PayOrderBo bo) {
|
||||
PayOrder update = MapstructUtils.convert(bo, PayOrder.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(PayOrder entity){
|
||||
// 校验商户订单号唯一性(如果已存在)
|
||||
if (StringUtils.isNotBlank(entity.getMerchantOrderId())) {
|
||||
LambdaQueryWrapper<PayOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PayOrder::getMerchantOrderId, entity.getMerchantOrderId());
|
||||
if (entity.getId() != null) {
|
||||
wrapper.ne(PayOrder::getId, entity.getId());
|
||||
}
|
||||
Long count = baseMapper.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new ServiceException("商户订单号已存在");
|
||||
}
|
||||
}
|
||||
// 校验支付单号唯一性
|
||||
if (StringUtils.isNotBlank(entity.getNo())) {
|
||||
LambdaQueryWrapper<PayOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PayOrder::getNo, entity.getNo());
|
||||
if (entity.getId() != null) {
|
||||
wrapper.ne(PayOrder::getId, entity.getId());
|
||||
}
|
||||
Long count = baseMapper.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new ServiceException("支付单号已存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除支付订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
// 校验订单是否可以删除(已支付的订单不能删除)
|
||||
for (Long id : ids) {
|
||||
PayOrder order = baseMapper.selectById(id);
|
||||
if (order != null && PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
||||
throw new ServiceException("已支付的订单不能删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayOrderSubmitRespVO submitOrder(PayOrderBo bo) {
|
||||
// 1. 校验 App
|
||||
PayApp app = payAppMapper.selectById(bo.getAppId());
|
||||
if (app == null || app.getStatus() == 1) { // 假设 1 为关闭
|
||||
throw new ServiceException("应用不存在或已禁用");
|
||||
}
|
||||
|
||||
// 2. 校验 Channel
|
||||
PayChannel channel = payChannelMapper.selectOne(new LambdaQueryWrapper<PayChannel>()
|
||||
.eq(PayChannel::getAppId, bo.getAppId())
|
||||
.eq(PayChannel::getCode, bo.getChannelCode()));
|
||||
if (channel == null || channel.getStatus() == 1) {
|
||||
throw new ServiceException("支付渠道不存在或已禁用");
|
||||
}
|
||||
|
||||
// 3. 创建订单
|
||||
PayOrder order = MapstructUtils.convert(bo, PayOrder.class);
|
||||
order.setChannelId(channel.getId());
|
||||
order.setNo(IdUtil.getSnowflakeNextIdStr()); // 生成内部单号
|
||||
order.setStatus(PayOrderStatusEnum.WAITING.getStatus());
|
||||
order.setExpireTime(new Date(System.currentTimeMillis() + 30 * 60 * 1000)); // 30分钟过期
|
||||
// 补全 NotifyUrl
|
||||
if (StringUtils.isBlank(order.getNotifyUrl())) {
|
||||
String baseUrl = app.getPayNotifyUrl();
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
}
|
||||
order.setNotifyUrl(baseUrl + "/" + channel.getCode() + "/" + channel.getId());
|
||||
}
|
||||
|
||||
baseMapper.insert(order);
|
||||
|
||||
// 4. 调用策略
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channel.getCode());
|
||||
PayOrderSubmitRespVO resp = strategy.unifiedOrder(order, channel);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void notifyOrderSuccess(String orderNo, String channelOrderNo) {
|
||||
PayOrder order = baseMapper.selectOne(new LambdaQueryWrapper<PayOrder>().eq(PayOrder::getNo, orderNo));
|
||||
if (order == null) {
|
||||
log.error("支付回调订单不存在: {}", orderNo);
|
||||
return;
|
||||
}
|
||||
if (PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
||||
log.info("订单已支付,忽略回调: {}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
order.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
|
||||
order.setSuccessTime(new Date());
|
||||
order.setChannelOrderNo(channelOrderNo);
|
||||
baseMapper.updateById(order);
|
||||
|
||||
log.info("订单支付成功: {}", orderNo);
|
||||
// 发布支付成功事件
|
||||
eventPublisher.publishEvent(new PaySuccessEvent(this, order));
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package org.dromara.pay.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.pay.core.PayStrategy;
|
||||
import org.dromara.pay.core.PayStrategyFactory;
|
||||
import org.dromara.pay.domain.PayChannel;
|
||||
import org.dromara.pay.domain.PayOrder;
|
||||
import org.dromara.pay.domain.PayRefund;
|
||||
import org.dromara.pay.domain.bo.PayRefundBo;
|
||||
import org.dromara.pay.domain.vo.PayRefundVo;
|
||||
import org.dromara.pay.enums.PayOrderStatusEnum;
|
||||
import org.dromara.pay.mapper.PayChannelMapper;
|
||||
import org.dromara.pay.mapper.PayOrderMapper;
|
||||
import org.dromara.pay.mapper.PayRefundMapper;
|
||||
import org.dromara.pay.service.PayRefundService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 退款订单Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class PayRefundServiceImpl implements PayRefundService {
|
||||
|
||||
private final PayRefundMapper baseMapper;
|
||||
private final PayOrderMapper payOrderMapper;
|
||||
private final PayChannelMapper payChannelMapper;
|
||||
private final PayStrategyFactory payStrategyFactory;
|
||||
|
||||
/**
|
||||
* 查询退款订单
|
||||
*/
|
||||
@Override
|
||||
public PayRefundVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<PayRefundVo> queryPageList(PayRefundBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<PayRefund> lqw = buildQueryWrapper(bo);
|
||||
Page<PayRefundVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款订单列表
|
||||
*/
|
||||
@Override
|
||||
public List<PayRefundVo> queryList(PayRefundBo bo) {
|
||||
LambdaQueryWrapper<PayRefund> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<PayRefund> buildQueryWrapper(PayRefundBo bo) {
|
||||
LambdaQueryWrapper<PayRefund> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.eq(bo.getAppId() != null, PayRefund::getAppId, bo.getAppId());
|
||||
lqw.eq(bo.getChannelId() != null, PayRefund::getChannelId, bo.getChannelId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getChannelCode()), PayRefund::getChannelCode, bo.getChannelCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMerchantOrderId()), PayRefund::getMerchantOrderId, bo.getMerchantOrderId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getMerchantRefundId()), PayRefund::getMerchantRefundId, bo.getMerchantRefundId());
|
||||
lqw.eq(bo.getStatus() != null, PayRefund::getStatus, bo.getStatus());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增退款订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(PayRefundBo bo) {
|
||||
PayRefund add = MapstructUtils.convert(bo, PayRefund.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改退款订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(PayRefundBo bo) {
|
||||
PayRefund update = MapstructUtils.convert(bo, PayRefund.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(PayRefund entity){
|
||||
// 校验退款单号唯一性
|
||||
if (StringUtils.isNotBlank(entity.getNo())) {
|
||||
LambdaQueryWrapper<PayRefund> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PayRefund::getNo, entity.getNo());
|
||||
if (entity.getId() != null) {
|
||||
wrapper.ne(PayRefund::getId, entity.getId());
|
||||
}
|
||||
Long count = baseMapper.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new ServiceException("退款单号已存在");
|
||||
}
|
||||
}
|
||||
// 校验商户退款编号唯一性
|
||||
if (StringUtils.isNotBlank(entity.getMerchantRefundId())) {
|
||||
LambdaQueryWrapper<PayRefund> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PayRefund::getMerchantRefundId, entity.getMerchantRefundId());
|
||||
if (entity.getId() != null) {
|
||||
wrapper.ne(PayRefund::getId, entity.getId());
|
||||
}
|
||||
Long count = baseMapper.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new ServiceException("商户退款编号已存在");
|
||||
}
|
||||
}
|
||||
// 校验退款金额不能超过支付金额
|
||||
if (entity.getRefundPrice() != null && entity.getPayPrice() != null) {
|
||||
if (entity.getRefundPrice() > entity.getPayPrice()) {
|
||||
throw new ServiceException("退款金额不能超过支付金额");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除退款订单
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
// 校验退款订单是否可以删除(处理中的退款不能删除)
|
||||
for (Long id : ids) {
|
||||
PayRefund refund = baseMapper.selectById(id);
|
||||
if (refund != null && refund.getStatus() == 0) {
|
||||
throw new ServiceException("处理中的退款订单不能删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean createRefund(PayRefundBo bo) {
|
||||
// 1. 校验订单
|
||||
PayOrder order = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrder>()
|
||||
.eq(PayOrder::getNo, bo.getOrderNo()));
|
||||
if (order == null) {
|
||||
throw new ServiceException("订单不存在");
|
||||
}
|
||||
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
||||
throw new ServiceException("订单未支付或已退款");
|
||||
}
|
||||
|
||||
// 2. 创建退款单
|
||||
PayRefund refund = MapstructUtils.convert(bo, PayRefund.class);
|
||||
refund.setAppId(order.getAppId());
|
||||
refund.setChannelId(order.getChannelId());
|
||||
refund.setChannelCode(order.getChannelCode());
|
||||
refund.setMerchantOrderId(order.getMerchantOrderId());
|
||||
refund.setPayPrice(order.getPrice());
|
||||
refund.setNo(IdUtil.getSnowflakeNextIdStr()); // 生成退款单号
|
||||
refund.setStatus(0); // WAITING
|
||||
// NotifyUrl 可以从 App 配置获取,或者 BO 传入
|
||||
|
||||
baseMapper.insert(refund);
|
||||
|
||||
// 3. 执行退款策略
|
||||
PayChannel channel = payChannelMapper.selectById(order.getChannelId());
|
||||
PayStrategy strategy = payStrategyFactory.getStrategy(channel.getCode());
|
||||
Boolean success = strategy.refund(refund, channel);
|
||||
|
||||
// 4. 更新状态
|
||||
if (success) {
|
||||
refund.setStatus(10); // SUCCESS (假设 10 为成功)
|
||||
refund.setSuccessTime(new Date());
|
||||
baseMapper.updateById(refund);
|
||||
|
||||
// 更新订单状态 (如果是全额退款,可以标记为已退款)
|
||||
// 简单处理:只要退款成功,订单状态改为已退款 (20)
|
||||
order.setStatus(PayOrderStatusEnum.REFUND.getStatus());
|
||||
payOrderMapper.updateById(order);
|
||||
} else {
|
||||
refund.setStatus(20); // FAIL (假设 20 为失败)
|
||||
baseMapper.updateById(refund);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退款回调成功
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void notifyRefundSuccess(String refundNo, String channelRefundNo) {
|
||||
PayRefund refund = baseMapper.selectOne(new LambdaQueryWrapper<PayRefund>().eq(PayRefund::getNo, refundNo));
|
||||
if (refund == null) {
|
||||
log.error("退款回调订单不存在: {}", refundNo);
|
||||
return;
|
||||
}
|
||||
if (refund.getStatus() == 10) { // 已成功
|
||||
log.info("退款已成功,忽略回调: {}", refundNo);
|
||||
return;
|
||||
}
|
||||
|
||||
refund.setStatus(10); // SUCCESS
|
||||
refund.setSuccessTime(new Date());
|
||||
refund.setChannelRefundNo(channelRefundNo);
|
||||
baseMapper.updateById(refund);
|
||||
|
||||
log.info("退款成功: {}", refundNo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.dromara.pay.util;
|
||||
|
||||
import com.wechat.pay.java.core.cipher.RSASigner;
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 微信支付 V3 工具类
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public class WechatPayUtil {
|
||||
|
||||
/**
|
||||
* App 支付二次签名
|
||||
*
|
||||
* @param appId 应用ID
|
||||
* @param mchId 商户ID
|
||||
* @param prepayId 预支付交易会话ID
|
||||
* @param privateKey 商户私钥
|
||||
* @param mchSerialNo 商户API证书序列号
|
||||
* @return 返回给 App 的参数
|
||||
*/
|
||||
public static Map<String, String> signApp(String appId, String mchId, String prepayId, PrivateKey privateKey, String mchSerialNo) {
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String nonceStr = UUID.randomUUID().toString().replace("-", "");
|
||||
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
|
||||
|
||||
RSASigner signer = new RSASigner(mchSerialNo, privateKey);
|
||||
String sign = signer.sign(message).getSign();
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("appid", appId);
|
||||
map.put("partnerid", mchId);
|
||||
map.put("prepayid", prepayId);
|
||||
map.put("package", "Sign=WXPay");
|
||||
map.put("noncestr", nonceStr);
|
||||
map.put("timestamp", String.valueOf(timestamp));
|
||||
map.put("sign", sign);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSAPI/小程序 支付二次签名
|
||||
*
|
||||
* @param appId 应用ID
|
||||
* @param prepayId 预支付交易会话ID
|
||||
* @param privateKey 商户私钥
|
||||
* @return 返回给 JSAPI/小程序的参数
|
||||
*/
|
||||
public static Map<String, String> signJsapi(String appId, String prepayId, PrivateKey privateKey, String mchSerialNo) {
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String nonceStr = UUID.randomUUID().toString().replace("-", "");
|
||||
String packageStr = "prepay_id=" + prepayId;
|
||||
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + packageStr + "\n";
|
||||
|
||||
RSASigner signer = new RSASigner(mchSerialNo, privateKey);
|
||||
String sign = signer.sign(message).getSign();
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("appId", appId);
|
||||
map.put("timeStamp", String.valueOf(timestamp));
|
||||
map.put("nonceStr", nonceStr);
|
||||
map.put("package", packageStr);
|
||||
map.put("signType", "RSA");
|
||||
map.put("paySign", sign);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.pay.mapper.PayAppMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.pay.mapper.PayChannelMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.pay.mapper.PayOrderMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.pay.mapper.PayRefundMapper">
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user