Преглед изворни кода

Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product

puhui999 пре 1 година
родитељ
комит
c9c176ea05
50 измењених фајлова са 2334 додато и 1678 уклоњено
  1. 2 3
      README.md
  2. 1 1
      pom.xml
  3. 0 43
      sql/mysql/optional/go-view.sql
  4. 0 357
      sql/mysql/optional/jimureport.mysql5.7.create.sql
  5. 0 174
      sql/mysql/optional/member_point.sql
  6. 0 118
      sql/mysql/optional/mp.sql
  7. 0 44
      sql/mysql/optional/pay_temp.sql
  8. 61 254
      sql/mysql/ruoyi-vue-pro.sql
  9. 18 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  10. 23 21
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java
  11. 31 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  12. 75 13
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  13. 154 13
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  14. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java
  15. 1 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java
  16. 10 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java
  17. 0 2
      yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java
  18. 1 1
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/address/MemberAddressDO.java
  19. 1 2
      yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql
  20. 2 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
  21. 0 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java
  22. 6 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java
  23. 13 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java
  24. 7 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java
  25. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java
  26. 31 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java
  27. 43 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java
  28. 0 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/package-info.java
  29. 31 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/refund/PayRefundSyncJob.java
  30. 16 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java
  31. 172 12
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
  32. 7 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java
  33. 106 65
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
  34. 3 1
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java
  35. 555 12
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
  36. 588 43
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
  37. 8 11
      yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql
  38. 15 3
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java
  39. 2 2
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImplTest.java
  40. 15 15
      yudao-server/pom.xml
  41. 0 8
      yudao-ui-admin/src/utils/constants.js
  42. 2 7
      yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue
  43. 0 328
      yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
  44. 257 0
      yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue
  45. 61 111
      yudao-ui-admin/src/views/pay/app/index.vue
  46. 0 1
      yudao-ui-admin/src/views/pay/cashier/index.vue
  47. 5 0
      yudao-ui-admin/src/views/pay/demo/index.vue
  48. 1 0
      yudao-ui-admin/src/views/pay/notify/index.vue
  49. 3 0
      yudao-ui-admin/src/views/pay/order/index.vue
  50. 2 0
      yudao-ui-admin/src/views/pay/refund/index.vue

+ 2 - 3
README.md

@@ -160,12 +160,11 @@
 
 |     | 功能   | 描述                        |
 |-----|------|---------------------------|
-| 🚀  | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能  |
 | 🚀  | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
 | 🚀  | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单     |
 | 🚀  | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单     |
-
-ps:核心功能已经实现,正在对接微信小程序中...
+| 🚀  | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果    |
+| 🚀  | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战    |
 
 ### 基础设施
 

+ 1 - 1
pom.xml

@@ -20,7 +20,7 @@
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
-        <module>yudao-module-mall</module>
+<!--        <module>yudao-module-mall</module>-->
         <!-- 示例项目 -->
         <module>yudao-example</module>
     </modules>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 43
sql/mysql/optional/go-view.sql


Разлика између датотеке није приказан због своје велике величине
+ 0 - 357
sql/mysql/optional/jimureport.mysql5.7.create.sql


+ 0 - 174
sql/mysql/optional/member_point.sql

@@ -1,174 +0,0 @@
-/*
- Navicat Premium Data Transfer
-
- Source Server         : docer-master-root(3308)
- Source Server Type    : MySQL
- Source Server Version : 80030
- Source Host           : 10.211.55.5:3308
- Source Schema         : mall
-
- Target Server Type    : MySQL
- Target Server Version : 80030
- File Encoding         : 65001
-
- Date: 28/06/2023 22:40:52
-*/
-
-SET NAMES utf8mb4;
-SET FOREIGN_KEY_CHECKS = 0;
-
--- ----------------------------
--- Table structure for member_point_config
--- ----------------------------
-DROP TABLE IF EXISTS `member_point_config`;
-CREATE TABLE `member_point_config` (
-                                       `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
-                                       `trade_deduct_enable` bit(1) DEFAULT NULL COMMENT '1 开启积分抵扣\n0 关闭积分抵扣',
-                                       `trade_deduct_unit_price` int DEFAULT NULL COMMENT '积分抵扣(单位:分)',
-                                       `trade_deduct_max_price` int DEFAULT NULL COMMENT '积分抵扣最大值',
-                                       `trade_give_point` bigint DEFAULT NULL COMMENT '1元赠送多少分',
-                                       `creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
-                                       `create_time` datetime DEFAULT NULL COMMENT '创建时间',
-                                       `updater` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新人',
-                                       `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '变更时间',
-                                       `tenant_id` varchar(255) DEFAULT NULL COMMENT '租户id',
-                                       `deleted` bit(1) DEFAULT b'0' COMMENT '是否被删除 0 未删除 1已删除',
-                                       PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员积分配置表';
-
--- ----------------------------
--- Records of member_point_config
--- ----------------------------
-BEGIN;
-INSERT INTO `member_point_config` (`id`, `trade_deduct_enable`, `trade_deduct_unit_price`, `trade_deduct_max_price`, `trade_give_point`, `creator`, `create_time`, `updater`, `update_time`, `tenant_id`, `deleted`) VALUES (1, b'1', 0, 10000, 1, '1', '2023-06-10 10:57:22', '1', '2023-06-10 03:06:58', '1', b'1');
-INSERT INTO `member_point_config` (`id`, `trade_deduct_enable`, `trade_deduct_unit_price`, `trade_deduct_max_price`, `trade_give_point`, `creator`, `create_time`, `updater`, `update_time`, `tenant_id`, `deleted`) VALUES (2, b'1', 32, 10003, 1212, '1', '2023-06-10 11:07:12', '1', '2023-06-28 21:50:34', '1', b'0');
-INSERT INTO `member_point_config` (`id`, `trade_deduct_enable`, `trade_deduct_unit_price`, `trade_deduct_max_price`, `trade_give_point`, `creator`, `create_time`, `updater`, `update_time`, `tenant_id`, `deleted`) VALUES (3, b'1', 12, 12, 12, '1', '2023-06-10 16:09:26', '1', '2023-06-10 08:10:53', '1', b'1');
-COMMIT;
-
--- ----------------------------
--- Table structure for member_point_record
--- ----------------------------
-DROP TABLE IF EXISTS `member_point_record`;
-CREATE TABLE `member_point_record` (
-                                       `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
-                                       `biz_id` varchar(255) DEFAULT NULL COMMENT '业务编码',
-                                       `biz_type` varchar(255) DEFAULT NULL COMMENT '业务类型',
-                                       `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '1增加 0扣减',
-                                       `title` varchar(255) DEFAULT NULL COMMENT '积分标题',
-                                       `description` varchar(5000) DEFAULT NULL COMMENT '积分描述',
-                                       `point` int DEFAULT NULL COMMENT '积分',
-                                       `total_point` int NOT NULL COMMENT '变动后的积分',
-                                       `status` int DEFAULT NULL COMMENT '状态:1-订单创建,2-冻结期,3-完成,4-失效(订单退款)\n',
-                                       `user_id` int DEFAULT NULL COMMENT '用户id',
-                                       `freezing_time` datetime DEFAULT NULL COMMENT '冻结时间',
-                                       `thawing_time` datetime DEFAULT NULL COMMENT '解冻时间',
-                                       `create_time` datetime DEFAULT NULL COMMENT '发生时间',
-                                       `tenant_id` varchar(255) DEFAULT NULL COMMENT '租户',
-                                       `deleted` int DEFAULT '0' COMMENT '是否删除',
-                                       `creator` varchar(255) DEFAULT NULL COMMENT '创建用户',
-                                       `updater` varchar(255) DEFAULT NULL COMMENT '更新用户',
-                                       `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-                                       PRIMARY KEY (`id`),
-                                       KEY `index_userId` (`user_id`),
-                                       KEY `index_title` (`title`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户积分记录';
-
--- ----------------------------
--- Records of member_point_record
--- ----------------------------
-BEGIN;
-INSERT INTO `member_point_record` (`id`, `biz_id`, `biz_type`, `type`, `title`, `description`, `point`, `total_point`, `status`, `user_id`, `freezing_time`, `thawing_time`, `create_time`, `tenant_id`, `deleted`, `creator`, `updater`, `update_time`) VALUES (1, '1', '1', '1', '12', NULL, 212, 12, 1, 12, '2023-06-13 00:00:00', '2023-06-20 00:00:00', '2023-06-10 12:38:48', '1', 1, '1', '1', '2023-06-10 04:42:24');
-INSERT INTO `member_point_record` (`id`, `biz_id`, `biz_type`, `type`, `title`, `description`, `point`, `total_point`, `status`, `user_id`, `freezing_time`, `thawing_time`, `create_time`, `tenant_id`, `deleted`, `creator`, `updater`, `update_time`) VALUES (2, '12', '1', '0', NULL, NULL, 1212, 12, 2, 12, '2023-06-28 00:00:00', NULL, '2023-06-10 12:42:48', '1', 0, '1', '1', '2023-06-10 12:43:04');
-INSERT INTO `member_point_record` (`id`, `biz_id`, `biz_type`, `type`, `title`, `description`, `point`, `total_point`, `status`, `user_id`, `freezing_time`, `thawing_time`, `create_time`, `tenant_id`, `deleted`, `creator`, `updater`, `update_time`) VALUES (3, '12', '1', '1', '12', NULL, 12, 12, 1, 12, '2023-06-27 00:00:00', '2023-06-23 00:00:00', '2023-06-10 20:06:48', '1', 0, '1', '1', '2023-06-10 20:06:48');
-COMMIT;
-
--- ----------------------------
--- Table structure for member_sign_in_config
--- ----------------------------
-DROP TABLE IF EXISTS `member_sign_in_config`;
-CREATE TABLE `member_sign_in_config` (
-                                         `id` int NOT NULL AUTO_INCREMENT COMMENT '规则自增主键',
-                                         `day` int DEFAULT NULL COMMENT '签到第x天',
-                                         `point` int DEFAULT NULL COMMENT '签到天数对应分数',
-                                         `create_time` datetime DEFAULT NULL COMMENT '创建时间',
-                                         `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '变更时间',
-                                         `tenant_id` varchar(255) DEFAULT NULL COMMENT '租户id',
-                                         `deleted` int DEFAULT '0' COMMENT '是否删除',
-                                         `creator` varchar(255) DEFAULT NULL COMMENT '创建人',
-                                         `updater` varchar(255) DEFAULT NULL COMMENT '变更人',
-                                         PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='积分签到规则';
-
--- ----------------------------
--- Records of member_sign_in_config
--- ----------------------------
-BEGIN;
-INSERT INTO `member_sign_in_config` (`id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (1, 1, 10, '2023-06-10 11:34:43', '2023-06-10 11:34:43', '1', 0, '1', '1');
-INSERT INTO `member_sign_in_config` (`id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (2, 2, 20, '2023-06-10 11:34:59', '2023-06-10 03:55:35', '1', 1, '1', '1');
-INSERT INTO `member_sign_in_config` (`id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (3, 7, 1001, '2023-06-10 17:47:45', '2023-06-10 19:54:37', '1', 0, '1', '1');
-INSERT INTO `member_sign_in_config` (`id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (4, 6, 12121, '2023-06-10 17:47:55', '2023-06-10 19:48:37', '1', 0, '1', '1');
-INSERT INTO `member_sign_in_config` (`id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (5, 2, 12, '2023-06-10 19:54:52', '2023-06-10 19:54:52', '1', 0, '1', '1');
-COMMIT;
-
--- ----------------------------
--- Table structure for member_sign_in_record
--- ----------------------------
-DROP TABLE IF EXISTS `member_sign_in_record`;
-CREATE TABLE `member_sign_in_record` (
-                                         `id` bigint NOT NULL AUTO_INCREMENT COMMENT '签到自增id',
-                                         `user_id` int DEFAULT NULL COMMENT '签到用户',
-                                         `day` int DEFAULT NULL COMMENT '第几天签到',
-                                         `point` int DEFAULT NULL COMMENT '签到的分数',
-                                         `create_time` datetime DEFAULT NULL COMMENT '签到时间',
-                                         `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '变更时间',
-                                         `tenant_id` varchar(255) DEFAULT NULL COMMENT '租户id',
-                                         `deleted` int DEFAULT '0' COMMENT '是否删除',
-                                         `creator` varchar(255) DEFAULT NULL COMMENT '创建人',
-                                         `updater` varchar(255) DEFAULT NULL COMMENT '更新人',
-                                         PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户签到积分';
-
--- ----------------------------
--- Records of member_sign_in_record
--- ----------------------------
-BEGIN;
-INSERT INTO `member_sign_in_record` (`id`, `user_id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (1, 121, 1, 123, '2023-06-10 12:58:18', '2023-06-10 04:59:00', '1', 1, '1', '1');
-INSERT INTO `member_sign_in_record` (`id`, `user_id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (2, 12, 12, 12, '2023-06-10 19:56:39', '2023-06-10 11:56:45', '1', 1, '1', '1');
-INSERT INTO `member_sign_in_record` (`id`, `user_id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (3, 12, 12, 1212, '2023-06-10 20:01:17', '2023-06-10 12:01:23', '1', 1, '1', '1');
-INSERT INTO `member_sign_in_record` (`id`, `user_id`, `day`, `point`, `create_time`, `update_time`, `tenant_id`, `deleted`, `creator`, `updater`) VALUES (4, 12, 12, 1212, '2023-06-10 20:01:27', '2023-06-10 20:01:27', '1', 0, '1', '1');
-COMMIT;
-
-
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (170, '积分业务类型', 'member_point_biz_type', 0, '', '1', '2023-06-10 12:15:00', '1', '2023-06-28 13:48:20', b'0', '1970-01-01 00:00:00');
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (171, '积分订单状态', 'member_point_status', 0, '', '1', '2023-06-10 12:16:27', '1', '2023-06-28 13:48:17', b'0', '1970-01-01 00:00:00');
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (169, '是否抵扣积分', 'trade_deduct_enable', 0, NULL, '1', '2023-06-10 00:34:12', '1', '2023-06-10 04:14:20', b'1', '2023-06-10 12:14:20');
-
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1235, 1, '购物', '1', 'member_point_biz_type', 0, '', '', '', '1', '2023-06-10 12:15:27', '1', '2023-06-28 13:48:28', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1236, 2, '签到', '2', 'member_point_biz_type', 0, '', '', '', '1', '2023-06-10 12:15:48', '1', '2023-06-28 13:48:31', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1237, 1, '订单创建', '1', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:16:42', '1', '2023-06-28 13:48:34', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1238, 2, '冻结期', '2', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:16:58', '1', '2023-06-28 13:48:36', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1239, 3, '完成', '3', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:07', '1', '2023-06-28 13:48:38', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1240, 4, '失效(订单退款)', '4', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:21', '1', '2023-06-28 13:48:42', b'0');
-
-
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2162, '会员中心', '', 1, 55, 0, '/member', 'date-range', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-06-10 00:42:03', '1', '2023-06-28 21:52:34', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2175, '积分配置', '', 2, 0, 2199, 'config', '', 'member/point/config/index', 'PointConfig', 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '1', '2023-06-27 22:50:59', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2176, '积分设置查询', 'point:config:query', 3, 1, 2175, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '', '2023-06-10 02:07:44', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2177, '积分设置创建', 'point:config:save', 3, 2, 2175, '', '', '', '', 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '1', '2023-06-27 20:32:31', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2181, '签到配置', '', 2, 2, 2200, 'sign-in-config', '', 'member/signin/config/index', 'SignInConfig', 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '1', '2023-06-27 22:51:45', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2182, '积分签到规则查询', 'point:sign-in-config:query', 3, 1, 2181, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2183, '积分签到规则创建', 'point:sign-in-config:create', 3, 2, 2181, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2184, '积分签到规则更新', 'point:sign-in-config:update', 3, 3, 2181, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2185, '积分签到规则删除', 'point:sign-in-config:delete', 3, 4, 2181, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2186, '积分签到规则导出', 'point:sign-in-config:export', 3, 5, 2181, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2187, '积分记录', '', 2, 1, 2199, 'record', '', 'member/point/record/index', 'PointRecord', 0, b'1', b'1', b'1', '', '2023-06-10 04:18:50', '1', '2023-06-27 22:51:07', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2188, '用户积分记录查询', 'point:record:query', 3, 1, 2187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:18:50', '', '2023-06-10 04:18:50', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2192, '用户积分记录导出', 'point:record:export', 3, 5, 2187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:18:50', '', '2023-06-10 04:18:50', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2193, '签到记录', '', 2, 3, 2200, 'sign-in-record', '', 'member/signin/record/index', 'SignInRecord', 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '1', '2023-06-27 22:51:54', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2194, '用户签到积分查询', 'point:sign-in-record:query', 3, 1, 2193, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2197, '用户签到积分删除', 'point:sign-in-record:delete', 3, 4, 2193, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2198, '用户签到积分导出', 'point:sign-in-record:export', 3, 5, 2193, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2199, '会员积分', '', 1, 1, 2162, 'point', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:48:51', '1', '2023-06-27 22:48:51', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2200, '会员签到', '', 1, 2, 2162, 'signin', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:49:53', '1', '2023-06-27 22:49:53', b'0');
-
-SET FOREIGN_KEY_CHECKS = 1;

Разлика између датотеке није приказан због своје велике величине
+ 0 - 118
sql/mysql/optional/mp.sql


+ 0 - 44
sql/mysql/optional/pay_temp.sql

@@ -1,44 +0,0 @@
--- ----------------------------
--- 支付-会员钱包表
--- ----------------------------
-DROP TABLE IF EXISTS `pay_member_wallet`;
-CREATE TABLE `pay_member_wallet`
-(
-    `id`             bigint   NOT NULL AUTO_INCREMENT COMMENT '编号',
-    `user_id`        bigint   NOT NULL COMMENT '用户 id',
-    `balance`        int      NOT NULL DEFAULT 0 COMMENT '余额, 单位分',
-    `total_spending` int      NOT NULL DEFAULT 0 COMMENT '累计支出, 单位分',
-    `total_top_up`   int      NOT NULL DEFAULT 0 COMMENT '累计充值, 单位分',
-    `creator`        varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-    `create_time`    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater`        varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-    `update_time`    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `deleted`        bit(1)   NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    `tenant_id`      bigint   NOT NULL DEFAULT 0 COMMENT '租户编号',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB COMMENT='会员钱包表';
-
--- ----------------------------
--- 支付-会员钱包明细表
--- ----------------------------
-DROP TABLE IF EXISTS `pay_member_wallet_transaction`;
-CREATE TABLE `pay_member_wallet_transaction`
-(
-    `id`               bigint      NOT NULL AUTO_INCREMENT COMMENT '编号',
-    `wallet_id`        bigint      NOT NULL COMMENT '会员钱包 id',
-    `user_id`          bigint      NOT NULL COMMENT '用户 id',
-    `trade_no`         varchar(64) COMMENT '交易单号',
-    `category`         tinyint     NOT NULL COMMENT '交易大类',
-    `operate_type`     tinyint     NOT NULL COMMENT '操作分类',
-    `operate_desc`     varchar(64) NOT NULL COMMENT '操作说明',
-    `amount`           int         NOT NULL COMMENT '交易金额, 单位分',
-    `balance`          int         NOT NULL COMMENT '余额, 单位分',
-    `mark`             varchar(512) COMMENT '备注',
-    `transaction_time` datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '交易时间',
-    `create_time`      datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater`          varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-    `update_time`      datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `deleted`          bit(1)      NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    `tenant_id`        bigint      NOT NULL DEFAULT 0 COMMENT '租户编号',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB COMMENT='会员钱包明细表';

+ 61 - 254
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80026
  File Encoding         : 65001
 
- Date: 09/07/2023 20:25:37
+ Date: 24/07/2023 08:51:31
 */
 
 SET NAMES utf8mb4;
@@ -73,6 +73,9 @@ CREATE TABLE `QRTZ_CRON_TRIGGERS`  (
 -- ----------------------------
 BEGIN;
 INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', '* * * * * ?', 'Asia/Shanghai');
+INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai');
+INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai');
+INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai');
 COMMIT;
 
 -- ----------------------------
@@ -133,6 +136,9 @@ CREATE TABLE `QRTZ_JOB_DETAILS`  (
 -- ----------------------------
 BEGIN;
 INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000057400104A4F425F48414E444C45525F4E414D4574000C7061794E6F746966794A6F627800);
+INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000127400104A4F425F48414E444C45525F4E414D457400117061794F726465724578706972654A6F627800);
+INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000117400104A4F425F48414E444C45525F4E414D4574000F7061794F7264657253796E634A6F627800);
+INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000137400104A4F425F48414E444C45525F4E414D45740010706179526566756E6453796E634A6F627800);
 COMMIT;
 
 -- ----------------------------
@@ -185,7 +191,7 @@ CREATE TABLE `QRTZ_SCHEDULER_STATE`  (
 -- Records of QRTZ_SCHEDULER_STATE
 -- ----------------------------
 BEGIN;
-INSERT INTO `QRTZ_SCHEDULER_STATE` (`SCHED_NAME`, `INSTANCE_NAME`, `LAST_CHECKIN_TIME`, `CHECKIN_INTERVAL`) VALUES ('schedulerName', 'Yunai1677076619095', 1677076631456, 15000);
+INSERT INTO `QRTZ_SCHEDULER_STATE` (`SCHED_NAME`, `INSTANCE_NAME`, `LAST_CHECKIN_TIME`, `CHECKIN_INTERVAL`) VALUES ('schedulerName', 'Yunai1690117495401', 1690119854263, 15000);
 COMMIT;
 
 -- ----------------------------
@@ -279,202 +285,10 @@ CREATE TABLE `QRTZ_TRIGGERS`  (
 -- Records of QRTZ_TRIGGERS
 -- ----------------------------
 BEGIN;
-INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1677076638000, 1677076637000, 5, 'WAITING', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_form
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_form`;
-CREATE TABLE `bpm_form`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '表单名',
-  `status` tinyint NOT NULL COMMENT '开启状态',
-  `conf` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '表单的配置',
-  `fields` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '表单项的数组',
-  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的表单定义';
-
--- ----------------------------
--- Records of bpm_form
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_oa_leave
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_oa_leave`;
-CREATE TABLE `bpm_oa_leave`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '请假表单主键',
-  `user_id` bigint NOT NULL COMMENT '申请人的用户编号',
-  `type` tinyint NOT NULL COMMENT '请假类型',
-  `reason` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请假原因',
-  `start_time` datetime NOT NULL COMMENT '开始时间',
-  `end_time` datetime NOT NULL COMMENT '结束时间',
-  `day` tinyint NOT NULL COMMENT '请假天数',
-  `result` tinyint NOT NULL COMMENT '请假结果',
-  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '流程实例的编号',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OA 请假申请表';
-
--- ----------------------------
--- Records of bpm_oa_leave
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_process_definition_ext
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_process_definition_ext`;
-CREATE TABLE `bpm_process_definition_ext`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程定义的编号',
-  `model_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程模型的编号',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述',
-  `form_type` tinyint NOT NULL COMMENT '表单类型',
-  `form_id` bigint NULL DEFAULT NULL COMMENT '表单编号',
-  `form_conf` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '表单的配置',
-  `form_fields` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '表单项的数组',
-  `form_custom_create_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '自定义表单的提交路径',
-  `form_custom_view_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '自定义表单的查看路径',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 141 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 流程定义的拓展表\n';
-
--- ----------------------------
--- Records of bpm_process_definition_ext
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_process_instance_ext
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_process_instance_ext`;
-CREATE TABLE `bpm_process_instance_ext`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `start_user_id` bigint NOT NULL COMMENT '发起流程的用户编号',
-  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '流程实例的名字',
-  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程实例的编号',
-  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程定义的编号',
-  `category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '流程分类',
-  `status` tinyint NOT NULL COMMENT '流程实例的状态',
-  `result` tinyint NOT NULL COMMENT '流程实例的结果',
-  `end_time` datetime NULL DEFAULT NULL COMMENT '结束时间',
-  `form_variables` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '表单值',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 296 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程实例的拓展';
-
--- ----------------------------
--- Records of bpm_process_instance_ext
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_task_assign_rule
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_task_assign_rule`;
-CREATE TABLE `bpm_task_assign_rule`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `model_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程模型的编号',
-  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程定义的编号',
-  `task_definition_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程任务定义的 key',
-  `type` tinyint NOT NULL COMMENT '规则类型',
-  `options` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则值,JSON 数组',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 276 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 任务规则表';
-
--- ----------------------------
--- Records of bpm_task_assign_rule
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_task_ext
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_task_ext`;
-CREATE TABLE `bpm_task_ext`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `assignee_user_id` bigint NULL DEFAULT NULL COMMENT '任务的审批人',
-  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '任务的名字',
-  `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务的编号',
-  `result` tinyint NOT NULL COMMENT '任务的结果',
-  `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批建议',
-  `end_time` datetime NULL DEFAULT NULL COMMENT '任务的结束时间',
-  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程实例的编号',
-  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程定义的编号',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 351 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程任务的拓展表';
-
--- ----------------------------
--- Records of bpm_task_ext
--- ----------------------------
-BEGIN;
-COMMIT;
-
--- ----------------------------
--- Table structure for bpm_user_group
--- ----------------------------
-DROP TABLE IF EXISTS `bpm_user_group`;
-CREATE TABLE `bpm_user_group`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '组名',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '描述',
-  `member_user_ids` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '成员编号数组',
-  `status` tinyint NOT NULL COMMENT '状态(0正常 1停用)',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 113 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户组';
-
--- ----------------------------
--- Records of bpm_user_group
--- ----------------------------
-BEGIN;
+INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1688907102000, 1688907101000, 5, 'PAUSED', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
+INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', 'payOrderExpireJob', 'DEFAULT', NULL, 1690011600000, -1, 5, 'PAUSED', 'CRON', 1690011553000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
+INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', 'payOrderSyncJob', 'DEFAULT', NULL, 1690011600000, 1690011540000, 5, 'PAUSED', 'CRON', 1690007785000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
+INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', 'payRefundSyncJob', 'DEFAULT', NULL, 1690117560000, 1690117500000, 5, 'PAUSED', 'CRON', 1690117424000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800);
 COMMIT;
 
 -- ----------------------------
@@ -546,7 +360,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1291 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 1391 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -584,7 +398,7 @@ CREATE TABLE `infra_codegen_column`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1698 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 1715 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
 
 -- ----------------------------
 -- Records of infra_codegen_column
@@ -617,7 +431,7 @@ CREATE TABLE `infra_codegen_table`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 131 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 132 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
 
 -- ----------------------------
 -- Records of infra_codegen_table
@@ -782,14 +596,17 @@ CREATE TABLE `infra_job`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
+) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
 
 -- ----------------------------
 -- Records of infra_job
 -- ----------------------------
 BEGIN;
-INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, '支付通知 Job', 1, 'payNotifyJob', NULL, '* * * * * ?', 0, 0, 0, '1', '2021-10-27 08:34:42', '1', '2022-11-24 23:01:35', b'0');
+INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, '支付通知 Job', 2, 'payNotifyJob', NULL, '* * * * * ?', 0, 0, 0, '1', '2021-10-27 08:34:42', '1', '2023-07-09 20:51:41', b'0');
 INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 'Job 示例', 1, 'demoJob', NULL, '* * * L * ?', 1, 1, 0, '1', '2022-09-24 22:31:41', '1', '2022-09-24 22:31:42', b'0');
+INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, '支付订单同步 Job', 2, 'payOrderSyncJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-22 14:36:26', '1', '2023-07-22 15:39:08', b'0');
+INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (18, '支付订单过期 Job', 2, 'payOrderExpireJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-22 15:36:23', '1', '2023-07-22 15:39:54', b'0');
+INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (19, '退款订单的同步 Job', 2, 'payRefundSyncJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-23 21:03:44', '1', '2023-07-23 21:09:00', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -813,7 +630,7 @@ CREATE TABLE `infra_job_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 161 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
 
 -- ----------------------------
 -- Records of infra_job_log
@@ -936,7 +753,7 @@ CREATE TABLE `system_dict_data`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1341 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1348 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
 
 -- ----------------------------
 -- Records of system_dict_data
@@ -1012,33 +829,22 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (86, 0, '病假', '1', 'bpm_oa_leave_type', 0, 'primary', '', NULL, '1', '2021-09-21 22:35:28', '1', '2022-02-16 10:00:41', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (87, 1, '事假', '2', 'bpm_oa_leave_type', 0, 'info', '', NULL, '1', '2021-09-21 22:36:11', '1', '2022-02-16 10:00:49', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (88, 2, '婚假', '3', 'bpm_oa_leave_type', 0, 'warning', '', NULL, '1', '2021-09-21 22:36:38', '1', '2022-02-16 10:00:53', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (98, 1, 'v2', 'v2', 'pay_channel_wechat_version', 0, '', '', 'v2版本', '1', '2021-11-08 17:00:58', '1', '2021-11-08 17:00:58', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (99, 2, 'v3', 'v3', 'pay_channel_wechat_version', 0, '', '', 'v3版本', '1', '2021-11-08 17:01:07', '1', '2021-11-08 17:01:07', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (108, 1, 'RSA2', 'RSA2', 'pay_channel_alipay_sign_type', 0, '', '', 'RSA2', '1', '2021-11-18 15:39:29', '1', '2021-11-18 15:39:29', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (109, 1, '公钥模式', '1', 'pay_channel_alipay_mode', 0, '', '', '公钥模式:privateKey + alipayPublicKey', '1', '2021-11-18 15:45:23', '1', '2021-11-18 15:45:23', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (110, 2, '证书模式', '2', 'pay_channel_alipay_mode', 0, '', '', '证书模式:appCertContent + alipayPublicCertContent + rootCertContent', '1', '2021-11-18 15:45:40', '1', '2021-11-18 15:45:40', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (111, 1, '线上', 'https://openapi.alipay.com/gateway.do', 'pay_channel_alipay_server_type', 0, '', '', '网关地址 - 线上', '1', '2021-11-18 16:59:32', '1', '2021-11-21 17:37:29', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (112, 2, '沙箱', 'https://openapi.alipaydev.com/gateway.do', 'pay_channel_alipay_server_type', 0, '', '', '网关地址 - 沙箱', '1', '2021-11-18 16:59:48', '1', '2021-11-21 17:37:39', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (113, 1, '微信 JSAPI 支付', 'wx_pub', 'pay_channel_code_type', 0, '', '', '微信 JSAPI(公众号) 支付', '1', '2021-12-03 10:40:24', '1', '2021-12-04 16:41:00', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (114, 2, '微信小程序支付', 'wx_lite', 'pay_channel_code_type', 0, '', '', '微信小程序支付', '1', '2021-12-03 10:41:06', '1', '2021-12-03 10:41:06', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (115, 3, '微信 App 支付', 'wx_app', 'pay_channel_code_type', 0, '', '', '微信 App 支付', '1', '2021-12-03 10:41:20', '1', '2021-12-03 10:41:20', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (116, 4, '支付宝 PC 网站支付', 'alipay_pc', 'pay_channel_code_type', 0, '', '', '支付宝 PC 网站支付', '1', '2021-12-03 10:42:09', '1', '2021-12-03 10:42:09', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (117, 5, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code_type', 0, '', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2021-12-03 10:42:26', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (118, 6, '支付宝App 支付', 'alipay_app', 'pay_channel_code_type', 0, '', '', '支付宝App 支付', '1', '2021-12-03 10:42:55', '1', '2021-12-03 10:42:55', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (119, 7, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code_type', 0, '', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2021-12-03 10:43:10', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (120, 1, '通知成功', '10', 'pay_order_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2022-02-16 13:59:13', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, 2, '通知失败', '20', 'pay_order_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2022-02-16 13:59:17', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, 3, '未通知', '0', 'pay_order_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2022-02-16 13:59:23', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (123, 1, '支付成功', '10', 'pay_order_status', 0, 'success', '', '支付成功', '1', '2021-12-03 11:18:29', '1', '2022-02-16 15:24:25', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (124, 2, '支付关闭', '20', 'pay_order_status', 0, 'danger', '', '支付关闭', '1', '2021-12-03 11:18:42', '1', '2022-02-16 15:24:31', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (125, 3, '未支付', '0', 'pay_order_status', 0, 'info', '', '未支付', '1', '2021-12-03 11:18:18', '1', '2022-02-16 15:24:35', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (126, 1, '未退款', '0', 'pay_order_refund_status', 0, '', '', '未退款', '1', '2021-12-03 11:30:35', '1', '2021-12-03 11:34:05', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (127, 2, '部分退款', '10', 'pay_order_refund_status', 0, '', '', '部分退款', '1', '2021-12-03 11:30:44', '1', '2021-12-03 11:34:10', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (128, 3, '全部退款', '20', 'pay_order_refund_status', 0, '', '', '全部退款', '1', '2021-12-03 11:30:52', '1', '2021-12-03 11:34:14', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1117, 1, '退款订单生成', '0', 'pay_refund_order_status', 0, 'primary', '', '退款订单生成', '1', '2021-12-10 16:44:44', '1', '2022-02-16 14:05:24', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1118, 2, '退款成功', '1', 'pay_refund_order_status', 0, 'success', '', '退款成功', '1', '2021-12-10 16:44:59', '1', '2022-02-16 14:05:28', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1119, 3, '退款失败', '2', 'pay_refund_order_status', 0, 'danger', '', '退款失败', '1', '2021-12-10 16:45:10', '1', '2022-02-16 14:05:34', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1124, 8, '退款关闭', '99', 'pay_refund_order_status', 0, 'info', '', '退款关闭', '1', '2021-12-10 16:46:26', '1', '2022-02-16 14:05:40', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (113, 1, '微信公众号支付', 'wx_pub', 'pay_channel_code', 0, 'success', '', '微信公众号支付', '1', '2021-12-03 10:40:24', '1', '2023-07-19 20:08:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (114, 2, '微信小程序支付', 'wx_lite', 'pay_channel_code', 0, 'success', '', '微信小程序支付', '1', '2021-12-03 10:41:06', '1', '2023-07-19 20:08:50', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (115, 3, '微信 App 支付', 'wx_app', 'pay_channel_code', 0, 'success', '', '微信 App 支付', '1', '2021-12-03 10:41:20', '1', '2023-07-19 20:08:56', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (116, 10, '支付宝 PC 网站支付', 'alipay_pc', 'pay_channel_code', 0, 'primary', '', '支付宝 PC 网站支付', '1', '2021-12-03 10:42:09', '1', '2023-07-19 20:09:12', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (123, 10, '支付成功', '10', 'pay_order_status', 0, 'success', '', '支付成功', '1', '2021-12-03 11:18:29', '1', '2023-07-19 18:04:28', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (124, 30, '支付关闭', '30', 'pay_order_status', 0, 'info', '', '支付关闭', '1', '2021-12-03 11:18:42', '1', '2023-07-19 18:05:07', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (125, 0, '等待支付', '0', 'pay_order_status', 0, 'info', '', '未支付', '1', '2021-12-03 11:18:18', '1', '2023-07-19 18:04:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1118, 0, '等待退款', '0', 'pay_refund_status', 0, 'info', '', '等待退款', '1', '2021-12-10 16:44:59', '1', '2023-07-19 10:14:39', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1119, 20, '退款失败', '20', 'pay_refund_status', 0, 'danger', '', '退款失败', '1', '2021-12-10 16:45:10', '1', '2023-07-19 10:15:10', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1124, 10, '退款成功', '10', 'pay_refund_status', 0, 'success', '', '退款成功', '1', '2021-12-10 16:46:26', '1', '2023-07-19 10:15:00', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1125, 0, '默认', '1', 'bpm_model_category', 0, 'primary', '', '流程分类 - 默认', '1', '2022-01-02 08:41:11', '1', '2022-02-16 20:01:42', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1126, 0, 'OA', '2', 'bpm_model_category', 0, 'success', '', '流程分类 - OA', '1', '2022-01-02 08:41:22', '1', '2022-02-16 20:01:50', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1127, 0, '进行中', '1', 'bpm_process_instance_status', 0, 'primary', '', '流程实例的状态 - 进行中', '1', '2022-01-07 23:47:22', '1', '2022-02-16 20:07:49', b'0');
@@ -1061,9 +867,6 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1144, 21, '流程发起人的二级领导', '21', 'bpm_task_assign_script', 0, '', '', '任务分配自定义脚本 - 流程发起人的二级领导', '103', '2022-01-15 21:24:46', '103', '2022-01-15 21:24:57', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1145, 1, '管理后台', '1', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 管理后台', '1', '2022-02-02 13:15:06', '1', '2022-03-10 16:32:59', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1146, 2, '用户 APP', '2', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 用户 APP', '1', '2022-02-02 13:15:19', '1', '2022-03-10 16:33:03', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1147, 0, '未退款', '0', 'pay_refund_order_type', 0, 'info', '', '退款类型 - 未退款', '1', '2022-02-16 14:09:01', '1', '2022-02-16 14:09:01', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1148, 10, '部分退款', '10', 'pay_refund_order_type', 0, 'success', '', '退款类型 - 部分退款', '1', '2022-02-16 14:09:25', '1', '2022-02-16 14:11:38', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1149, 20, '全部退款', '20', 'pay_refund_order_type', 0, 'warning', '', '退款类型 - 全部退款', '1', '2022-02-16 14:11:33', '1', '2022-02-16 14:11:33', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1150, 1, '数据库', '1', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:25:28', '1', '2022-03-15 00:25:28', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1151, 10, '本地磁盘', '10', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:25:41', '1', '2022-03-15 00:25:56', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1152, 11, 'FTP 服务器', '11', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:26:06', '1', '2022-03-15 00:26:10', b'0');
@@ -1142,8 +945,8 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1226, 30, '不发送', '30', 'system_mail_send_status', 0, 'info', '', '邮件发送状态 -  不发送', '1', '2023-01-26 09:55:06', '1', '2023-01-26 16:36:36', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1227, 1, '通知公告', '1', 'system_notify_template_type', 0, 'primary', '', '站内信模版的类型 - 通知公告', '1', '2023-01-28 10:35:59', '1', '2023-01-28 10:35:59', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1228, 2, '系统消息', '2', 'system_notify_template_type', 0, 'success', '', '站内信模版的类型 - 系统消息', '1', '2023-01-28 10:36:20', '1', '2023-01-28 10:36:25', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1229, 0, '模拟支付', 'mock', 'pay_channel_code_type', 0, 'default', '', NULL, '1', '2023-02-12 21:50:22', '1', '2023-02-12 21:50:22', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1230, 8, '支付宝条码支付', 'alipay_bar', 'pay_channel_code_type', 0, 'default', '', NULL, '1', '2023-02-18 23:32:24', '1', '2023-02-18 23:32:32', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1229, 0, '模拟支付', 'mock', 'pay_channel_code', 0, 'default', '', '模拟支付', '1', '2023-02-12 21:50:22', '1', '2023-07-10 10:11:02', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1230, 13, '支付宝条码支付', 'alipay_bar', 'pay_channel_code', 0, 'primary', '', '支付宝条码支付', '1', '2023-02-18 23:32:24', '1', '2023-07-19 20:09:23', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1231, 10, 'Vue2 Element UI 标准模版', '10', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:03:55', '1', '2023-04-13 00:03:55', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1232, 20, 'Vue3 Element Plus 标准模版', '20', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:04:08', '1', '2023-04-13 00:04:08', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1233, 21, 'Vue3 Element Plus Schema 模版', '21', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:04:26', '1', '2023-04-13 00:04:26', b'0');
@@ -1166,6 +969,13 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1338, 2, '冻结期', '2', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:16:58', '1', '2023-06-28 13:48:36', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1339, 3, '完成', '3', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:07', '1', '2023-06-28 13:48:38', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1340, 4, '失效(订单退款)', '4', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:21', '1', '2023-06-28 13:48:42', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1341, 20, '已退款', '20', 'pay_order_status', 0, 'danger', '', '已退款', '1', '2023-07-19 18:05:37', '1', '2023-07-19 18:05:37', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1342, 21, '请求成功,但是结果失败', '21', 'pay_notify_status', 0, 'warning', '', '请求成功,但是结果失败', '1', '2023-07-19 18:10:47', '1', '2023-07-19 18:11:38', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1343, 22, '请求失败', '22', 'pay_notify_status', 0, 'warning', '', NULL, '1', '2023-07-19 18:11:05', '1', '2023-07-19 18:11:27', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1344, 4, '微信扫码支付', 'wx_native', 'pay_channel_code', 0, 'success', '', '微信扫码支付', '1', '2023-07-19 20:07:47', '1', '2023-07-19 20:09:03', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1345, 5, '微信条码支付', 'wx_bar', 'pay_channel_code', 0, 'success', '', '微信条码支付\n', '1', '2023-07-19 20:08:06', '1', '2023-07-19 20:09:08', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1346, 1, '支付单', '1', 'pay_notify_type', 0, 'primary', '', '支付单', '1', '2023-07-20 12:23:17', '1', '2023-07-20 12:23:17', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1347, 2, '退款单', '2', 'pay_notify_type', 0, 'danger', '', NULL, '1', '2023-07-20 12:23:26', '1', '2023-07-20 12:23:26', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1186,7 +996,7 @@ CREATE TABLE `system_dict_type`  (
   `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 173 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 174 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
@@ -1212,16 +1022,10 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (115, '错误码的类型', 'system_error_code_type', 0, NULL, '1', '2021-04-21 00:06:30', '1', '2022-02-01 16:36:49', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (116, '登陆日志的类型', 'system_login_type', 0, '登陆日志的类型', '1', '2021-10-06 00:50:46', '1', '2022-02-01 16:35:56', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (117, 'OA 请假类型', 'bpm_oa_leave_type', 0, NULL, '1', '2021-09-21 22:34:33', '1', '2022-01-22 10:41:37', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (122, '支付渠道微信版本', 'pay_channel_wechat_version', 0, '支付渠道微信版本', '1', '2021-11-08 17:00:26', '1', '2021-11-08 17:00:26', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (127, '支付渠道支付宝算法类型', 'pay_channel_alipay_sign_type', 0, '支付渠道支付宝算法类型', '1', '2021-11-18 15:39:09', '1', '2021-11-18 15:39:09', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (128, '支付渠道支付宝公钥类型', 'pay_channel_alipay_mode', 0, '支付渠道支付宝公钥类型', '1', '2021-11-18 15:44:28', '1', '2021-11-18 15:44:28', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (129, '支付宝网关地址', 'pay_channel_alipay_server_type', 0, '支付宝网关地址', '1', '2021-11-18 16:58:55', '1', '2021-11-18 17:01:34', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (130, '支付渠道编码类型', 'pay_channel_code_type', 0, '支付渠道的编码', '1', '2021-12-03 10:35:08', '1', '2021-12-03 10:35:08', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (131, '支付订单回调状态', 'pay_order_notify_status', 0, '支付订单回调状态', '1', '2021-12-03 10:53:29', '1', '2021-12-03 10:53:29', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (130, '支付渠道编码类型', 'pay_channel_code', 0, '支付渠道的编码', '1', '2021-12-03 10:35:08', '1', '2023-07-10 10:11:39', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (131, '支付回调状态', 'pay_notify_status', 0, '支付回调状态(包括退款回调)', '1', '2021-12-03 10:53:29', '1', '2023-07-19 18:09:43', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (132, '支付订单状态', 'pay_order_status', 0, '支付订单状态', '1', '2021-12-03 11:17:50', '1', '2021-12-03 11:17:50', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (133, '支付订单退款状态', 'pay_order_refund_status', 0, '支付订单退款状态', '1', '2021-12-03 11:27:31', '1', '2021-12-03 11:27:31', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (134, '退款订单状态', 'pay_refund_order_status', 0, '退款订单状态', '1', '2021-12-10 16:42:50', '1', '2021-12-10 16:42:50', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (135, '退款订单类别', 'pay_refund_order_type', 0, '退款订单类别', '1', '2021-12-10 17:14:53', '1', '2021-12-10 17:14:53', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (134, '退款订单状态', 'pay_refund_status', 0, '退款订单状态', '1', '2021-12-10 16:42:50', '1', '2023-07-19 10:13:17', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (138, '流程分类', 'bpm_model_category', 0, '流程分类', '1', '2022-01-02 08:40:45', '1', '2022-01-02 08:40:45', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (139, '流程实例的状态', 'bpm_process_instance_status', 0, '流程实例的状态', '1', '2022-01-07 23:46:42', '1', '2022-01-07 23:46:42', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (140, '流程实例的结果', 'bpm_process_instance_result', 0, '流程实例的结果', '1', '2022-01-07 23:48:10', '1', '2022-01-07 23:48:10', b'0', NULL);
@@ -1256,6 +1060,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (170, '快递计费方式', 'trade_delivery_express_charge_mode', 0, '用于商城交易模块配送管理', '1', '2023-05-21 22:45:03', '1', '2023-05-21 22:45:03', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (171, '积分业务类型', 'member_point_biz_type', 0, '', '1', '2023-06-10 12:15:00', '1', '2023-06-28 13:48:20', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (172, '积分订单状态', 'member_point_status', 0, '', '1', '2023-06-10 12:16:27', '1', '2023-06-28 13:48:17', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (173, '支付通知类型', 'pay_notify_type', 0, NULL, '1', '2023-07-20 12:23:03', '1', '2023-07-20 12:23:03', b'0', '1970-01-01 00:00:00');
 COMMIT;
 
 -- ----------------------------
@@ -1304,7 +1109,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2223 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -1434,7 +1239,7 @@ CREATE TABLE `system_menu`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2301 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
+) ENGINE = InnoDB AUTO_INCREMENT = 2303 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
 
 -- ----------------------------
 -- Records of system_menu
@@ -1568,7 +1373,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1118, '请假查询', '', 2, 0, 5, 'leave', 'user', 'bpm/oa/leave/index', 'BpmOALeave', 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2023-04-08 11:30:40', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1119, '请假申请查询', 'bpm:oa-leave:query', 3, 1, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1120, '请假申请创建', 'bpm:oa-leave:create', 3, 2, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1126, '应用信息', '', 2, 1, 1117, 'app', 'table', 'pay/app/index', 'PayMerchant', 0, b'1', b'1', b'1', '', '2021-11-10 01:13:30', '1', '2023-04-08 10:43:14', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1126, '应用信息', '', 2, 1, 1117, 'app', 'table', 'pay/app/index', 'PayApp', 0, b'1', b'1', b'1', '', '2021-11-10 01:13:30', '1', '2023-07-20 12:13:32', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1127, '支付应用信息查询', 'pay:app:query', 3, 1, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1128, '支付应用信息创建', 'pay:app:create', 3, 2, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1129, '支付应用信息更新', 'pay:app:update', 3, 3, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0');
@@ -1846,6 +1651,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2297, '用户签到积分删除', 'point:sign-in-record:delete', 3, 4, 2293, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2299, '会员积分', '', 1, 1, 2262, 'point', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:48:51', '1', '2023-06-27 22:48:51', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2300, '会员签到', '', 1, 2, 2262, 'signin', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:49:53', '1', '2023-06-27 22:49:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2301, '回调通知', '', 2, 4, 1117, 'notify', 'example', 'pay/notify/index', 'PayNotify', 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '1', '2023-07-20 13:45:08', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2302, '支付通知查询', 'pay:notify:query', 3, 1, 2301, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '', '2023-07-20 04:41:32', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1962,7 +1769,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2007 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 2231 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2084,7 +1891,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 804 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2124,7 +1931,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6601 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 7134 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -3415,7 +3222,7 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://test.yudao.iocoder.cn/e1fdd7271685ec143a0900681606406621717a666ad0b2798b096df41422b32f.png', 0, '127.0.0.1', '2023-07-09 12:19:37', 'admin', '2021-01-05 17:03:47', NULL, '2023-07-09 12:19:37', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://test.yudao.iocoder.cn/e1fdd7271685ec143a0900681606406621717a666ad0b2798b096df41422b32f.png', 0, '127.0.0.1', '2023-07-24 08:41:23', 'admin', '2021-01-05 17:03:47', NULL, '2023-07-24 08:41:23', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '127.0.0.1', '2022-05-28 15:43:17', '', '2021-01-21 02:13:53', NULL, '2022-07-09 09:00:33', b'0', 1);

+ 18 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -27,7 +27,7 @@ public interface PayClient {
      * 调用支付渠道,统一下单
      *
      * @param reqDTO 下单信息
-     * @return 各支付渠道的返回结果
+     * @return 支付订单信息
      */
     PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
 
@@ -40,6 +40,14 @@ public interface PayClient {
      */
     PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
 
+    /**
+     * 获得支付订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @return 支付订单信息
+     */
+    PayOrderRespDTO getOrder(String outTradeNo);
+
     // ============ 退款相关 ==========
 
     /**
@@ -59,4 +67,13 @@ public interface PayClient {
      */
     PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
 
+    /**
+     * 获得退款订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @param outRefundNo 外部退款号
+     * @return 退款订单信息
+     */
+    PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
+
 }

+ 23 - 21
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java

@@ -94,38 +94,40 @@ public class PayOrderRespDTO {
     /**
      * 创建【SUCCESS】状态的订单返回
      */
-    public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime,
-                           String outTradeNo, Object rawData) {
-        this.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
-        this.channelOrderNo = channelOrderNo;
-        this.channelUserId = channelUserId;
-        this.successTime = successTime;
+    public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
+                                            String outTradeNo, Object rawData) {
+        PayOrderRespDTO respDTO = new PayOrderRespDTO();
+        respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
+        respDTO.channelOrderNo = channelOrderNo;
+        respDTO.channelUserId = channelUserId;
+        respDTO.successTime = successTime;
         // 相对通用的字段
-        this.outTradeNo = outTradeNo;
-        this.rawData = rawData;
+        respDTO.outTradeNo = outTradeNo;
+        respDTO.rawData = rawData;
+        return respDTO;
     }
 
     /**
-     * 创建【SUCCESS】或【CLOSED】状态的订单返回,适合支付渠道回调时
+     * 创建指定状态的订单返回,适合支付渠道回调时
      */
-    public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
-                           String outTradeNo, Object rawData) {
-        this.status = status;
-        this.channelOrderNo = channelOrderNo;
-        this.channelUserId = channelUserId;
-        this.successTime = successTime;
+    public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
+                                     String outTradeNo, Object rawData) {
+        PayOrderRespDTO respDTO = new PayOrderRespDTO();
+        respDTO.status = status;
+        respDTO.channelOrderNo = channelOrderNo;
+        respDTO.channelUserId = channelUserId;
+        respDTO.successTime = successTime;
         // 相对通用的字段
-        this.outTradeNo = outTradeNo;
-        this.rawData = rawData;
+        respDTO.outTradeNo = outTradeNo;
+        respDTO.rawData = rawData;
+        return respDTO;
     }
 
     /**
      * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时
-     *
-     * 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突,所以独立个方法出来
      */
-    public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg,
-                                        String outTradeNo, Object rawData) {
+    public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
+                                           String outTradeNo, Object rawData) {
         PayOrderRespDTO respDTO = new PayOrderRespDTO();
         respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
         respDTO.channelErrorCode = channelErrorCode;

+ 31 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -94,7 +94,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
             throws Throwable;
 
     @Override
-    public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
+    public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
         try {
             return doParseOrderNotify(params, body);
         } catch (Throwable ex) {
@@ -107,10 +107,24 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
             throws Throwable;
 
+    @Override
+    public final PayOrderRespDTO getOrder(String outTradeNo) {
+        try {
+            return doGetOrder(outTradeNo);
+        } catch (Throwable ex) {
+            log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
+                    getId(), outTradeNo, ex);
+            throw buildPayException(ex);
+        }
+    }
+
+    protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
+            throws Throwable;
+
     // ============ 退款相关 ==========
 
     @Override
-    public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
+    public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
         ValidationUtils.validate(reqDTO);
         // 执行统一退款
         PayRefundRespDTO resp;
@@ -131,7 +145,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
 
     @Override
-    public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
+    public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
         try {
             return doParseRefundNotify(params, body);
         } catch (Throwable ex) {
@@ -144,6 +158,20 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
             throws Throwable;
 
+    @Override
+    public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
+        try {
+            return doGetRefund(outTradeNo, outRefundNo);
+        } catch (Throwable ex) {
+            log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
+                    getId(), outTradeNo, outRefundNo, ex);
+            throw buildPayException(ex);
+        }
+    }
+
+    protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
+            throws Throwable;
+
     // ========== 各种工具方法 ==========
 
     private PayException buildPayException(Throwable ex) {

+ 75 - 13
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java

@@ -17,15 +17,22 @@ import com.alipay.api.AlipayApiException;
 import com.alipay.api.AlipayConfig;
 import com.alipay.api.AlipayResponse;
 import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel;
+import com.alipay.api.domain.AlipayTradeQueryModel;
 import com.alipay.api.domain.AlipayTradeRefundModel;
 import com.alipay.api.internal.util.AlipaySignature;
+import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
+import com.alipay.api.request.AlipayTradeQueryRequest;
 import com.alipay.api.request.AlipayTradeRefundRequest;
+import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
+import com.alipay.api.response.AlipayTradeQueryResponse;
 import com.alipay.api.response.AlipayTradeRefundResponse;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Supplier;
@@ -63,7 +70,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
      */
     protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
         Assert.isFalse(response.isSuccess());
-        return PayOrderRespDTO.build(response.getSubCode(), response.getSubMsg(),
+        return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
                 reqDTO.getOutTradeNo(), response);
     }
 
@@ -76,10 +83,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
 
         // 2. 解析订单的状态
         // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
-        String tradeStatus = bodyObj.get("trade_status");
-        Integer status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
-                : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
-                : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
+        Integer status = parseStatus(bodyObj.get("trade_status"));
         // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
         if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
             status = PayOrderStatusRespEnum.REFUND.getStatus();
@@ -87,10 +91,40 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         Assert.notNull(status, (Supplier<Throwable>) () -> {
             throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
         });
-        return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
+        return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
                 bodyObj.get("out_trade_no"), body);
     }
 
+    @Override
+    protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
+        // 1.1 构建 AlipayTradeRefundModel 请求
+        AlipayTradeQueryModel model = new AlipayTradeQueryModel();
+        model.setOutTradeNo(outTradeNo);
+        // 1.2 构建 AlipayTradeQueryRequest 请求
+        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayTradeQueryResponse response =  client.execute(request);
+        if (!response.isSuccess()) { // 不成功,例如说订单不存在
+            return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    outTradeNo, response);
+        }
+        // 2.2 解析订单的状态
+        Integer status = parseStatus(response.getTradeStatus());
+        Assert.notNull(status, (Supplier<Throwable>) () -> {
+            throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
+        });
+        return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
+                outTradeNo, response);
+    }
+
+    private static Integer parseStatus(String tradeStatus) {
+        return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
+                : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
+                : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
+    }
+
     // ============ 退款相关 ==========
 
     /**
@@ -112,16 +146,15 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         request.setBizModel(model);
 
         // 2.1 执行请求
-        AlipayTradeRefundResponse response =  client.execute(request);
+        AlipayTradeRefundResponse response = client.execute(request);
+        if (!response.isSuccess()) {
+            return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
+        }
         // 2.2 创建返回结果
         // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
         // 另外,支付宝没有退款单号,所以不用设置
-        if (response.isSuccess()) {
-            return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
-                    reqDTO.getOutRefundNo(), response);
-        } else {
-            return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
-        }
+        return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
+                reqDTO.getOutRefundNo(), response);
     }
 
     @Override
@@ -134,6 +167,35 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         throw new UnsupportedOperationException("支付宝无退款回调");
     }
 
+    @Override
+    protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
+        // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
+        AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
+        model.setOutTradeNo(outTradeNo);
+        model.setOutRequestNo(outRefundNo);
+        model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
+        // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
+        AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayTradeFastpayRefundQueryResponse response = client.execute(request);
+        if (!response.isSuccess()) {
+            // 明确不存在的情况,应该就是失败,可进行关闭
+            if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
+                return PayRefundRespDTO.failureOf(outRefundNo, response);
+            }
+            // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待
+            return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
+        }
+        // 2.2 创建返回结果
+        if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
+            return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
+                    outRefundNo, response);
+        }
+        return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
+    }
+
     // ========== 各种工具方法 ==========
 
     protected String formatAmount(Integer amount) {

+ 154 - 13
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.date.TemporalAccessorUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
@@ -16,10 +18,8 @@ import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
-import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
-import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
-import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
-import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
+import com.github.binarywang.wxpay.bean.request.*;
+import com.github.binarywang.wxpay.bean.result.*;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
@@ -90,7 +90,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         } catch (WxPayException e) {
             String errorCode = getErrorCode(e);
             String errorMessage = getErrorMessage(e);
-            return PayOrderRespDTO.build(errorCode, errorMessage,
+            return PayOrderRespDTO.closedOf(errorCode, errorMessage,
                     reqDTO.getOutTradeNo(), e.getXmlString());
         }
     }
@@ -115,7 +115,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
 
     @Override
     public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
-        // 微信支付 v2 回调结果处理
         switch (config.getApiVersion()) {
             case API_VERSION_V2:
                 return doParseOrderNotifyV2(body);
@@ -130,10 +129,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         // 1. 解析回调
         WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
         // 2. 构建结果
-        // 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
+        // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
         Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
                 PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
-        return new PayOrderRespDTO(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+        return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
                 response.getOutTradeNo(), body);
     }
 
@@ -142,14 +141,79 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
         WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
         // 2. 构建结果
-        // 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
-        Integer status = Objects.equals(result.getTradeState(), "SUCCESS") ?
-                PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
+        Integer status = parseStatus(result.getTradeState());
         String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
-        return new PayOrderRespDTO(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
+        return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
                 result.getOutTradeNo(), body);
     }
 
+    @Override
+    protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
+        try {
+            switch (config.getApiVersion()) {
+                case API_VERSION_V2:
+                    return doGetOrderV2(outTradeNo);
+                case WxPayClientConfig.API_VERSION_V3:
+                    return doGetOrderV3(outTradeNo);
+                default:
+                    throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+            }
+        } catch (WxPayException e) {
+            if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
+                String errorCode = getErrorCode(e);
+                String errorMessage = getErrorMessage(e);
+                return PayOrderRespDTO.closedOf(errorCode, errorMessage,
+                        outTradeNo, e.getXmlString());
+            }
+            throw e;
+        }
+    }
+
+    private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
+                .outTradeNo(outTradeNo).build();
+        // 执行请求
+        WxPayOrderQueryResult response = client.queryOrder(request);
+
+        // 转换结果
+        Integer status = parseStatus(response.getTradeState());
+        return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+                outTradeNo, response);
+    }
+
+    private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
+                .setOutTradeNo(outTradeNo);
+        // 执行请求
+        WxPayOrderQueryV3Result response = client.queryOrderV3(request);
+
+        // 转换结果
+        Integer status = parseStatus(response.getTradeState());
+        String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
+        return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
+                outTradeNo, response);
+    }
+
+    private static Integer parseStatus(String tradeState) {
+        switch (tradeState) {
+            case "NOTPAY":
+            case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
+                return PayOrderStatusRespEnum.WAITING.getStatus();
+            case "SUCCESS":
+                return PayOrderStatusRespEnum.SUCCESS.getStatus();
+            case "REFUND":
+                return PayOrderStatusRespEnum.REFUND.getStatus();
+            case "CLOSED":
+            case "REVOKED": // 已撤销(刷卡支付独有)
+            case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
+                return PayOrderStatusRespEnum.CLOSED.getStatus();
+            default:
+                throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
+        }
+    }
+
     // ============ 退款相关 ==========
 
     @Override
@@ -183,7 +247,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         // 2.1 执行请求
         WxPayRefundResult response = client.refundV2(request);
         // 2.2 创建返回结果
-        if (Objects.equals("SUCCESS", response.getResultCode())) {
+        if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
             return PayRefundRespDTO.waitingOf(response.getRefundId(),
                     reqDTO.getOutRefundNo(), response);
         }
@@ -249,6 +313,83 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
     }
 
+    @Override
+    protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
+        try {
+            switch (config.getApiVersion()) {
+                case API_VERSION_V2:
+                    return doGetRefundV2(outTradeNo, outRefundNo);
+                case WxPayClientConfig.API_VERSION_V3:
+                    return doGetRefundV3(outTradeNo, outRefundNo);
+                default:
+                    throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+            }
+        } catch (WxPayException e) {
+            if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
+                String errorCode = getErrorCode(e);
+                String errorMessage = getErrorMessage(e);
+                return PayRefundRespDTO.failureOf(errorCode, errorMessage,
+                        outRefundNo, e.getXmlString());
+            }
+            throw e;
+        }
+    }
+
+    private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
+        // 1. 构建 WxPayRefundRequest 请求
+        WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
+                .outTradeNo(outTradeNo)
+                .outRefundNo(outRefundNo)
+                .build();
+        // 2.1 执行请求
+        WxPayRefundQueryResult response = client.refundQuery(request);
+        // 2.2 创建返回结果
+        if (!Objects.equals("SUCCESS", response.getResultCode())) {
+            return PayRefundRespDTO.waitingOf(null,
+                    outRefundNo, response);
+        }
+        WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
+                record -> record.getOutRefundNo().equals(outRefundNo));
+        if (refund == null) {
+            return PayRefundRespDTO.failureOf(outRefundNo, response);
+        }
+        switch (refund.getRefundStatus()) {
+            case "SUCCESS":
+                return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
+                        outRefundNo, response);
+            case "PROCESSING":
+                return PayRefundRespDTO.waitingOf(refund.getRefundId(),
+                        outRefundNo, response);
+            case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
+            case "FAIL":
+                return PayRefundRespDTO.failureOf(outRefundNo, response);
+            default:
+                throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
+        }
+    }
+
+    private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
+        // 1. 构建 WxPayRefundRequest 请求
+        WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
+        request.setOutRefundNo(outRefundNo);
+        // 2.1 执行请求
+        WxPayRefundQueryV3Result response = client.refundQueryV3(request);
+        // 2.2 创建返回结果
+        switch (response.getStatus()) {
+            case "SUCCESS":
+                return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
+                        outRefundNo, response);
+            case "PROCESSING":
+                return PayRefundRespDTO.waitingOf(response.getRefundId(),
+                        outRefundNo, response);
+            case "ABNORMAL": // 退款异常
+            case "CLOSED":
+                return PayRefundRespDTO.failureOf(outRefundNo, response);
+            default:
+                throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
+        }
+    }
+
     // ========== 各种工具方法 ==========
 
     static String formatDateV2(LocalDateTime time) {

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java

@@ -68,7 +68,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
             try {
                 WxPayMicropayResult response = client.micropay(request);
                 // 支付成功,例如说:1)用户输入了密码;2)用户免密支付
-                return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+                return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
                         response.getOutTradeNo(), response)
                         .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
             } catch (WxPayException ex) {

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java

@@ -4,6 +4,7 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;

+ 10 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java

@@ -33,6 +33,16 @@ public enum PayOrderStatusRespEnum {
         return Objects.equals(status, SUCCESS.getStatus());
     }
 
+    /**
+     * 判断是否已退款
+     *
+     * @param status 状态
+     * @return 是否支付成功
+     */
+    public static boolean isRefund(Integer status) {
+        return Objects.equals(status, REFUND.getStatus());
+    }
+
     /**
      * 判断是否支付关闭
      *

+ 0 - 2
yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java

@@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration
 import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
 import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
 import org.redisson.spring.starter.RedissonAutoConfiguration;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -41,7 +40,6 @@ public class BaseDbAndRedisUnitTest {
 
             // Redis 配置类
             RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
-            RedisAutoConfiguration.class, // Spring Redis 自动配置类
             YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
             RedissonAutoConfiguration.class, // Redisson 自动高配置类
     })

+ 1 - 1
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/address/MemberAddressDO.java

@@ -39,7 +39,7 @@ public class MemberAddressDO extends BaseDO {
     /**
      * 地区编号
      */
-    private Integer areaId;
+    private Long areaId;
     /**
      * 收件详细地址
      */

+ 1 - 2
yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql

@@ -23,9 +23,8 @@ CREATE TABLE IF NOT EXISTS "member_address" (
     "name" varchar(10) NOT NULL,
     "mobile" varchar(20) NOT NULL,
     "area_id" bigint(20) NOT NULL,
-    "post_code" varchar(16) NOT NULL,
     "detail_address" varchar(250) NOT NULL,
-    "defaulted" bit NOT NULL,
+    "default_status" bit NOT NULL,
     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "creator" varchar(64) DEFAULT '',
     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

+ 2 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java

@@ -23,7 +23,7 @@ public interface ErrorCodeConstants {
     // ========== ORDER 模块 1007002000 ==========
     ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
     ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
-    ErrorCode ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
+    ErrorCode ORDER_STATUS_IS_SUCCESS = new ErrorCode(1007002002, "订单已支付,请刷新页面");
     ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
     ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
     ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款");
@@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
     // ========== ORDER 模块(拓展单) 1007003000 ==========
     ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
     ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
+    ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果");
 
     // ========== 支付模块(退款) 1007006000 ==========
     ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");

+ 0 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java

@@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
-import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -44,8 +43,6 @@ public class PayRefundController {
     private PayRefundService refundService;
     @Resource
     private PayAppService appService;
-    @Resource
-    private PayOrderService orderService;
 
     @GetMapping("/get")
     @Operation(summary = "获得退款订单")

+ 6 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java

@@ -70,6 +70,12 @@ public class PayRefundDO extends BaseDO {
      * 关联 {@link PayOrderDO#getId()}
      */
     private Long orderId;
+    /**
+     * 支付订单编号
+     *
+     * 冗余 {@link PayOrderDO#getNo()}
+     */
+    private String orderNo;
 
     // ========== 商户相关字段 ==========
     /**

+ 13 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java

@@ -5,6 +5,9 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
+import java.util.List;
+
 @Mapper
 public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> {
 
@@ -17,4 +20,14 @@ public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO
                 .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status));
     }
 
+    default List<PayOrderExtensionDO> selectListByOrderId(Long orderId) {
+        return selectList(PayOrderExtensionDO::getOrderId, orderId);
+    }
+
+    default List<PayOrderExtensionDO> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
+        return selectList(new LambdaQueryWrapper<PayOrderExtensionDO>()
+                .eq(PayOrderExtensionDO::getStatus, status)
+                .ge(PayOrderExtensionDO::getCreateTime, minCreateTime));
+    }
+
 }

+ 7 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Mapper
@@ -52,4 +53,10 @@ public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
                 .eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status));
     }
 
+    default List<PayOrderDO> selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) {
+        return selectList(new LambdaQueryWrapper<PayOrderDO>()
+                .eq(PayOrderDO::getStatus, status)
+                .lt(PayOrderDO::getExpireTime, expireTime));
+    }
+
 }

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java

@@ -68,4 +68,8 @@ public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
                 .orderByDesc(PayRefundDO::getId));
     }
 
+    default List<PayRefundDO> selectListByStatus(Integer status) {
+        return selectList(PayRefundDO::getStatus, status);
+    }
+
 }

+ 31 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.pay.job.order;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 支付订单的过期 Job
+ *
+ * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
+ *
+ * @author 芋道源码
+ */
+@Component
+@TenantJob
+public class PayOrderExpireJob implements JobHandler {
+
+    @Resource
+    private PayOrderService orderService;
+
+    @Override
+    public String execute(String param) {
+        int count = orderService.expireOrder();
+        return StrUtil.format("支付过期 {} 个", count);
+    }
+
+}

+ 43 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.pay.job.order;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 支付订单的同步 Job
+ *
+ * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
+ *
+ * @author 芋道源码
+ */
+@Component
+@TenantJob
+public class PayOrderSyncJob implements JobHandler {
+
+    /**
+     * 同步创建时间在 N 分钟之前的订单
+     *
+     * 为什么同步 10 分钟之前的订单?
+     *  因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。
+     *  如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。
+     */
+    private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10);
+
+    @Resource
+    private PayOrderService orderService;
+
+    @Override
+    public String execute(String param) {
+        LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE);
+        int count = orderService.syncOrder(minCreateTime);
+        return StrUtil.format("同步支付订单 {} 个", count);
+    }
+
+}

+ 0 - 4
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 占位,无特殊含义
- */
-package cn.iocoder.yudao.module.pay.job;

+ 31 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/refund/PayRefundSyncJob.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.pay.job.refund;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 退款订单的同步 Job
+ *
+ * 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
+ *
+ * @author 芋道源码
+ */
+@Component
+@TenantJob
+public class PayRefundSyncJob implements JobHandler {
+
+    @Resource
+    private PayRefundService refundService;
+
+    @Override
+    public String execute(String param) {
+        int count = refundService.syncRefund();
+        return StrUtil.format("同步退款订单 {} 个", count);
+    }
+
+}

+ 16 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -105,4 +106,19 @@ public interface PayOrderService {
      */
     PayOrderExtensionDO getOrderExtension(Long id);
 
+    /**
+     * 同步订单的支付状态
+     *
+     * @param minCreateTime 最小创建时间
+     * @return 同步到已支付的订单数量
+     */
+    int syncOrder(LocalDateTime minCreateTime);
+
+    /**
+     * 将已过期的订单,状态修改为已关闭
+     *
+     * @return 过期的订单数量
+     */
+    int expireOrder();
+
 }

+ 172 - 12
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.pay.service.order;
 
-import cn.hutool.core.lang.Pair;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
@@ -33,12 +33,14 @@ import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
 import cn.iocoder.yudao.module.pay.util.MoneyUtils;
+import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;
 
@@ -129,10 +131,10 @@ public class PayOrderServiceImpl implements PayOrderService {
 
     @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
     public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
-        // 1. 获得 PayOrderDO ,并校验其是否存在
+        // 1.1 获得 PayOrderDO ,并校验其是否存在
         PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
-        // 1.2 校验支付渠道是否有效
-        PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
+        // 1.32 校验支付渠道是否有效
+        PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
         PayClient client = payClientFactory.getPayClient(channel.getId());
 
         // 2. 插入 PayOrderExtensionDO
@@ -173,16 +175,52 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (order == null) { // 是否存在
             throw exception(ORDER_NOT_FOUND);
         }
+        if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付
+            throw exception(ORDER_STATUS_IS_SUCCESS);
+        }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
         }
         if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
             throw exception(ORDER_IS_EXPIRED);
         }
+
+        // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常
+        validateOrderActuallyPaid(id);
         return order;
     }
 
-    private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
+    /**
+     * 校验支付订单实际已支付
+     *
+     * @param id 支付编号
+     */
+    @VisibleForTesting
+    void validateOrderActuallyPaid(Long id) {
+        List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(id);
+        orderExtensions.forEach(orderExtension -> {
+            // 情况一:校验数据库中的 orderExtension 是不是已支付
+            if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
+                log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]",
+                        id, orderExtension.getId());
+                throw exception(ORDER_EXTENSION_IS_PAID);
+            }
+            // 情况二:调用三方接口,查询支付单状态,是不是已支付
+            PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+            if (payClient == null) {
+                log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                return;
+            }
+            PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+            if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
+                log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]",
+                        id, toJsonString(respDTO));
+                throw exception(ORDER_EXTENSION_IS_PAID);
+            }
+        });
+    }
+
+    private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) {
         // 校验 App
         appService.validPayApp(appId);
         // 校验支付渠道是否有效
@@ -261,7 +299,7 @@ public class PayOrderServiceImpl implements PayOrderService {
             throw exception(ORDER_EXTENSION_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
-            log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
+            log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId());
             return orderExtension;
         }
         if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
@@ -274,7 +312,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
+        log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId());
         return orderExtension;
     }
 
@@ -295,7 +333,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         }
         if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
                 && Objects.equals(order.getExtensionId(), orderExtension.getId())) {
-            log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
+            log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId());
             return true;
         }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
@@ -313,7 +351,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
+        log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId());
         return false;
     }
 
@@ -328,12 +366,12 @@ public class PayOrderServiceImpl implements PayOrderService {
             throw exception(ORDER_EXTENSION_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新
-            log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId());
+            log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId());
             return;
         }
         // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
         if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
-            log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
+            log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
             return;
         }
         if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
@@ -347,7 +385,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         if (updateCounts == 0) { // 校验状态,必须是待支付
             throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId());
+        log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId());
     }
 
     @Override
@@ -378,6 +416,128 @@ public class PayOrderServiceImpl implements PayOrderService {
         return orderExtensionMapper.selectById(id);
     }
 
+    @Override
+    public int syncOrder(LocalDateTime minCreateTime) {
+        // 1. 查询指定创建时间内的待支付订单
+        List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe(
+                PayOrderStatusEnum.WAITING.getStatus(), minCreateTime);
+        if (CollUtil.isEmpty(orderExtensions)) {
+            return 0;
+        }
+        // 2. 遍历执行
+        int count = 0;
+        for (PayOrderExtensionDO orderExtension : orderExtensions) {
+            count += syncOrder(orderExtension) ? 1 : 0;
+        }
+        return count;
+    }
+
+    /**
+     * 同步单个支付拓展单
+     *
+     * @param orderExtension 支付拓展单
+     * @return 是否已支付
+     */
+    private boolean syncOrder(PayOrderExtensionDO orderExtension) {
+        try {
+            // 1.1 查询支付订单信息
+            PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+            if (payClient == null) {
+                log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                return false;
+            }
+            PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+            // 1.2 回调支付结果
+            notifyOrder(orderExtension.getChannelId(), respDTO);
+
+            // 2. 如果是已支付,则返回 true
+            return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus());
+        } catch (Throwable e) {
+            log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e);
+            return false;
+        }
+    }
+
+    @Override
+    public int expireOrder() {
+        // 1. 查询过期的待支付订单
+        List<PayOrderDO> orders = orderMapper.selectListByStatusAndExpireTimeLt(
+                PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now());
+        if (CollUtil.isEmpty(orders)) {
+            return 0;
+        }
+
+        // 2. 遍历执行
+        int count = 0;
+        for (PayOrderDO order : orders) {
+            count += expireOrder(order) ? 1 : 0;
+        }
+        return count;
+    }
+
+    /**
+     * 同步单个支付单
+     *
+     * @param order 支付单
+     * @return 是否已过期
+     */
+    private boolean expireOrder(PayOrderDO order) {
+        try {
+            // 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单
+            List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId());
+            for (PayOrderExtensionDO orderExtension : orderExtensions) {
+                if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) {
+                    continue;
+                }
+                // 情况一:校验数据库中的 orderExtension 是不是已支付
+                if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
+                    log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]",
+                            order.getId(), orderExtension.getId());
+                    return false;
+                }
+                // 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款
+                PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
+                if (payClient == null) {
+                    log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
+                    return false;
+                }
+                PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
+                if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) {
+                    // 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程
+                    // 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程
+                    log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]",
+                            orderExtension.getId(), toJsonString(respDTO));
+                    return false;
+                }
+                if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
+                    notifyOrder(orderExtension.getChannelId(), respDTO);
+                    return false;
+                }
+                // 兜底逻辑:将支付拓展单更新为已关闭
+                PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                        .setChannelNotifyData(toJsonString(respDTO));
+                if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(),
+                        updateObj) == 0) {
+                    log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId());
+                    return false;
+                }
+                log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId());
+            }
+
+            // 2. 都没有上述情况,可以安心更新为已关闭
+            PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus());
+            if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) {
+                log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
+                return false;
+            }
+            log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
+            return true;
+        } catch (Throwable e) {
+            log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e);
+            return false;
+        }
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

+ 7 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java

@@ -64,4 +64,11 @@ public interface PayRefundService {
      */
     void notifyRefund(Long channelId, PayRefundRespDTO notify);
 
+    /**
+     * 同步渠道退款的退款状态
+     *
+     * @return 同步到状态的退款数量,包括退款成功、退款失败
+     */
+    int syncRefund();
+
 }

+ 106 - 65
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.pay.service.refund;
 
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
@@ -16,10 +16,9 @@ import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
-import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
-import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
@@ -35,12 +34,11 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
-import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 
 /**
  * 退款订单 Service 实现类
@@ -60,6 +58,8 @@ public class PayRefundServiceImpl implements PayRefundService {
 
     @Resource
     private PayRefundMapper refundMapper;
+    @Resource
+    private PayNoRedisDAO noRedisDAO;
 
     @Resource
     private PayOrderService orderService;
@@ -91,7 +91,6 @@ public class PayRefundServiceImpl implements PayRefundService {
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
     public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
         // 1.1 校验 App
         PayAppDO app = appService.validPayApp(reqDTO.getAppId());
@@ -108,12 +107,13 @@ public class PayRefundServiceImpl implements PayRefundService {
         PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
                 app.getId(), reqDTO.getMerchantRefundId());
         if (refund != null) {
-            throw exception(ErrorCodeConstants.REFUND_EXISTS);
+            throw exception(REFUND_EXISTS);
         }
 
         // 2.1 插入退款单
+        String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
         refund = PayRefundConvert.INSTANCE.convert(reqDTO)
-                .setNo(generateRefundNo()).setOrderId(order.getId())
+                .setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo())
                 .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
                 // 商户相关的字段
                 .setNotifyUrl(app.getRefundNotifyUrl())
@@ -123,20 +123,27 @@ public class PayRefundServiceImpl implements PayRefundService {
                 .setStatus(PayRefundStatusEnum.WAITING.getStatus())
                 .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice());
         refundMapper.insert(refund);
-        // 2.2 向渠道发起退款申请
-        PayOrderExtensionDO orderExtension = orderService.getOrderExtension(order.getExtensionId());
-        PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
-                .setPayPrice(order.getPrice())
-                .setRefundPrice(reqDTO.getPrice())
-                .setOutTradeNo(orderExtension.getNo())
-                .setOutRefundNo(refund.getNo())
-                .setNotifyUrl(genChannelRefundNotifyUrl(channel))
-                .setReason(reqDTO.getReason());
-        PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO);
-        // 2.3 处理退款返回
-        notifyRefund(channel, refundRespDTO);
+        try {
+            // 2.2 向渠道发起退款申请
+            PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
+                    .setPayPrice(order.getPrice())
+                    .setRefundPrice(reqDTO.getPrice())
+                    .setOutTradeNo(order.getNo())
+                    .setOutRefundNo(refund.getNo())
+                    .setNotifyUrl(genChannelRefundNotifyUrl(channel))
+                    .setReason(reqDTO.getReason());
+            PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO);
+            // 2.3 处理退款返回
+            getSelf().notifyRefund(channel, refundRespDTO);
+        } catch (Throwable e) {
+            // 注意:这里仅打印异常,不进行抛出。
+            // 原因是:虽然调用支付渠道进行退款发生异常(网络请求超时),实际退款成功。这个结果,后续通过退款回调、或者退款轮询补偿可以拿到。
+            // 最终,在异常的情况下,支付中心会异步回调业务的退款回调接口,提供退款结果
+            log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]",
+                    refund.getId(), reqDTO, e);
+        }
 
-        // 成功在 退款回调中处理
+        // 返回退款编号
         return refund.getId();
     }
 
@@ -149,21 +156,21 @@ public class PayRefundServiceImpl implements PayRefundService {
     private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
         PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
         if (order == null) {
-            throw exception(ErrorCodeConstants.ORDER_NOT_FOUND);
+            throw exception(ORDER_NOT_FOUND);
         }
-        // 校验状态,必须是支付状态
-        if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
-            throw exception(ErrorCodeConstants.ORDER_STATUS_IS_NOT_SUCCESS);
+        // 校验状态,必须是已支付、或者已退款
+        if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) {
+            throw exception(ORDER_REFUND_FAIL_STATUS_ERROR);
         }
 
-        // 校验金额 退款金额不能大于原定的金额
+        // 校验金额退款金额不能大于原定的金额
         if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
-            throw exception(ErrorCodeConstants.REFUND_PRICE_EXCEED);
+            throw exception(REFUND_PRICE_EXCEED);
         }
         // 是否有退款中的订单
         if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
                 PayRefundStatusEnum.WAITING.getStatus()) > 0) {
-            throw exception(ErrorCodeConstants.REFUND_HAS_REFUNDING);
+            throw exception(REFUND_HAS_REFUNDING);
         }
         return order;
     }
@@ -178,38 +185,22 @@ public class PayRefundServiceImpl implements PayRefundService {
         return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
     }
 
-    private String generateRefundNo() {
-//    wx
-//    2014
-//    10
-//    27
-//    20
-//    09
-//    39
-//    5522657
-//    a690389285100
-        // 目前的算法
-        // 时间序列,年月日时分秒 14 位
-        // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
-        return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
-                RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
-                ;
-    }
-
     @Override
     public void notifyRefund(Long channelId, PayRefundRespDTO notify) {
-        // 校验支付渠道是否有效
-        channelService.validPayChannel(channelId);
-        // 通知结果
-
         // 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(channelId);
         // 更新退款订单
-        TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
+        TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify));
     }
 
-    // TODO 芋艿:事务问题
-    private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
+    /**
+     * 通知并更新订单的退款结果
+     *
+     * @param channel 支付渠道
+     * @param notify 通知
+     */
+    @Transactional(rollbackFor = Exception.class)  // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
+    public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
         // 情况一:退款成功
         if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
             notifyRefundSuccess(channel, notify);
@@ -221,19 +212,19 @@ public class PayRefundServiceImpl implements PayRefundService {
         }
     }
 
-    public void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
+    private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
         // 1.1 查询 PayRefundDO
         PayRefundDO refund = refundMapper.selectByAppIdAndNo(
                 channel.getAppId(), notify.getOutRefundNo());
         if (refund == null) {
-            throw exception(ErrorCodeConstants.REFUND_NOT_FOUND);
+            throw exception(REFUND_NOT_FOUND);
         }
         if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
             log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功,无需更新]", refund.getId());
             return;
         }
         if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
-            throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING);
+            throw exception(REFUND_STATUS_IS_NOT_WAITING);
         }
         // 1.2 更新 PayRefundDO
         PayRefundDO updateRefundObj = new PayRefundDO()
@@ -243,32 +234,31 @@ public class PayRefundServiceImpl implements PayRefundService {
                 .setChannelNotifyData(toJsonString(notify));
         int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
         if (updateCounts == 0) { // 校验状态,必须是等待状态
-            throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING);
+            throw exception(REFUND_STATUS_IS_NOT_WAITING);
         }
         log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId());
 
         // 2. 更新订单
         orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
 
-        // 3. 插入退款通知记录 TODO 芋艿:退款成功
+        // 3. 插入退款通知记录
         notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
                 .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
     }
 
-    @Transactional(rollbackFor = Exception.class)
-    public void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
+    private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
         // 1.1 查询 PayRefundDO
         PayRefundDO refund = refundMapper.selectByAppIdAndNo(
                 channel.getAppId(), notify.getOutRefundNo());
         if (refund == null) {
-            throw exception(ErrorCodeConstants.REFUND_NOT_FOUND);
+            throw exception(REFUND_NOT_FOUND);
         }
         if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
             log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭,无需更新]", refund.getId());
             return;
         }
         if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
-            throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING);
+            throw exception(REFUND_STATUS_IS_NOT_WAITING);
         }
         // 1.2 更新 PayRefundDO
         PayRefundDO updateRefundObj = new PayRefundDO()
@@ -278,13 +268,64 @@ public class PayRefundServiceImpl implements PayRefundService {
                 .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg());
         int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
         if (updateCounts == 0) { // 校验状态,必须是等待状态
-            throw exception(ErrorCodeConstants.REFUND_STATUS_IS_NOT_WAITING);
+            throw exception(REFUND_STATUS_IS_NOT_WAITING);
         }
         log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId());
 
-        // 2. 插入退款通知记录 TODO 芋艿:退款失败
+        // 2. 插入退款通知记录
         notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
                 .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
     }
 
+    @Override
+    public int syncRefund() {
+        // 1. 查询指定创建时间内的待退款订单
+        List<PayRefundDO> refunds = refundMapper.selectListByStatus(PayRefundStatusEnum.WAITING.getStatus());
+        if (CollUtil.isEmpty(refunds)) {
+            return 0;
+        }
+        // 2. 遍历执行
+        int count = 0;
+        for (PayRefundDO refund : refunds) {
+            count += syncRefund(refund) ? 1 : 0;
+        }
+        return count;
+    }
+
+    /**
+     * 同步单个退款订单
+     *
+     * @param refund 退款订单
+     * @return 是否同步到
+     */
+    private boolean syncRefund(PayRefundDO refund) {
+        try {
+            // 1.1 查询退款订单信息
+            PayClient payClient = payClientFactory.getPayClient(refund.getChannelId());
+            if (payClient == null) {
+                log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId());
+                return false;
+            }
+            PayRefundRespDTO respDTO = payClient.getRefund(refund.getOrderNo(), refund.getNo());
+            // 1.2 回调退款结果
+            notifyRefund(refund.getChannelId(), respDTO);
+
+            // 2. 如果同步到,则返回 true
+            return PayRefundStatusEnum.isSuccess(respDTO.getStatus())
+                    || PayRefundStatusEnum.isFailure(respDTO.getStatus());
+        } catch (Throwable e) {
+            log.error("[syncRefund][refund({}) 同步退款状态异常]", refund.getId(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private PayRefundServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
+    }
+
 }

+ 3 - 1
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java

@@ -20,9 +20,11 @@ import org.springframework.context.annotation.Import;
 import javax.annotation.Resource;
 import javax.validation.Validator;
 
+import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
@@ -68,7 +70,7 @@ public class PayChannelServiceTest extends BaseDbUnitTest {
     public void testRefreshLocalCache() {
         // mock 数据 01
         PayChannelDO dbChannel = randomPojo(PayChannelDO.class,
-                o -> o.setConfig(randomWxPayClientConfig()));
+                o -> o.setConfig(randomWxPayClientConfig()).setUpdateTime(addTime(Duration.ofMinutes(-2))));
         channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据
         channelService.initLocalCache();
         // mock 数据 02

+ 555 - 12
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java

@@ -30,13 +30,13 @@ import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentMatcher;
 import org.mockito.MockedStatic;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
 import java.time.Duration;
+import java.time.LocalDateTime;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
@@ -44,8 +44,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -85,6 +84,51 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1");
     }
 
+    @Test
+    public void testGetOrder_id() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class);
+        orderMapper.insert(order);
+        // 准备参数
+        Long id = order.getId();
+
+        // 调用
+        PayOrderDO dbOrder = orderService.getOrder(id);
+        // 断言
+        assertPojoEquals(dbOrder, order);
+    }
+
+    @Test
+    public void testGetOrder_appIdAndMerchantOrderId() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class);
+        orderMapper.insert(order);
+        // 准备参数
+        Long appId = order.getAppId();
+        String merchantOrderId = order.getMerchantOrderId();
+
+        // 调用
+        PayOrderDO dbOrder = orderService.getOrder(appId, merchantOrderId);
+        // 断言
+        assertPojoEquals(dbOrder, order);
+    }
+
+    @Test
+    public void testGetOrderCountByAppId() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order01 = randomPojo(PayOrderDO.class);
+        orderMapper.insert(order01);
+        PayOrderDO order02 = randomPojo(PayOrderDO.class);
+        orderMapper.insert(order02);
+        // 准备参数
+        Long appId = order01.getAppId();
+
+        // 调用
+        Long count = orderService.getOrderCountByAppId(appId);
+        // 断言
+        assertEquals(count, 1L);
+    }
+
     @Test
     public void testGetOrderPage() {
         // mock 数据
@@ -224,7 +268,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
     @Test
     public void testSubmitOrder_notWaiting() {
         // mock 数据(order)
-        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()));
         orderMapper.insert(order);
         // 准备参数
         PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
@@ -234,6 +278,19 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_NOT_WAITING);
     }
 
+    @Test
+    public void testSubmitOrder_isSuccess() {
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        orderMapper.insert(order);
+        // 准备参数
+        PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()));
+        String userIp = randomString();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_SUCCESS);
+    }
+
     @Test
     public void testSubmitOrder_expired() {
         // mock 数据(order)
@@ -350,7 +407,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
             // mock 方法(client)
             PayClient client = mock(PayClient.class);
             when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
-            // mock 方法()
+            // mock 方法(支付渠道的调用
             PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null)
                     .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou"));
             when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> {
@@ -383,6 +440,57 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         }
     }
 
+    @Test
+    public void testValidateOrderActuallyPaid_dbPaid() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
+                ORDER_EXTENSION_IS_PAID);
+    }
+
+    @Test
+    public void testValidateOrderActuallyPaid_remotePaid() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient 已支付)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
+        when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.validateOrderActuallyPaid(id),
+                ORDER_EXTENSION_IS_PAID);
+    }
+
+    @Test
+    public void testValidateOrderActuallyPaid_success() {
+        // 准备参数
+        Long id = randomLongId();
+        // mock 方法(OrderExtension 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient 已支付)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client);
+        when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())));
+
+        // 调用,并断言异常
+        orderService.validateOrderActuallyPaid(id);
+    }
+
     @Test
     public void testNotifyOrder_channelId() {
         PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
@@ -553,14 +661,449 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
         assertPojoEquals(order, orderMapper.selectOne(null),
                 "updateTime", "updater");
         // 断言,调用
-        verify(notifyService).createPayNotifyTask(argThat(new ArgumentMatcher<PayNotifyTaskCreateReqDTO>() {
-            @Override
-            public boolean matches(PayNotifyTaskCreateReqDTO reqDTO) {
-                assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType());
-                assertEquals(reqDTO.getDataId(), orderExtension.getOrderId());
-                return true;
-            }
+        verify(notifyService).createPayNotifyTask(argThat(reqDTO -> {
+            assertEquals(reqDTO.getType(), PayNotifyTypeEnum.ORDER.getType());
+            assertEquals(reqDTO.getDataId(), orderExtension.getOrderId());
+            return true;
         }));
     }
 
+    @Test
+    public void testNotifyOrderClosed_orderExtension_notFound() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_EXTENSION_NOT_FOUND);
+    }
+
+    @Test
+    public void testNotifyOrderClosed_orderExtension_closed() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言
+        orderService.notifyOrder(channel, notify);
+        // 断言 PayOrderExtensionDO :数据未更新,因为它是 CLOSED
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
+    }
+
+    @Test
+    public void testNotifyOrderClosed_orderExtension_paid() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言
+        orderService.notifyOrder(channel, notify);
+        // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null));
+    }
+
+    @Test
+    public void testNotifyOrderClosed_orderExtension_refund() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.notifyOrder(channel, notify),
+                ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
+    }
+
+    @Test
+    public void testNotifyOrderClosed_orderExtension_waiting() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setNo("P110"));
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+        PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
+                        .setOutTradeNo("P110"));
+
+        // 调用
+        orderService.notifyOrder(channel, notify);
+        // 断言 PayOrderExtensionDO
+        orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()).setChannelNotifyData(toJsonString(notify))
+                .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg());
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null),
+                "updateTime", "updater");
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_notFound() {
+        // 准备参数
+        Long id = randomLongId();
+        Integer incrRefundPrice = randomInteger();
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice),
+                ORDER_NOT_FOUND);
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_waiting() {
+        testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.WAITING.getStatus());
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_closed() {
+        testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus());
+    }
+
+    private void testUpdateOrderRefundPrice_waitingOrClosed(Integer status) {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(status));
+        orderMapper.insert(order);
+        // 准备参数
+        Long id = order.getId();
+        Integer incrRefundPrice = randomInteger();
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice),
+                ORDER_REFUND_FAIL_STATUS_ERROR);
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_priceExceed() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setRefundPrice(1).setPrice(10));
+        orderMapper.insert(order);
+        // 准备参数
+        Long id = order.getId();
+        Integer incrRefundPrice = 10;
+
+        // 调用,并断言异常
+        assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice),
+                REFUND_PRICE_EXCEED);
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_refund() {
+        testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.REFUND.getStatus());
+    }
+
+    @Test
+    public void testUpdateOrderRefundPrice_success() {
+        testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.SUCCESS.getStatus());
+    }
+
+    private void testUpdateOrderRefundPrice_refundOrSuccess(Integer status) {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(status).setRefundPrice(1).setPrice(10));
+        orderMapper.insert(order);
+        // 准备参数
+        Long id = order.getId();
+        Integer incrRefundPrice = 8;
+
+        // 调用
+        orderService.updateOrderRefundPrice(id, incrRefundPrice);
+        // 断言
+        order.setRefundPrice(9).setStatus(PayOrderStatusEnum.REFUND.getStatus());
+        assertPojoEquals(order, orderMapper.selectOne(null),
+                "updateTime", "updater");
+    }
+
+    @Test
+    public void testGetOrderExtension() {
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class);
+        orderExtensionMapper.insert(orderExtension);
+        // 准备参数
+        Long id = orderExtension.getId();
+
+        // 调用
+        PayOrderExtensionDO dbOrderExtension = orderService.getOrderExtension(id);
+        // 断言
+        assertPojoEquals(dbOrderExtension, orderExtension);
+    }
+
+    @Test
+    public void testSyncOrder_payClientNotFound() {
+        // 准备参数
+        LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10));
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setCreateTime(LocalDateTime.now()));
+        orderExtensionMapper.insert(orderExtension);
+
+        // 调用
+        int count = orderService.syncOrder(minCreateTime);
+        // 断言
+        assertEquals(count, 0);
+    }
+
+    @Test
+    public void testSyncOrder_exception() {
+        // 准备参数
+        LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10));
+        // mock 数据(PayOrderExtensionDO)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setChannelId(10L)
+                        .setCreateTime(LocalDateTime.now()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 方法(PayClient 异常)
+        when(client.getOrder(any())).thenThrow(new RuntimeException());
+
+        // 调用
+        int count = orderService.syncOrder(minCreateTime);
+        // 断言
+        assertEquals(count, 0);
+    }
+
+    @Test
+    public void testSyncOrder_orderSuccess() {
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+
+            // 准备参数
+            LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10));
+            // mock 数据(PayOrderExtensionDO)
+            PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                            .setChannelId(10L).setNo("P110")
+                            .setCreateTime(LocalDateTime.now()));
+            orderExtensionMapper.insert(orderExtension);
+            // mock 方法(PayClient)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法(PayClient 成功返回)
+            PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+            when(client.getOrder(eq("P110"))).thenReturn(respDTO);
+            // mock 方法(PayChannelDO)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            int count = orderService.syncOrder(minCreateTime);
+            // 断言
+            assertEquals(count, 1);
+            verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO));
+        }
+    }
+
+    @Test
+    public void testSyncOrder_orderClosed() {
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+
+            // 准备参数
+            LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10));
+            // mock 数据(PayOrderExtensionDO)
+            PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                            .setChannelId(10L).setNo("P110")
+                            .setCreateTime(LocalDateTime.now()));
+            orderExtensionMapper.insert(orderExtension);
+            // mock 方法(PayClient)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法(PayClient 成功返回)
+            PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()));
+            when(client.getOrder(eq("P110"))).thenReturn(respDTO);
+            // mock 方法(PayChannelDO)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            int count = orderService.syncOrder(minCreateTime);
+            // 断言
+            assertEquals(count, 0);
+            verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO));
+        }
+    }
+
+    @Test
+    public void testExpireOrder_orderExtension_isSuccess() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setExpireTime(addTime(Duration.ofMinutes(-1))));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO 已支付)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
+                        .setOrderId(order.getId()));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+
+        // 调用
+        int count = orderService.expireOrder();
+        // 断言
+        assertEquals(count, 0);
+        // 断言 order 没有变化,因为没更新
+        assertPojoEquals(order, orderMapper.selectOne(null));
+    }
+
+    @Test
+    public void testExpireOrder_payClient_notFound() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setExpireTime(addTime(Duration.ofMinutes(-1))));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO 等待中)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setOrderId(order.getId())
+                        .setChannelId(10L));
+        orderExtensionMapper.insert(orderExtension);
+
+        // 调用
+        int count = orderService.expireOrder();
+        // 断言
+        assertEquals(count, 0);
+        // 断言 order 没有变化,因为没更新
+        assertPojoEquals(order, orderMapper.selectOne(null));
+    }
+
+    @Test
+    public void testExpireOrder_getOrder_isRefund() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setExpireTime(addTime(Duration.ofMinutes(-1))));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO 等待中)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setOrderId(order.getId()).setNo("P110")
+                        .setChannelId(10L));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 方法(PayClient 退款返回)
+        PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()));
+        when(client.getOrder(eq("P110"))).thenReturn(respDTO);
+
+        // 调用
+        int count = orderService.expireOrder();
+        // 断言
+        assertEquals(count, 0);
+        // 断言 order 没有变化,因为没更新
+        assertPojoEquals(order, orderMapper.selectOne(null));
+    }
+
+    @Test
+    public void testExpireOrder_getOrder_isSuccess() {
+        PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class)))
+                    .thenReturn(payOrderServiceImpl);
+
+            // mock 数据(PayOrderDO)
+            PayOrderDO order = randomPojo(PayOrderDO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                            .setExpireTime(addTime(Duration.ofMinutes(-1))));
+            orderMapper.insert(order);
+            // mock 数据(PayOrderExtensionDO 等待中)
+            PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                            .setOrderId(order.getId()).setNo("P110")
+                            .setChannelId(10L));
+            orderExtensionMapper.insert(orderExtension);
+            // mock 方法(PayClient)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法(PayClient 成功返回)
+            PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
+                    o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
+            when(client.getOrder(eq("P110"))).thenReturn(respDTO);
+            // mock 方法(PayChannelDO)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            int count = orderService.expireOrder();
+            // 断言
+            assertEquals(count, 0);
+            // 断言 order 没有变化,因为没更新
+            assertPojoEquals(order, orderMapper.selectOne(null));
+            verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO));
+        }
+    }
+
+    @Test
+    public void testExpireOrder_success() {
+        // mock 数据(PayOrderDO)
+        PayOrderDO order = randomPojo(PayOrderDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setExpireTime(addTime(Duration.ofMinutes(-1))));
+        orderMapper.insert(order);
+        // mock 数据(PayOrderExtensionDO 等待中)
+        PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class,
+                o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())
+                        .setOrderId(order.getId()).setNo("P110")
+                        .setChannelId(10L));
+        orderExtensionMapper.insert(orderExtension);
+        // mock 方法(PayClient)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 方法(PayClient 关闭返回)
+        PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class,
+                o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()));
+        when(client.getOrder(eq("P110"))).thenReturn(respDTO);
+
+        // 调用
+        int count = orderService.expireOrder();
+        // 断言
+        assertEquals(count, 1);
+        // 断言 extension 变化
+        orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                .setChannelNotifyData(toJsonString(respDTO));
+        assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null),
+                "updateTime", "updater");
+        // 断言 order 变化
+        order.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
+        assertPojoEquals(order, orderMapper.selectOne(null),
+                "updateTime", "updater");
+    }
+
 }

+ 588 - 43
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java

@@ -1,35 +1,62 @@
 package cn.iocoder.yudao.module.pay.service.refund;
 
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
+import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
+import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
 
-@Import(PayRefundServiceImpl.class)
+/**
+ * {@link PayRefundServiceImpl} 的单元测试类
+ *
+ * @author 芋艿
+ */
+@Import({PayRefundServiceImpl.class, PayNoRedisDAO.class})
 public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
 
     @Resource
@@ -51,50 +78,81 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     @MockBean
     private PayNotifyService notifyService;
 
+    @BeforeEach
+    public void setUp() {
+        when(payProperties.getRefundNotifyUrl()).thenReturn("http://127.0.0.1");
+    }
+
+    @Test
+    public void testGetRefund() {
+        // mock 数据
+        PayRefundDO refund = randomPojo(PayRefundDO.class);
+        refundMapper.insert(refund);
+        // 准备参数
+        Long id = refund.getId();
+
+        // 调用
+        PayRefundDO dbRefund = refundService.getRefund(id);
+        // 断言
+        assertPojoEquals(dbRefund, refund);
+    }
+
+    @Test
+    public void testGetRefundCountByAppId() {
+        // mock 数据
+        PayRefundDO refund01 = randomPojo(PayRefundDO.class);
+        refundMapper.insert(refund01);
+        PayRefundDO refund02 = randomPojo(PayRefundDO.class);
+        refundMapper.insert(refund02);
+        // 准备参数
+        Long appId = refund01.getAppId();
+
+        // 调用
+        Long count = refundService.getRefundCountByAppId(appId);
+        // 断言
+        assertEquals(count, 1);
+    }
+
     @Test
     public void testGetRefundPage() {
         // mock 数据
         PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到
             o.setAppId(1L);
-            o.setChannelId(1L);
             o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
-            o.setOrderId(1L);
-            o.setNo("OT0000001");
             o.setMerchantOrderId("MOT0000001");
             o.setMerchantRefundId("MRF0000001");
-            o.setNotifyUrl("https://www.cancanzi.com");
             o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
-            o.setPayPrice(100);
-            o.setRefundPrice(500);
-            o.setReason("就是想退款了,你有意见吗");
-            o.setUserIp("127.0.0.1");
             o.setChannelOrderNo("CH0000001");
             o.setChannelRefundNo("CHR0000001");
-            o.setChannelErrorCode("");
-            o.setChannelErrorMsg("");
-            o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
-            o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
-            o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
+            o.setCreateTime(buildTime(2021, 1, 10));
         });
         refundMapper.insert(dbRefund);
         // 测试 appId 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L)));
         // 测试 channelCode 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
-        // 测试 merchantRefundNo 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
+        // 测试 merchantOrderId 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString())));
+        // 测试 merchantRefundId 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString())));
+        // 测试 channelOrderNo 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString())));
+        // 测试 channelRefundNo 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString())));
         // 测试 status 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())));
         // 测试 createTime 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o ->
-                o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1))));
         // 准备参数
         PayRefundPageReqVO reqVO = new PayRefundPageReqVO();
         reqVO.setAppId(1L);
         reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode());
+        reqVO.setMerchantOrderId("MOT0000001");
         reqVO.setMerchantRefundId("MRF0000001");
-        reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-        reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
+        reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
+        reqVO.setChannelOrderNo("CH0000001");
+        reqVO.setChannelRefundNo("CHR0000001");
+        reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11));
 
         // 调用
         PageResult<PayRefundDO> pageResult = refundService.getRefundPage(reqVO);
@@ -109,45 +167,41 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
         // mock 数据
         PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到
             o.setAppId(1L);
-            o.setChannelId(1L);
             o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
-            o.setOrderId(1L);
-            o.setNo("OT0000001");
             o.setMerchantOrderId("MOT0000001");
             o.setMerchantRefundId("MRF0000001");
-            o.setNotifyUrl("https://www.cancanzi.com");
             o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
-            o.setPayPrice(100);
-            o.setRefundPrice(500);
-            o.setReason("就是想退款了,你有意见吗");
-            o.setUserIp("127.0.0.1");
             o.setChannelOrderNo("CH0000001");
             o.setChannelRefundNo("CHR0000001");
-            o.setChannelErrorCode("");
-            o.setChannelErrorMsg("");
-            o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
-            o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
-            o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
+            o.setCreateTime(buildTime(2021, 1, 10));
         });
         refundMapper.insert(dbRefund);
         // 测试 appId 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L)));
         // 测试 channelCode 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
-        // 测试 merchantRefundNo 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
+        // 测试 merchantOrderId 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString())));
+        // 测试 merchantRefundId 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString())));
+        // 测试 channelOrderNo 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString())));
+        // 测试 channelRefundNo 不匹配
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString())));
         // 测试 status 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus())));
         // 测试 createTime 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o ->
-                o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
-
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1))));
         // 准备参数
         PayRefundExportReqVO reqVO = new PayRefundExportReqVO();
         reqVO.setAppId(1L);
         reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode());
-        reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-        reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
+        reqVO.setMerchantOrderId("MOT0000001");
+        reqVO.setMerchantRefundId("MRF0000001");
+        reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
+        reqVO.setChannelOrderNo("CH0000001");
+        reqVO.setChannelRefundNo("CHR0000001");
+        reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11));
 
         // 调用
         List<PayRefundDO> list = refundService.getRefundList(reqVO);
@@ -156,4 +210,495 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
         assertPojoEquals(dbRefund, list.get(0));
     }
 
+    @Test
+    public void testCreateRefund_orderNotFound() {
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                ORDER_NOT_FOUND);
+    }
+
+    @Test
+    public void testCreateRefund_orderWaiting() {
+        testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.WAITING.getStatus());
+    }
+
+    @Test
+    public void testCreateRefund_orderClosed() {
+        testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus());
+    }
+
+    private void testCreateRefund_orderWaitingOrClosed(Integer status) {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100"));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                ORDER_REFUND_FAIL_STATUS_ERROR);
+    }
+
+    @Test
+    public void testCreateRefund_refundPriceExceed() {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setPrice(10).setRefundPrice(1));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                REFUND_PRICE_EXCEED);
+    }
+
+    @Test
+    public void testCreateRefund_orderHasRefunding() {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setPrice(10).setRefundPrice(1));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+        // mock 数据(refund 在退款中)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o ->
+                o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                REFUND_PRICE_EXCEED);
+    }
+
+    @Test
+    public void testCreateRefund_channelNotFound() {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setPrice(10).setRefundPrice(1)
+                        .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+        // mock 方法(channel)
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(channelService.validPayChannel(eq(1L))).thenReturn(channel);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                CHANNEL_NOT_FOUND);
+    }
+
+    @Test
+    public void testCreateRefund_refundExists() {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                        .setMerchantRefundId("200").setReason("测试退款"));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setPrice(10).setRefundPrice(1)
+                        .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+        // mock 方法(channel)
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(channelService.validPayChannel(eq(1L))).thenReturn(channel);
+        // mock 方法(client)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 数据(refund 已存在)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o ->
+                o.setAppId(1L).setMerchantRefundId("200"));
+        refundMapper.insert(refund);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.createPayRefund(reqDTO),
+                REFUND_EXISTS);
+    }
+
+    @Test
+    public void testCreateRefund_invokeException() {
+        // 准备参数
+        PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                        .setMerchantRefundId("200").setReason("测试退款"));
+        // mock 方法(app)
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        // mock 数据(order)
+        PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                        .setPrice(10).setRefundPrice(1)
+                        .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+        // mock 方法(channel)
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+        when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+        // mock 方法(client)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 方法(client 调用发生异常)
+        when(client.unifiedRefund(any(PayRefundUnifiedReqDTO.class))).thenThrow(new RuntimeException());
+
+        // 调用
+        Long refundId = refundService.createPayRefund(reqDTO);
+        // 断言
+        PayRefundDO refundDO = refundMapper.selectById(refundId);
+        assertPojoEquals(reqDTO, refundDO);
+        assertNotNull(refundDO.getNo());
+        assertThat(refundDO)
+                .extracting("orderId", "orderNo", "channelId", "channelCode",
+                        "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice")
+                .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(),
+                        app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(),
+                        order.getPrice(), reqDTO.getPrice());
+    }
+
+    @Test
+    public void testCreateRefund_invokeSuccess() {
+        PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class)))
+                    .thenReturn(payRefundServiceImpl);
+
+            // 准备参数
+            PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
+                    o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                            .setMerchantRefundId("200").setReason("测试退款"));
+            // mock 方法(app)
+            PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
+            when(appService.validPayApp(eq(1L))).thenReturn(app);
+            // mock 数据(order)
+            PayOrderDO order = randomPojo(PayOrderDO.class, o ->
+                    o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
+                            .setPrice(10).setRefundPrice(1)
+                            .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
+                    .setCode(PayChannelEnum.ALIPAY_APP.getCode()));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+            // mock 方法(client)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法(client 成功)
+            PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class);
+            when(client.unifiedRefund(argThat(unifiedReqDTO -> {
+                assertNotNull(unifiedReqDTO.getOutRefundNo());
+                assertThat(unifiedReqDTO)
+                        .extracting("payPrice", "refundPrice", "outTradeNo",
+                                 "notifyUrl", "reason")
+                        .containsExactly(order.getPrice(), reqDTO.getPrice(), order.getNo(),
+                                "http://127.0.0.1/10", reqDTO.getReason());
+                return true;
+            }))).thenReturn(refundRespDTO);
+
+            // 调用
+            Long refundId = refundService.createPayRefund(reqDTO);
+            // 断言
+            PayRefundDO refundDO = refundMapper.selectById(refundId);
+            assertPojoEquals(reqDTO, refundDO);
+            assertNotNull(refundDO.getNo());
+            assertThat(refundDO)
+                    .extracting("orderId", "orderNo", "channelId", "channelCode",
+                            "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice")
+                    .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(),
+                            app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(),
+                            order.getPrice(), reqDTO.getPrice());
+            // 断言调用
+            verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO));
+        }
+    }
+
+    @Test
+    public void testNotifyRefund() {
+        PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class)))
+                    .thenReturn(payRefundServiceImpl);
+
+            // 准备参数
+            Long channelId = 10L;
+            PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            refundService.notifyRefund(channelId, refundRespDTO);
+            // 断言
+            verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO));
+        }
+    }
+
+    @Test
+    public void testNotifyRefundSuccess_notFound() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
+                REFUND_NOT_FOUND);
+    }
+
+    @Test
+    public void testNotifyRefundSuccess_isSuccess() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用
+        refundService.notifyRefund(channel, refundRespDTO);
+        // 断言,refund 没有更新,因为已经退款成功
+        assertPojoEquals(refund, refundMapper.selectById(refund.getId()));
+    }
+
+    @Test
+    public void testNotifyRefundSuccess_failure() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.FAILURE.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
+                REFUND_STATUS_IS_NOT_WAITING);
+    }
+
+    @Test
+    public void testNotifyRefundSuccess_updateOrderException() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                .setOrderId(100L).setRefundPrice(23));
+        refundMapper.insert(refund);
+        // mock 方法(order + 更新异常)
+        doThrow(new RuntimeException()).when(orderService)
+                .updateOrderRefundPrice(eq(100L), eq(23));
+
+        // 调用,并断言异常
+        assertThrows(RuntimeException.class, () -> refundService.notifyRefund(channel, refundRespDTO));
+        // 断言,refund 没有更新,因为事务回滚了
+        assertPojoEquals(refund, refundMapper.selectById(refund.getId()));
+    }
+
+    @Test
+    public void testNotifyRefundSuccess_success() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                .setOrderId(100L).setRefundPrice(23));
+        refundMapper.insert(refund);
+
+        // 调用
+        refundService.notifyRefund(channel, refundRespDTO);
+        // 断言,refund
+        refund.setSuccessTime(refundRespDTO.getSuccessTime())
+                .setChannelRefundNo(refundRespDTO.getChannelRefundNo())
+                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
+                .setChannelNotifyData(toJsonString(refundRespDTO));
+        assertPojoEquals(refund, refundMapper.selectById(refund.getId()),
+                "updateTime", "updater");
+        // 断言,调用
+        verify(orderService).updateOrderRefundPrice(eq(100L), eq(23));
+        verify(notifyService).createPayNotifyTask(eq(PayNotifyTaskCreateReqDTO.builder()
+                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build()));
+    }
+
+    @Test
+    public void testNotifyRefundFailure_notFound() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
+                REFUND_NOT_FOUND);
+    }
+
+    @Test
+    public void testNotifyRefundFailure_isFailure() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 退款失败)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.FAILURE.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用
+        refundService.notifyRefund(channel, refundRespDTO);
+        // 断言,refund 没有更新,因为已经退款失败
+        assertPojoEquals(refund, refundMapper.selectById(refund.getId()));
+    }
+
+    @Test
+    public void testNotifyRefundFailure_isSuccess() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用,并断言异常
+        assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
+                REFUND_STATUS_IS_NOT_WAITING);
+    }
+
+    @Test
+    public void testNotifyRefundFailure_success() {
+        // 准备参数
+        PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
+        PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
+                o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
+        // mock 数据(refund + 已支付)
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                .setOrderId(100L).setRefundPrice(23));
+        refundMapper.insert(refund);
+
+        // 调用
+        refundService.notifyRefund(channel, refundRespDTO);
+        // 断言,refund
+        refund.setChannelRefundNo(refundRespDTO.getChannelRefundNo())
+                .setStatus(PayRefundStatusEnum.FAILURE.getStatus())
+                .setChannelNotifyData(toJsonString(refundRespDTO))
+                .setChannelErrorCode(refundRespDTO.getChannelErrorCode())
+                .setChannelErrorMsg(refundRespDTO.getChannelErrorMsg());
+        assertPojoEquals(refund, refundMapper.selectById(refund.getId()),
+                "updateTime", "updater");
+        // 断言,调用
+        verify(notifyService).createPayNotifyTask(eq(PayNotifyTaskCreateReqDTO.builder()
+                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build()));
+    }
+
+    @Test
+    public void testSyncRefund_notFound() {
+        // 准备参数
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L)
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus()));
+        refundMapper.insert(refund);
+
+        // 调用
+        int count = refundService.syncRefund();
+        // 断言
+        assertEquals(count, 0);
+    }
+
+    @Test
+    public void testSyncRefund_waiting() {
+        assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.WAITING.getStatus()), 0);
+    }
+
+    @Test
+    public void testSyncRefund_success() {
+        assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.SUCCESS.getStatus()), 1);
+    }
+
+    @Test
+    public void testSyncRefund_failure() {
+        assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.FAILURE.getStatus()), 1);
+    }
+
+    private int testSyncRefund_waitingOrSuccessOrFailure(Integer status) {
+        PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class);
+        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+            springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class)))
+                    .thenReturn(payRefundServiceImpl);
+
+            // 准备参数
+            PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L)
+                    .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                    .setOrderNo("P110").setNo("R220"));
+            refundMapper.insert(refund);
+            // mock 方法(client)
+            PayClient client = mock(PayClient.class);
+            when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+            // mock 方法(client 返回指定状态)
+            PayRefundRespDTO respDTO = randomPojo(PayRefundRespDTO.class, o -> o.setStatus(status));
+            when(client.getRefund(eq("P110"), eq("R220"))).thenReturn(respDTO);
+            // mock 方法(channel)
+            PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
+            when(channelService.validPayChannel(eq(10L))).thenReturn(channel);
+
+            // 调用
+            return refundService.syncRefund();
+        }
+    }
+
+    @Test
+    public void testSyncRefund_exception() {
+        // 准备参数
+        PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L)
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                .setOrderNo("P110").setNo("R220"));
+        refundMapper.insert(refund);
+        // mock 方法(client)
+        PayClient client = mock(PayClient.class);
+        when(payClientFactory.getPayClient(eq(10L))).thenReturn(client);
+        // mock 方法(client 抛出异常)
+        when(client.getRefund(eq("P110"), eq("R220"))).thenThrow(new RuntimeException());
+
+        // 调用
+        int count = refundService.syncRefund();
+        // 断言
+        assertEquals(count, 0);
+    }
+
 }

+ 8 - 11
yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql

@@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS "pay_app" (
     "name"              varchar(64)   NOT NULL,
     "status"            tinyint       NOT NULL,
     "remark"            varchar(255)           DEFAULT NULL,
-    `pay_notify_url`    varchar(1024) NOT NULL,
+    `order_notify_url`    varchar(1024) NOT NULL,
     `refund_notify_url` varchar(1024) NOT NULL,
     "creator"           varchar(64)            DEFAULT '',
     "create_time"       datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -82,29 +82,26 @@ CREATE TABLE IF NOT EXISTS `pay_order_extension` (
 
 CREATE TABLE IF NOT EXISTS `pay_refund` (
     "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    `no`           varchar(64)         NOT NULL,
     `app_id`             bigint(20)    NOT NULL,
     `channel_id`         bigint(20)    NOT NULL,
     `channel_code`       varchar(32)   NOT NULL,
     `order_id`           bigint(20)    NOT NULL,
-    `trade_no`           varchar(64)   NOT NULL,
+    `order_no`           varchar(64)    NOT NULL,
     `merchant_order_id`  varchar(64)   NOT NULL,
-    `merchant_refund_no` varchar(64)   NOT NULL,
+    `merchant_refund_id` varchar(64)   NOT NULL,
     `notify_url`         varchar(1024) NOT NULL,
-    `notify_status`      tinyint(4)    NOT NULL,
     `status`             tinyint(4)    NOT NULL,
-    `type`               tinyint(4)    NOT NULL,
-    `pay_amount`         bigint(20)    NOT NULL,
-    `refund_amount`      bigint(20)    NOT NULL,
+    `pay_price`         bigint(20)    NOT NULL,
+    `refund_price`      bigint(20)    NOT NULL,
     `reason`             varchar(256)  NOT NULL,
     `user_ip`            varchar(50)   NULL     DEFAULT NULL,
     `channel_order_no`   varchar(64)   NOT NULL,
     `channel_refund_no`  varchar(64)   NULL     DEFAULT NULL,
+    `success_time`       datetime(0)   NULL     DEFAULT NULL,
     `channel_error_code` varchar(128)  NULL     DEFAULT NULL,
     `channel_error_msg`  varchar(256)  NULL     DEFAULT NULL,
-    `channel_extras`     varchar(1024) NULL     DEFAULT NULL,
-    `expire_time`        datetime(0)   NULL     DEFAULT NULL,
-    `success_time`       datetime(0)   NULL     DEFAULT NULL,
-    `notify_time`        datetime(0)   NULL     DEFAULT NULL,
+    `channel_notify_data` varchar(1024)  NULL,
     `creator`            varchar(64)   NULL     DEFAULT '',
     `create_time`        datetime(0)   NOT NULL DEFAULT CURRENT_TIMESTAMP,
     `updater`            varchar(64)   NULL     DEFAULT '',

+ 15 - 3
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java

@@ -84,6 +84,9 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
             o.setParams(Lists.newArrayList("code", "op"));
         });
         when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
+        String title = RandomUtils.randomString();
+        when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams)))
+                .thenReturn(title);
         String content = RandomUtils.randomString();
         when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams)))
                 .thenReturn(content);
@@ -101,7 +104,7 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
         assertEquals(mailLogId, resultMailLogId);
         // 断言调用
         verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(user.getEmail()),
-                eq(account.getId()), eq(template.getNickname()), eq(template.getTitle()), eq(content));
+                eq(account.getId()), eq(template.getNickname()), eq(title), eq(content));
     }
 
     @Test
@@ -122,6 +125,9 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
             o.setParams(Lists.newArrayList("code", "op"));
         });
         when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
+        String title = RandomUtils.randomString();
+        when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams)))
+                .thenReturn(title);
         String content = RandomUtils.randomString();
         when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams)))
                 .thenReturn(content);
@@ -139,7 +145,7 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
         assertEquals(mailLogId, resultMailLogId);
         // 断言调用
         verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail),
-                eq(account.getId()), eq(template.getNickname()), eq(template.getTitle()), eq(content));
+                eq(account.getId()), eq(template.getNickname()), eq(title), eq(content));
     }
 
     /**
@@ -161,6 +167,9 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
             o.setParams(Lists.newArrayList("code", "op"));
         });
         when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
+        String title = RandomUtils.randomString();
+        when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams)))
+                .thenReturn(title);
         String content = RandomUtils.randomString();
         when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams)))
                 .thenReturn(content);
@@ -178,7 +187,7 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
         assertEquals(mailLogId, resultMailLogId);
         // 断言调用
         verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail),
-                eq(account.getId()), eq(template.getNickname()), eq(template.getTitle()), eq(content));
+                eq(account.getId()), eq(template.getNickname()), eq(title), eq(content));
     }
 
     /**
@@ -200,6 +209,9 @@ class MailSendServiceImplTest extends BaseMockitoUnitTest {
             o.setParams(Lists.newArrayList("code", "op"));
         });
         when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
+        String title = RandomUtils.randomString();
+        when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams)))
+                .thenReturn(title);
         String content = RandomUtils.randomString();
         when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams)))
                 .thenReturn(content);

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImplTest.java

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.service.social;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
@@ -35,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.*;
 
 @Import(SocialUserServiceImpl.class)
-public class SocialUserServiceImplTest extends BaseDbAndRedisUnitTest {
+public class SocialUserServiceImplTest extends BaseDbUnitTest {
 
     @Resource
     private SocialUserServiceImpl socialUserService;

+ 15 - 15
yudao-server/pom.xml

@@ -68,21 +68,21 @@
 <!--        </dependency>-->
 
         <!-- 商城相关模块。默认注释,保证编译速度 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-promotion-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-product-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-trade-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-promotion-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-product-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-trade-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 
         <!-- spring boot 配置所需依赖 -->
         <dependency>

+ 0 - 8
yudao-ui-admin/src/utils/constants.js

@@ -180,14 +180,6 @@ export const PayDisplayModeEnum = {
   }
 }
 
-/**
- * 支付类型枚举
- */
-export const PayType = {
-  WECHAT: "WECHAT",
-  ALIPAY: "ALIPAY"
-}
-
 /**
  * 支付订单状态枚举
  */

+ 2 - 7
yudao-ui-admin/src/views/pay/app/components/alipayChannelForm.vue

@@ -172,7 +172,7 @@ export default {
           this.formData = response.data;
           this.formData.config = JSON.parse(response.data.config);
         }
-        this.title = this.formData.id ? '创建支付渠道' : '编辑支付渠道'
+        this.title = !this.formData.id ? '创建支付渠道' : '编辑支付渠道'
       }).finally(() => {
         this.formLoading = false;
       });
@@ -257,12 +257,7 @@ export default {
         this.formData.config.rootCertContent = e.target.result
       }
       readFile.readAsText(event.file);
-    },
-
+    }
   }
 }
-
 </script>
-<style scoped>
-
-</style>

+ 0 - 328
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue

@@ -1,328 +0,0 @@
-<template>
-  <div>
-    <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px">
-      <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px"
-               v-loading="transferParam.loading">
-        <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
-          <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
-            <template slot="append">%</template>
-          </el-input>
-        </el-form-item>
-        <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
-          <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
-          </el-input>
-        </el-form-item>
-        <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
-          <el-input v-model="form.weChatConfig.mchId" :style="{width: '100%'}"></el-input>
-        </el-form-item>
-        <el-form-item label-width="180px" label="渠道状态" prop="status">
-          <el-radio-group v-model="form.status" size="medium">
-            <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
-              {{ dict.label }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label-width="180px" label="API 版本" prop="weChatConfig.apiVersion">
-          <el-radio-group v-model="form.weChatConfig.apiVersion" size="medium">
-            <el-radio v-for="dict in versionDictDatas" :key="dict.value" :label="dict.value">
-              {{ dict.label }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <div v-if="form.weChatConfig.apiVersion === 'v2'">
-          <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
-            <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
-                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
-          </el-form-item>
-          <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
-            <el-input v-model="form.weChatConfig.keyContent" type="textarea"
-                      placeholder="请上传 apiclient_cert.p12 证书"
-                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
-          </el-form-item>
-          <el-form-item label-width="180px" label="">
-            <el-upload :limit="1" accept=".p12" action=""
-                       :before-upload="p12FileBeforeUpload"
-                       :http-request="keyContentUpload">
-              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
-            </el-upload>
-          </el-form-item>
-        </div>
-        <div v-if="form.weChatConfig.apiVersion === 'v3'">
-          <el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
-            <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
-                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
-          </el-form-item>
-          <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
-            <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
-                      placeholder="请上传 apiclient_key.perm 证书"
-                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
-          </el-form-item>
-          <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
-            <el-upload ref="privateKeyContentFile"
-                       :limit="1"
-                       accept=".pem"
-                       action=""
-                       :before-upload="pemFileBeforeUpload"
-                       :http-request="privateKeyContentUpload"
-            >
-              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
-            </el-upload>
-          </el-form-item>
-          <el-form-item label-width="180px" label="apiclient_cert.perm证书" prop="weChatConfig.privateCertContent">
-            <el-input v-model="form.weChatConfig.privateCertContent" type="textarea"
-                      placeholder="请上传apiclient_cert.perm证书"
-                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
-          </el-form-item>
-          <el-form-item label-width="180px" label="" prop="privateCertContentFile">
-            <el-upload ref="privateCertContentFile"
-                       :limit="1"
-                       accept=".pem"
-                       action=""
-                       :before-upload="pemFileBeforeUpload"
-                       :http-request="privateCertContentUpload"
-            >
-              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
-            </el-upload>
-          </el-form-item>
-        </div>
-        <el-form-item label-width="180px" label="备注" prop="remark">
-          <el-input v-model="form.remark" :style="{width: '100%'}" />
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="close">取消</el-button>
-        <el-button type="primary" @click="handleConfirm">确定</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-<script>
-import {DICT_TYPE, getDictDatas} from "@/utils/dict";
-import {createChannel, getChannel, updateChannel} from "@/api/pay/channel";
-
-const defaultForm = {
-  code: '',
-  status: null,
-  remark: '',
-  feeRate: null,
-  appId: '',
-  weChatConfig: {
-    appId: '',
-    mchId: '',
-    apiVersion: '',
-    mchKey: '',
-    keyContent: '',
-    privateKeyContent: '',
-    privateCertContent: '',
-    apiV3Key:'',
-  }
-}
-
-export default {
-  name: "wechatChannelForm",
-  components: {},
-  props: {
-    // 传输的参数
-    transferParam: {
-      // 加载动画
-      "loading": false,
-      // 是否修改
-      "edit": false,
-      // 是否显示
-      "wechatOpen": false,
-      // 应用ID
-      "appId": null,
-      // 渠道编码
-      "payCode": null,
-    }
-  },
-  data() {
-    return {
-      title:'',
-      form: JSON.parse(JSON.stringify(defaultForm)),
-      rules: {
-        feeRate: [{
-          required: true,
-          message: '请输入渠道费率',
-          trigger: 'blur'
-        }],
-        'weChatConfig.mchId': [{
-          required: true,
-          message: '请传入商户号',
-          trigger: 'blur'
-        }],
-        'weChatConfig.appId': [{
-          required: true,
-          message: '请输入公众号APPID',
-          trigger: 'blur'
-        }],
-        status: [{
-          required: true,
-          message: '渠道状态不能为空',
-          trigger: 'blur'
-        }],
-        'weChatConfig.apiVersion': [{
-          required: true,
-          message: 'API版本不能为空',
-          trigger: 'blur'
-        }],
-        'weChatConfig.mchKey': [{
-          required: true,
-          message: '请输入商户密钥',
-          trigger: 'blur'
-        }],
-        'weChatConfig.keyContent': [{
-          required: true,
-          message: '请上传 apiclient_cert.p12 证书',
-          trigger: 'blur'
-        }],
-        'weChatConfig.privateKeyContent': [{
-          required: true,
-          message: '请上传 apiclient_key.perm 证书',
-          trigger: 'blur'
-        }],
-        'weChatConfig.privateCertContent': [{
-          required: true,
-          message: '请上传 apiclient_cert.perm证 书',
-          trigger: 'blur'
-        }],
-        'weChatConfig.apiV3Key': [{
-          required: true,
-          message: '请上传 api V3 密钥值',
-          trigger: 'blur'
-        }],
-      },
-      // 渠道状态 数据字典
-      statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
-      versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
-    }
-  },
-  watch: {
-    transferParam: {
-      deep: true,  // 深度监听
-      handler(newVal) {
-        if (newVal.wechatOpen) {
-          this.form.code = newVal.payCode;
-          this.form.appId = newVal.appId;
-          // 只有在初次进来为编辑 并且为加载中的时候才回去请求数据
-          if (newVal.edit && newVal.loading) {
-            this.title = "编辑支付渠道";
-            this.init();
-          } else {
-            this.title = "创建支付渠道";
-          }
-        }
-      }
-    }
-  },
-  methods: {
-    init() {
-      getChannel(this.transferParam.appId, this.transferParam.payCode)
-        .then(response => {
-          this.form.id = response.data.id;
-          this.form.feeRate = response.data.feeRate;
-          this.form.appId = response.data.appId;
-          this.form.status = response.data.status;
-          this.form.remark = response.data.remark;
-
-          let config = JSON.parse(response.data.config);
-          this.form.weChatConfig.appId = config.appId;
-          this.form.weChatConfig.apiVersion = config.apiVersion;
-          this.form.weChatConfig.mchId = config.mchId;
-          this.form.weChatConfig.mchKey = config.mchKey;
-          this.form.weChatConfig.keyContent = config.keyContent;
-          this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
-          this.form.weChatConfig.privateCertContent = config.privateCertContent;
-          this.form.weChatConfig.apiV3Key = config.apiV3Key;
-          this.transferParam.loading = false;
-        })
-    },
-    close() {
-      this.transferParam.wechatOpen = false;
-      this.form = JSON.parse(JSON.stringify(defaultForm));
-    },
-    handleConfirm() {
-      this.$refs['wechatJsApiForm'].validate(valid => {
-        if (!valid) {
-          return
-        }
-        let data = this.form;
-        data.config = JSON.stringify(this.form.weChatConfig);
-        if (this.transferParam.edit) {
-          updateChannel(data).then(response => {
-            if (response.code === 0) {
-              this.$modal.msgSuccess("修改成功");
-              this.close();
-            }
-          })
-        } else {
-
-          createChannel(data).then(response => {
-            if (response.code === 0) {
-              this.$modal.msgSuccess("新增成功");
-              this.$parent.refreshTable();
-              this.close();
-            }
-          });
-        }
-      });
-    },
-    /**
-     * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
-     */
-    fileBeforeUpload(file, fileAccept) {
-      let format = '.' + file.name.split(".")[1];
-      if (format !== fileAccept) {
-        debugger
-        this.$message.error('请上传指定格式"' + fileAccept + '"文件');
-        return false;
-      }
-      let isRightSize = file.size / 1024 / 1024 < 2
-      if (!isRightSize) {
-        this.$message.error('文件大小超过 2MB')
-      }
-      return isRightSize
-    },
-    p12FileBeforeUpload(file) {
-      this.fileBeforeUpload(file, '.p12')
-    },
-    pemFileBeforeUpload(file) {
-      this.fileBeforeUpload(file, '.pem')
-    },
-    /**
-     * 读取 apiclient_key.pem 到 privateKeyContent 字段
-     */
-    privateKeyContentUpload(event) {
-      const readFile = new FileReader()
-      readFile.onload = (e) => {
-        this.form.weChatConfig.privateKeyContent = e.target.result
-      }
-      readFile.readAsText(event.file);
-    },
-    /**
-     * 读取 apiclient_cert.pem 到 privateCertContent 字段
-     */
-    privateCertContentUpload(event) {
-      const readFile = new FileReader()
-      readFile.onload = (e) => {
-        this.form.weChatConfig.privateCertContent = e.target.result
-      }
-      readFile.readAsText(event.file);
-    },
-    /**
-     * 读取 apiclient_cert.p12 到 keyContent 字段
-     */
-    keyContentUpload(event) {
-      const readFile = new FileReader()
-      readFile.onload = (e) => {
-        this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
-      }
-      readFile.readAsDataURL(event.file); // 读成 base64
-    }
-  }
-}
-
-</script>
-<style scoped>
-
-</style>

+ 257 - 0
yudao-ui-admin/src/views/pay/app/components/weixinChannelForm.vue

@@ -0,0 +1,257 @@
+<template>
+  <div>
+    <el-dialog :visible.sync="dialogVisible" :title="title" @close="close" append-to-body width="800px">
+      <el-form ref="form" :model="formData" :rules="rules" size="medium" label-width="120px"
+               v-loading="formLoading">
+        <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
+          <el-input v-model="formData.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
+            <template slot="append">%</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label-width="180px" label="公众号 APPID" prop="config.appId">
+          <el-input v-model="formData.config.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
+          </el-input>
+        </el-form-item>
+        <el-form-item label-width="180px" label="商户号" prop="config.mchId">
+          <el-input v-model="formData.config.mchId" :style="{width: '100%'}"></el-input>
+        </el-form-item>
+        <el-form-item label-width="180px" label="渠道状态" prop="status">
+          <el-radio-group v-model="formData.status" size="medium">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)" :key="parseInt(dict.value)"
+                      :label="parseInt(dict.value)">
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label-width="180px" label="API 版本" prop="config.apiVersion">
+          <el-radio-group v-model="formData.config.apiVersion" size="medium">
+            <el-radio label="v2">v2</el-radio>
+            <el-radio label="v3">v3</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <div v-if="formData.config.apiVersion === 'v2'">
+          <el-form-item label-width="180px" label="商户密钥" prop="config.mchKey">
+            <el-input v-model="formData.config.mchKey" placeholder="请输入商户密钥" clearable
+                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="config.keyContent">
+            <el-input v-model="formData.config.keyContent" type="textarea"
+                      placeholder="请上传 apiclient_cert.p12 证书"
+                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload :limit="1" accept=".p12" action=""
+                       :before-upload="p12FileBeforeUpload"
+                       :http-request="keyContentUpload">
+              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
+            </el-upload>
+          </el-form-item>
+        </div>
+        <div v-if="formData.config.apiVersion === 'v3'">
+          <el-form-item label-width="180px" label="API V3 密钥" prop="config.apiV3Key">
+            <el-input v-model="formData.config.apiV3Key" placeholder="请输入 API V3 密钥" clearable
+                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="config.privateKeyContent">
+            <el-input v-model="formData.config.privateKeyContent" type="textarea"
+                      placeholder="请上传 apiclient_key.perm 证书"
+                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
+            <el-upload ref="privateKeyContentFile"
+                       :limit="1"
+                       accept=".pem"
+                       action=""
+                       :before-upload="pemFileBeforeUpload"
+                       :http-request="privateKeyContentUpload"
+            >
+              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label-width="180px" label="apiclient_cert.perm证书" prop="config.privateCertContent">
+            <el-input v-model="formData.config.privateCertContent" type="textarea"
+                      placeholder="请上传apiclient_cert.perm证书"
+                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="" prop="privateCertContentFile">
+            <el-upload ref="privateCertContentFile"
+                       :limit="1"
+                       accept=".pem"
+                       action=""
+                       :before-upload="pemFileBeforeUpload"
+                       :http-request="privateCertContentUpload"
+            >
+              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
+            </el-upload>
+          </el-form-item>
+        </div>
+        <el-form-item label-width="180px" label="备注" prop="remark">
+          <el-input v-model="formData.remark" :style="{width: '100%'}" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="close">取消</el-button>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { createChannel, getChannel, updateChannel } from "@/api/pay/channel";
+import { CommonStatusEnum } from "@/utils/constants";
+
+export default {
+  name: "weixinChannelForm",
+  data() {
+    return {
+      dialogVisible: false,
+      formLoading: false,
+      title:'',
+      formData: {
+        appId: '',
+        code: '',
+        status: undefined,
+        feeRate: undefined,
+        remark: '',
+        config: {
+          appId: '',
+          mchId: '',
+          apiVersion: '',
+          mchKey: '',
+          keyContent: '',
+          privateKeyContent: '',
+          privateCertContent: '',
+          apiV3Key:'',
+        }
+      },
+      rules: {
+        feeRate: [{ required: true, message: '请输入渠道费率', trigger: 'blur' }],
+        status: [{ required: true, message: '渠道状态不能为空', trigger: 'blur'}],
+        'config.mchId': [{ required: true, message: '请传入商户号', trigger: 'blur'}],
+        'config.appId': [{ required: true, message: '请输入公众号APPID', trigger: 'blur'}],
+        'config.apiVersion': [{ required: true, message: 'API版本不能为空', trigger: 'blur'}],
+        'config.mchKey': [{ required: true, message: '请输入商户密钥', trigger: 'blur' }],
+        'config.keyContent': [{ required: true, message: '请上传 apiclient_cert.p12 证书', trigger: 'blur' }],
+        'config.privateKeyContent': [{ required: true, message: '请上传 apiclient_key.perm 证书', trigger: 'blur' }],
+        'config.privateCertContent': [{ required: true, message: '请上传 apiclient_cert.perm证 书', trigger: 'blur' }],
+        'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }],
+      },
+    }
+  },
+  methods: {
+    open(appId, code) {
+      this.dialogVisible = true;
+      this.formLoading = true;
+      this.reset(appId, code);
+      getChannel(appId, code).then(response => {
+        if (response.data && response.data.id) {
+          this.formData = response.data;
+          this.formData.config = JSON.parse(response.data.config);
+        }
+        this.title = !this.formData.id ? '创建支付渠道' : '编辑支付渠道'
+      }).finally(() => {
+        this.formLoading = false;
+      });
+    },
+    close() {
+      this.dialogVisible = false;
+      this.reset(undefined, undefined);
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) {
+          return
+        }
+        const data = { ...this.formData };
+        data.config = JSON.stringify(this.formData.config);
+        if (!data.id) {
+          createChannel(data).then(response => {
+            this.$modal.msgSuccess("新增成功");
+            this.$emit('success')
+            this.close();
+          });
+        } else {
+          updateChannel(data).then(response => {
+            this.$modal.msgSuccess("修改成功");
+            this.$emit('success')
+            this.close();
+          })
+        }
+      });
+    },
+    /** 重置表单 */
+    reset(appId, code) {
+      this.formData = {
+        appId: appId,
+        code: code,
+        status: CommonStatusEnum.ENABLE,
+        feeRate: undefined,
+        remark: '',
+        config: {
+          appId: '',
+          mchId: '',
+          apiVersion: '',
+          mchKey: '',
+          keyContent: '',
+          privateKeyContent: '',
+          privateCertContent: '',
+          apiV3Key:'',
+        }
+      }
+      this.resetForm('form')
+    },
+    /**
+     * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
+     */
+    fileBeforeUpload(file, fileAccept) {
+      let format = '.' + file.name.split(".")[1];
+      if (format !== fileAccept) {
+        debugger
+        this.$message.error('请上传指定格式"' + fileAccept + '"文件');
+        return false;
+      }
+      let isRightSize = file.size / 1024 / 1024 < 2
+      if (!isRightSize) {
+        this.$message.error('文件大小超过 2MB')
+      }
+      return isRightSize
+    },
+    p12FileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.p12')
+    },
+    pemFileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.pem')
+    },
+    /**
+     * 读取 apiclient_key.pem 到 privateKeyContent 字段
+     */
+    privateKeyContentUpload(event) {
+      const readFile = new FileReader()
+      readFile.onload = (e) => {
+        this.formData.config.privateKeyContent = e.target.result
+      }
+      readFile.readAsText(event.file);
+    },
+    /**
+     * 读取 apiclient_cert.pem 到 privateCertContent 字段
+     */
+    privateCertContentUpload(event) {
+      const readFile = new FileReader()
+      readFile.onload = (e) => {
+        this.formData.config.privateCertContent = e.target.result
+      }
+      readFile.readAsText(event.file);
+    },
+    /**
+     * 读取 apiclient_cert.p12 到 keyContent 字段
+     */
+    keyContentUpload(event) {
+      const readFile = new FileReader()
+      readFile.onload = (e) => {
+        this.formData.config.keyContent = e.target.result.split(',')[1]
+      }
+      readFile.readAsDataURL(event.file); // 读成 base64
+    }
+  }
+}
+</script>

+ 61 - 111
yudao-ui-admin/src/views/pay/app/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container">
-
+    <doc-alert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="应用名" prop="name">
@@ -9,7 +9,7 @@
       </el-form-item>
       <el-form-item label="开启状态" prop="status">
         <el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable>
-          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label"
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)" :key="parseInt(dict.value)" :label="dict.label"
                      :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
@@ -43,65 +43,59 @@
                      @change="handleStatusChange(scope.row)"/>
         </template>
       </el-table-column>
-      <el-table-column label="商户名称" align="center" prop="payMerchant.name"/>
       <el-table-column label="支付宝配置" align="center">
         <el-table-column :label="payChannelEnum.ALIPAY_APP.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.ALIPAY_APP.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.ALIPAY_APP.code,payType.ALIPAY)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.ALIPAY_APP.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_APP.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.ALIPAY_APP.code,payType.ALIPAY)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_APP.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.ALIPAY_PC.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.ALIPAY_PC.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.ALIPAY_PC.code,payType.ALIPAY)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.ALIPAY_PC.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_PC.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.ALIPAY_PC.code,payType.ALIPAY)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_PC.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.ALIPAY_WAP.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.ALIPAY_WAP.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.ALIPAY_WAP.code,payType.ALIPAY)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.ALIPAY_WAP.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_WAP.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.ALIPAY_WAP.code,payType.ALIPAY)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_WAP.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.ALIPAY_QR.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.ALIPAY_QR.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.ALIPAY_QR.code,payType.ALIPAY)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.ALIPAY_QR.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_QR.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.ALIPAY_QR.code,payType.ALIPAY)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_QR.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.ALIPAY_BAR.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.ALIPAY_BAR.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.ALIPAY_BAR.code,payType.ALIPAY)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.ALIPAY_BAR.code)"
+                       @click="handleChannel(scope.row,payChannelEnum.ALIPAY_BAR.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.ALIPAY_BAR.code,payType.ALIPAY)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.ALIPAY_BAR.code)">
             </el-button>
           </template>
         </el-table-column>
@@ -110,60 +104,55 @@
         <el-table-column :label="payChannelEnum.WX_LITE.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_LITE.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_LITE.code,payType.WECHAT)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.WX_LITE.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.WX_LITE.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_LITE.code,payType.WECHAT)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.WX_LITE.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.WX_PUB.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_PUB.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_PUB.code,payType.WECHAT)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.WX_PUB.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.WX_PUB.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_PUB.code,payType.WECHAT)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.WX_PUB.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.WX_APP.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_APP.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_APP.code,payType.WECHAT)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.WX_APP.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.WX_APP.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_APP.code,payType.WECHAT)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.WX_APP.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.WX_NATIVE.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.WX_NATIVE.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.WX_NATIVE.code)">
             </el-button>
           </template>
         </el-table-column>
         <el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
           <template v-slot="scope">
             <el-button type="success" icon="el-icon-check" circle
-                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
-                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+                       v-if="isChannelExists(scope.row.channelCodes, payChannelEnum.WX_BAR.code)"
+                       @click="handleChannel(scope.row, payChannelEnum.WX_BAR.code)">
             </el-button>
-            <el-button v-else
-                       type="danger" icon="el-icon-close" circle
-                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+            <el-button v-else type="danger" icon="el-icon-close" circle
+                       @click="handleChannel(scope.row, payChannelEnum.WX_BAR.code)">
             </el-button>
           </template>
         </el-table-column>
@@ -191,7 +180,7 @@
         </el-form-item>
         <el-form-item label="开启状态" prop="status">
           <el-radio-group v-model="form.status">
-            <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
               {{ dict.label }}
             </el-radio>
           </el-radio-group>
@@ -214,24 +203,23 @@
       </div>
     </el-dialog>
 
-    <!-- 弹窗 -->
-    <wechat-channel-form :transferParam="channelParam" />
+    <!-- 对话框(支付应用的配置) -->
+    <weixin-channel-form ref="weixinChannelFormRef" @success="getList" />
     <alipay-channel-form ref="alipayChannelFormRef" @success="getList" />
   </div>
 </template>
 
 <script>
 import { createApp, updateApp, changeAppStatus, deleteApp, getApp, getAppPage } from "@/api/pay/app";
-import { DICT_TYPE, getDictDatas } from "@/utils/dict";
-import { PayType, PayChannelEnum, CommonStatusEnum } from "@/utils/constants";
-import wechatChannelForm from "@/views/pay/app/components/wechatChannelForm";
+import { PayChannelEnum, CommonStatusEnum } from "@/utils/constants";
+import weixinChannelForm from "@/views/pay/app/components/weixinChannelForm";
 import alipayChannelForm from "@/views/pay/app/components/alipayChannelForm";
 
 export default {
   name: "PayApp",
   components: {
-    "wechatChannelForm": wechatChannelForm,
-    "alipayChannelForm": alipayChannelForm
+    weixinChannelForm,
+    alipayChannelForm
   },
   data() {
     return {
@@ -264,28 +252,8 @@ export default {
         orderNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
         refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}],
       },
-      // 数据字典
-      statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
-      sysCommonStatusEnum: CommonStatusEnum,
       // 支付渠道枚举
       payChannelEnum: PayChannelEnum,
-      // 支付类型
-      payType: PayType,
-      // 是否显示支付窗口
-      payOpen: false,
-      // 微信组件传参参数
-      channelParam: {
-        // 是否修改
-        "edit": false,
-        // 微信是否显示
-        "wechatOpen": false,
-        // 支付宝是否显示
-        "aliPayOpen": false,
-        // 应用ID
-        "appId": null,
-        // 渠道编码
-        "payCode": null,
-      }
     };
   },
   created() {
@@ -312,7 +280,7 @@ export default {
       this.form = {
         id: undefined,
         name: undefined,
-        status: undefined,
+        status: CommonStatusEnum.ENABLE,
         remark: undefined,
         orderNotifyUrl: undefined,
         refundNotifyUrl: undefined,
@@ -397,43 +365,25 @@ export default {
     /**
      * 修改支付渠道信息
      */
-    handleUpdateChannel(row, payCode, type) {
-      this.settingChannelParam(row, payCode, type)
-      this.channelParam.edit = true;
-      this.channelParam.loading = true;
-    },
-    /**
-     * 新增支付渠道信息
-     */
-    handleCreateChannel(row, payCode, type) {
-      this.settingChannelParam(row, payCode, type)
-      this.channelParam.edit = false;
-      this.channelParam.loading = false;
-    },
-    /**
-     * 设置支付渠道信息
-     */
-    settingChannelParam(row, payCode, type) {
-      if (type === PayType.WECHAT) {
-        this.channelParam.wechatOpen = true;
-        this.channelParam.aliPayOpen = false;
+    handleChannel(row, code) {
+      if (code.indexOf('alipay_') === 0) {
+        this.$refs['alipayChannelFormRef'].open(row.id, code);
+        return
       }
-      if (type === PayType.ALIPAY) {
-        console.log(this.$refs['alipayChannelFormRef'])
-        this.$refs['alipayChannelFormRef'].open(row.id, payCode);
-        return;
-        // this.channelParam.aliPayOpen = true;
-        // this.channelParam.wechatOpen = false;
+      if (code.indexOf('wx_') === 0) {
+        this.$refs['weixinChannelFormRef'].open(row.id, code);
+        return
       }
     },
     /**
      * 根据渠道编码判断渠道列表中是否存在
+     *
      * @param channels 渠道列表
      * @param channelCode 渠道编码
      */
-    judgeChannelExist(channels, channelCode) {
-      return channels.indexOf(channelCode) !== -1;
+    isChannelExists(channels, channelCode) {
+      return channels && channels.indexOf(channelCode) !== -1;
     }
   }
-};
+}
 </script>

+ 0 - 1
yudao-ui-admin/src/views/pay/cashier/index.vue

@@ -78,7 +78,6 @@
 </template>
 <script>
 import QrcodeVue from 'qrcode.vue'
-import { DICT_TYPE, getDictDatas } from "@/utils/dict";
 import { getOrder, submitOrder } from '@/api/pay/order';
 import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from "@/utils/constants";
 

+ 5 - 0
yudao-ui-admin/src/views/pay/demo/index.vue

@@ -1,5 +1,10 @@
 <template>
   <div class="app-container">
+    <doc-alert title="支付宝支付接入" url="https://doc.iocoder.cn/pay/alipay-pay-demo/" />
+    <doc-alert title="支付宝、微信退款接入" url="https://doc.iocoder.cn/pay/refund-demo/" />
+    <doc-alert title="微信公众号支付接入" url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/" />
+    <doc-alert title="微信小程序支付接入" url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/" />
+
     <!-- 操作工具栏 -->
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">

+ 1 - 0
yudao-ui-admin/src/views/pay/notify/index.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="app-container">
+    <doc-alert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
 
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">

+ 3 - 0
yudao-ui-admin/src/views/pay/order/index.vue

@@ -1,5 +1,8 @@
 <template>
   <div class="app-container">
+    <doc-alert title="支付宝支付接入" url="https://doc.iocoder.cn/pay/alipay-pay-demo/" />
+    <doc-alert title="微信公众号支付接入" url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/" />
+    <doc-alert title="微信小程序支付接入" url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/" />
 
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="120px">

+ 2 - 0
yudao-ui-admin/src/views/pay/refund/index.vue

@@ -1,5 +1,7 @@
 <template>
   <div class="app-container">
+    <doc-alert title="支付宝、微信退款接入" url="https://doc.iocoder.cn/pay/refund-demo/" />
+
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="120px">
       <el-form-item label="应用编号" prop="appId">

Неке датотеке нису приказане због велике количине промена