Browse Source

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuMessageController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/AppKeFuMessageController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java
#	yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImplTest.java
#	yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
YunaiV 1 month ago
parent
commit
bbb1bec1b9
84 changed files with 1177 additions and 325 deletions
  1. 2 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java
  2. 6 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
  3. 2 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/AppProductBrowseHistoryController.java
  4. 9 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java
  5. 20 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuConversationController.java
  6. 11 12
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuMessageController.java
  7. 34 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageListReqVO.java
  8. 0 15
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessagePageReqVO.java
  9. 0 18
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityPageReqVO.java
  10. 23 8
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/AppKeFuMessageController.java
  11. 22 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessagePageReqVO.java
  12. 0 16
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java
  13. 3 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  14. 16 14
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuMessageMapper.java
  15. 1 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointActivityMapper.java
  16. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  17. 8 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationService.java
  18. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationServiceImpl.java
  19. 5 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageService.java
  20. 10 11
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java
  21. 2 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  22. 0 3
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java
  23. 12 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/brokerage/BrokerageWithdrawTypeEnum.java
  24. 4 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/base/system/package-info.java
  25. 19 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/base/system/user/UserSimpleBaseVO.java
  26. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageRecordController.java
  27. 22 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageWithdrawController.java
  28. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java
  29. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java
  30. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java
  31. 26 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java
  32. 24 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpBindReqVO.java
  33. 5 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java
  34. 5 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreSimpleRespVO.java
  35. 4 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.java
  36. 1 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/delivery/DeliveryPickUpStoreConvert.java
  37. 15 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java
  38. 0 49
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreStaffDO.java
  39. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java
  40. 0 14
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/delivery/DeliveryPickUpStoreStaffMapper.java
  41. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordService.java
  42. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java
  43. 13 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawService.java
  44. 107 39
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  45. 13 7
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryPickUpStoreService.java
  46. 23 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java
  47. 4 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
  48. 15 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  49. 12 13
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImplTest.java
  50. 2 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
  51. 10 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java
  52. 31 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferRespDTO.java
  53. 19 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java
  54. 49 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java
  55. 7 1
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
  56. 10 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java
  57. 33 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java
  58. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
  59. 28 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
  60. 2 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
  61. 7 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
  62. 13 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java
  63. 9 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java
  64. 5 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
  65. 10 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
  66. 21 9
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
  67. 5 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
  68. 1 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml
  69. 12 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  70. 14 4
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
  71. 129 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/WxPayTransferPartnerNotifyV3Result.java
  72. 16 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  73. 6 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  74. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java
  75. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java
  76. 2 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java
  77. 5 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
  78. 79 4
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  79. 118 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java
  80. 3 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java
  81. 2 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java
  82. 7 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
  83. 2 1
      yudao-server/src/main/resources/application-dev.yaml
  84. 1 0
      yudao-server/src/main/resources/application-local.yaml

+ 2 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java

@@ -51,7 +51,8 @@ public class ProductBrowseHistoryController {
                 convertSet(pageResult.getList(), ProductBrowseHistoryDO::getSpuId));
         return success(BeanUtils.toBean(pageResult, ProductBrowseHistoryRespVO.class,
                 vo -> Optional.ofNullable(spuMap.get(vo.getSpuId()))
-                        .ifPresent(spu -> vo.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setPrice(spu.getPrice()))));
+                        .ifPresent(spu -> vo.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setPrice(spu.getPrice())
+                                .setSalesCount(spu.getSalesCount()).setStock(spu.getStock()))));
     }
 
 }

+ 6 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java

@@ -1,12 +1,9 @@
 package cn.iocoder.yudao.module.product.controller.admin.history.vo;
 
 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import java.time.LocalDateTime;
-
 import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
 
 @Schema(description = "管理后台 - 商品浏览记录 Response VO")
@@ -31,4 +28,10 @@ public class ProductBrowseHistoryRespVO {
     @Schema(description = "商品单价", example = "100")
     private Integer price;
 
+    @Schema(description = "商品销量", example = "100")
+    private Integer salesCount;
+
+    @Schema(description = "库存", example = "100")
+    private Integer stock;
+
 }

+ 2 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/AppProductBrowseHistoryController.java

@@ -67,7 +67,8 @@ public class AppProductBrowseHistoryController {
         Map<Long, ProductSpuDO> spuMap = convertMap(productSpuService.getSpuList(spuIds), ProductSpuDO::getId);
         return success(BeanUtils.toBean(pageResult, AppProductBrowseHistoryRespVO.class,
                 vo -> Optional.ofNullable(spuMap.get(vo.getSpuId()))
-                        .ifPresent(spu -> vo.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setPrice(spu.getPrice()))));
+                        .ifPresent(spu -> vo.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setPrice(spu.getPrice())
+                                .setSalesCount(spu.getSalesCount()).setStock(spu.getStock()))));
     }
 
 }

+ 9 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java

@@ -17,13 +17,19 @@ public class AppProductBrowseHistoryRespVO {
 
     // ========== 商品相关字段 ==========
 
-    @Schema(description = "商品 SPU 名称", example = "赵六")
+    @Schema(description = "商品 SPU 名称", requiredMode = REQUIRED, example = "赵六")
     private String spuName;
 
-    @Schema(description = "商品封面图", example = "https://domain/pic.png")
+    @Schema(description = "商品封面图", requiredMode = REQUIRED, example = "https://www.iocoder.cn/pic.png")
     private String picUrl;
 
-    @Schema(description = "商品单价", example = "100")
+    @Schema(description = "商品单价", requiredMode = REQUIRED, example = "50")
     private Integer price;
 
+    @Schema(description = "商品销量", requiredMode = REQUIRED, example = "60")
+    private Integer salesCount;
+
+    @Schema(description = "库存", requiredMode = REQUIRED, example = "80")
+    private Integer stock;
+
 }

+ 20 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuConversationController.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
 import cn.iocoder.yudao.module.promotion.service.kefu.KeFuConversationService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -34,6 +35,25 @@ public class KeFuConversationController {
     @Resource
     private MemberUserApi memberUserApi;
 
+    @GetMapping("/get")
+    @Operation(summary = "获得客服会话")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:query')")
+    public CommonResult<KeFuConversationRespVO> getConversation(@RequestParam("id") Long id) {
+        KeFuConversationDO conversation = conversationService.getConversation(id);
+        if (conversation == null) {
+            return success(null);
+        }
+
+        // 拼接数据
+        KeFuConversationRespVO result = BeanUtils.toBean(conversation, KeFuConversationRespVO.class);
+        MemberUserRespDTO memberUser = memberUserApi.getUser(conversation.getUserId());
+        if (memberUser != null) {
+            result.setUserAvatar(memberUser.getAvatar()).setUserNickname(memberUser.getNickname());
+        }
+        return success(result);
+    }
+
     @PutMapping("/update-conversation-pinned")
     @Operation(summary = "置顶/取消置顶客服会话")
     @PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:update')")

+ 11 - 12
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/KeFuMessageController.java

@@ -2,9 +2,8 @@ package cn.iocoder.yudao.module.promotion.controller.admin.kefu;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
@@ -14,12 +13,13 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -56,19 +56,18 @@ public class KeFuMessageController {
         return success(true);
     }
 
-    @GetMapping("/page")
-    @Operation(summary = "获得客服消息分页")
+    @GetMapping("/list")
+    @Operation(summary = "获得客服消息列表")
     @PreAuthorize("@ss.hasPermission('promotion:kefu-message:query')")
-    public CommonResult<PageResult<KeFuMessageRespVO>> getKeFuMessagePage(@Valid KeFuMessagePageReqVO pageReqVO) {
+    public CommonResult<List<KeFuMessageRespVO>> getKeFuMessageList(@Valid KeFuMessageListReqVO pageReqVO) {
         // 获得数据
-        PageResult<KeFuMessageDO> pageResult = messageService.getKeFuMessagePage(pageReqVO);
+        List<KeFuMessageDO> list = messageService.getKeFuMessageList(pageReqVO);
 
         // 拼接数据
-        PageResult<KeFuMessageRespVO> result = BeanUtils.toBean(pageResult, KeFuMessageRespVO.class);
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(filterList(result.getList(),
+        List<KeFuMessageRespVO> result = BeanUtils.toBean(list, KeFuMessageRespVO.class);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(filterList(result,
                 item -> UserTypeEnum.ADMIN.getValue().equals(item.getSenderType())), KeFuMessageRespVO::getSenderId));
-        result.getList().forEach(item-> findAndThen(userMap, item.getSenderId(),
-                user -> item.setSenderAvatar(user.getAvatar())));
+        result.forEach(item -> findAndThen(userMap, item.getSenderId(), user -> item.setSenderAvatar(user.getAvatar())));
         return success(result);
     }
 

+ 34 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageListReqVO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 客服消息列表 Request VO")
+@Data
+public class KeFuMessageListReqVO {
+
+    private static final Integer LIMIT = 10;
+
+    @Schema(description = "会话编号", example = "12580")
+    @NotNull(message = "会话编号不能为空")
+    private Long conversationId;
+
+    @Schema(description = "发送时间", example = "2024-03-27 12:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime createTime;
+
+    @Schema(description = "每次查询条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "每次查询条数不能为空")
+    @Min(value = 1, message = "每次查询条数最小值为 1")
+    @Max(value = 100, message = "每次查询最大值为 100")
+    private Integer limit = LIMIT;
+
+}

+ 0 - 15
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessagePageReqVO.java

@@ -1,15 +0,0 @@
-package cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message;
-
-import lombok.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-
-@Schema(description = "管理后台 - 客服消息分页 Request VO")
-@Data
-@ToString(callSuper = true)
-public class KeFuMessagePageReqVO extends PageParam {
-
-    @Schema(description = "会话编号", example = "12580")
-    private Long conversationId;
-
-}

+ 0 - 18
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityPageReqVO.java

@@ -5,11 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
-import org.springframework.format.annotation.DateTimeFormat;
-
-import java.time.LocalDateTime;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
 @Schema(description = "管理后台 - 积分商城活动分页 Request VO")
 @Data
@@ -17,20 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class PointActivityPageReqVO extends PageParam {
 
-    @Schema(description = "积分商城活动商品", example = "19509")
-    private Long spuId;
-
     @Schema(description = "活动状态", example = "2")
     private Integer status;
 
-    @Schema(description = "备注", example = "你说的对")
-    private String remark;
-
-    @Schema(description = "排序")
-    private Integer sort;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
 }

+ 23 - 8
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/AppKeFuMessageController.java

@@ -2,23 +2,29 @@ package cn.iocoder.yudao.module.promotion.controller.app.kefu;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
 import cn.iocoder.yudao.module.promotion.service.kefu.KeFuMessageService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "用户 APP - 客服消息")
@@ -30,6 +36,9 @@ public class AppKeFuMessageController {
     @Resource
     private KeFuMessageService kefuMessageService;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @PostMapping("/send")
     @Operation(summary = "发送客服消息")
     public CommonResult<Long> sendKefuMessage(@Valid @RequestBody AppKeFuMessageSendReqVO sendReqVO) {
@@ -45,11 +54,17 @@ public class AppKeFuMessageController {
         return success(true);
     }
 
-    @GetMapping("/page")
-    @Operation(summary = "获得客服消息分页")
-    public CommonResult<PageResult<KeFuMessageRespVO>> getKefuMessagePage(@Valid AppKeFuMessagePageReqVO pageReqVO) {
-        PageResult<KeFuMessageDO> pageResult = kefuMessageService.getKeFuMessagePage(pageReqVO, getLoginUserId());
-        return success(BeanUtils.toBean(pageResult, KeFuMessageRespVO.class));
+    @GetMapping("/list")
+    @Operation(summary = "获得客服消息列表")
+    public CommonResult<List<KeFuMessageRespVO>> getKefuMessageList(@Valid AppKeFuMessagePageReqVO pageReqVO) {
+        List<KeFuMessageDO> list = kefuMessageService.getKeFuMessageList(pageReqVO, getLoginUserId());
+
+        // 拼接数据
+        List<KeFuMessageRespVO> result = BeanUtils.toBean(list, KeFuMessageRespVO.class);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(filterList(result,
+                item -> UserTypeEnum.ADMIN.getValue().equals(item.getSenderType())), KeFuMessageRespVO::getSenderId));
+        result.forEach(item -> findAndThen(userMap, item.getSenderId(), user -> item.setSenderAvatar(user.getAvatar())));
+        return success(result);
     }
 
 }

+ 22 - 6
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessagePageReqVO.java

@@ -1,17 +1,33 @@
 package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message;
 
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
 
-@Schema(description = "用户 App - 客服消息分页 Request VO")
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "用户 App - 客服消息 Request VO")
 @Data
-@ToString(callSuper = true)
-public class AppKeFuMessagePageReqVO extends PageParam {
+public class AppKeFuMessagePageReqVO {
+
+    private static final Integer LIMIT = 10;
 
     @Schema(description = "会话编号", example = "12580")
     private Long conversationId;
 
+    @Schema(description = "发送时间", example = "2024-03-27 12:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime createTime;
+
+    @Schema(description = "每次查询条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "每次查询条数不能为空")
+    @Min(value = 1, message = "每次查询条数最小值为 1")
+    @Max(value = 100, message = "每次查询最大值为 100")
+    private Integer limit = LIMIT;
+
 }

+ 0 - 16
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java

@@ -4,8 +4,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import java.time.LocalDateTime;
-
 @Schema(description = "用户 App - 积分商城活动 Response VO")
 @Data
 public class AppPointActivityRespVO {
@@ -30,20 +28,6 @@ public class AppPointActivityRespVO {
     @ExcelProperty("积分商城活动总库存")
     private Integer totalStock;
 
-    // TODO @puhui999:只返回必要的字段,例如说 remark、sort、createTime 应该是不需要的呢。也可以看看别的也不需要哈。
-
-    @Schema(description = "备注", example = "你说的对")
-    @ExcelProperty("备注")
-    private String remark;
-
-    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("排序")
-    private Integer sort;
-
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
     // ========== 商品字段 ==========
 
     @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取

+ 3 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java

@@ -217,8 +217,7 @@ public interface CombinationActivityConvert {
         List<CombinationRecordDO> createRecords = new ArrayList<>(count);
         for (int i = 0; i < count; i++) {
             // 基础信息和团长保持一致
-            CombinationRecordDO newRecord = BeanUtils.toBean(headRecord, CombinationRecordDO.class)
-                    .setId(null).setHeadId(headRecord.getHeadId());
+            CombinationRecordDO newRecord = convert5(headRecord).setHeadId(headRecord.getId());
             // 虚拟信息
             newRecord.setCount(0) // 会单独更新下,在后续的 Service 逻辑里
                     .setUserId(0L).setNickname("").setAvatar("").setOrderId(0L);
@@ -226,5 +225,7 @@ public interface CombinationActivityConvert {
         }
         return createRecords;
     }
+    @Mapping(target = "id", ignore = true)
+    CombinationRecordDO convert5(CombinationRecordDO headRecord);
 
 }

+ 16 - 14
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuMessageMapper.java

@@ -1,10 +1,8 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.kefu;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
+import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -21,10 +19,20 @@ import java.util.List;
 @Mapper
 public interface KeFuMessageMapper extends BaseMapperX<KeFuMessageDO> {
 
-    default PageResult<KeFuMessageDO> selectPage(KeFuMessagePageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<KeFuMessageDO>()
-                .eqIfPresent(KeFuMessageDO::getConversationId, reqVO.getConversationId())
-                .orderByDesc(KeFuMessageDO::getCreateTime));
+    /**
+     * 获得消息列表
+     * 1. 第一次查询时,不带时间,默认查询最新的十条消息
+     * 2. 第二次查询时,带时间,查询历史消息
+     *
+     * @param reqVO 列表请求
+     * @return 消息列表
+     */
+    default List<KeFuMessageDO> selectList(KeFuMessageListReqVO reqVO) {
+        return selectList(new QueryWrapperX<KeFuMessageDO>()
+                .eqIfPresent("conversation_id", reqVO.getConversationId())
+                .ltIfPresent("create_time", reqVO.getCreateTime())
+                .orderByDesc("create_time")
+                .limitN(reqVO.getLimit()));
     }
 
     default List<KeFuMessageDO> selectListByConversationIdAndUserTypeAndReadStatus(Long conversationId, Integer userType,
@@ -40,10 +48,4 @@ public interface KeFuMessageMapper extends BaseMapperX<KeFuMessageDO> {
                 .in(KeFuMessageDO::getId, ids));
     }
 
-    default PageResult<KeFuMessageDO> selectPage(AppKeFuMessagePageReqVO pageReqVO) {
-        return selectPage(pageReqVO, new LambdaQueryWrapperX<KeFuMessageDO>()
-                .eqIfPresent(KeFuMessageDO::getConversationId, pageReqVO.getConversationId())
-                .orderByDesc(KeFuMessageDO::getCreateTime));
-    }
-
 }

+ 1 - 5
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/point/PointActivityMapper.java

@@ -19,12 +19,8 @@ public interface PointActivityMapper extends BaseMapperX<PointActivityDO> {
 
     default PageResult<PointActivityDO> selectPage(PointActivityPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<PointActivityDO>()
-                .eqIfPresent(PointActivityDO::getSpuId, reqVO.getSpuId())
                 .eqIfPresent(PointActivityDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(PointActivityDO::getRemark, reqVO.getRemark())
-                .eqIfPresent(PointActivityDO::getSort, reqVO.getSort())
-                .betweenIfPresent(PointActivityDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(PointActivityDO::getId));
+                .orderByDesc(PointActivityDO::getSort));
     }
 
     /**

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java

@@ -375,7 +375,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
             CombinationRecordDO updateRecord = new CombinationRecordDO().setId(item.getId())
                     .setStatus(status.getStatus()).setEndTime(now);
             if (CombinationRecordStatusEnum.isSuccess(status.getStatus())) { // 虚拟成团完事更改状态成功后还需要把参与人数修改为成团需要人数
-                updateRecord.setUserCount(updateRecord.getUserSize());
+                updateRecord.setUserCount(records.size());
             }
             updateRecords.add(updateRecord);
         });

+ 8 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationService.java

@@ -13,6 +13,14 @@ import java.util.List;
  */
 public interface KeFuConversationService {
 
+    /**
+     * 获得客服会话
+     *
+     * @param id 编号
+     * @return 客服会话
+     */
+    KeFuConversationDO getConversation(Long id);
+
     /**
      * 【管理员】删除客服会话
      *

+ 5 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuConversationServiceImpl.java

@@ -30,6 +30,11 @@ public class KeFuConversationServiceImpl implements KeFuConversationService {
     @Resource
     private KeFuConversationMapper conversationMapper;
 
+    @Override
+    public KeFuConversationDO getConversation(Long id) {
+        return conversationMapper.selectById(id);
+    }
+
     @Override
     public void deleteKefuConversation(Long id) {
         // 校验存在

+ 5 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageService.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.promotion.service.kefu;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
@@ -9,6 +8,8 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
 
 import javax.validation.Valid;
 
+import java.util.List;
+
 /**
  * 客服消息 Service 接口
  *
@@ -47,7 +48,7 @@ public interface KeFuMessageService {
      * @param pageReqVO 分页查询
      * @return 客服消息分页
      */
-    PageResult<KeFuMessageDO> getKeFuMessagePage(KeFuMessagePageReqVO pageReqVO);
+    List<KeFuMessageDO> getKeFuMessageList(KeFuMessageListReqVO pageReqVO);
 
     /**
      * 【会员】获得客服消息分页
@@ -56,6 +57,6 @@ public interface KeFuMessageService {
      * @param userId    用户编号
      * @return 客服消息分页
      */
-    PageResult<KeFuMessageDO> getKeFuMessagePage(AppKeFuMessagePageReqVO pageReqVO, Long userId);
+    List<KeFuMessageDO> getKeFuMessageList(AppKeFuMessagePageReqVO pageReqVO, Long userId);
 
 }

+ 10 - 11
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java

@@ -2,14 +2,12 @@ package cn.iocoder.yudao.module.promotion.service.kefu;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
-import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
@@ -17,12 +15,13 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuMessageMapper;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
+import java.util.Collections;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -113,9 +112,9 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
         // 2.3 发送消息通知会员,管理员已读 -> 会员更新发送的消息状态
         KeFuMessageDO keFuMessage = getFirst(filterList(messageList, message -> UserTypeEnum.MEMBER.getValue().equals(message.getSenderType())));
         assert keFuMessage != null; // 断言避免警告
-        getSelf().sendAsyncMessageToMember(keFuMessage.getSenderId(), KEFU_MESSAGE_ADMIN_READ, StrUtil.EMPTY);
+        getSelf().sendAsyncMessageToMember(keFuMessage.getSenderId(), KEFU_MESSAGE_ADMIN_READ, conversation.getId());
         // 2.4 通知所有管理员消息已读
-        getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_ADMIN_READ, StrUtil.EMPTY);
+        getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_ADMIN_READ, conversation.getId());
     }
 
     private void validateReceiverExist(Long receiverId, Integer receiverType) {
@@ -138,20 +137,20 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
     }
 
     @Override
-    public PageResult<KeFuMessageDO> getKeFuMessagePage(KeFuMessagePageReqVO pageReqVO) {
-        return keFuMessageMapper.selectPage(pageReqVO);
+    public List<KeFuMessageDO> getKeFuMessageList(KeFuMessageListReqVO pageReqVO) {
+        return keFuMessageMapper.selectList(pageReqVO);
     }
 
     @Override
-    public PageResult<KeFuMessageDO> getKeFuMessagePage(AppKeFuMessagePageReqVO pageReqVO, Long userId) {
+    public List<KeFuMessageDO> getKeFuMessageList(AppKeFuMessagePageReqVO pageReqVO, Long userId) {
         // 1. 获得客服会话
         KeFuConversationDO conversation = conversationService.getConversationByUserId(userId);
         if (conversation == null) {
-            return PageResult.empty();
+            return Collections.emptyList();
         }
         // 2. 设置会话编号
         pageReqVO.setConversationId(conversation.getId());
-        return keFuMessageMapper.selectPage(pageReqVO);
+        return keFuMessageMapper.selectList(BeanUtils.toBean(pageReqVO, KeFuMessageListReqVO.class));
     }
 
     private KeFuMessageServiceImpl getSelf() {

+ 2 - 0
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java

@@ -36,6 +36,7 @@ public interface ErrorCodeConstants {
     ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
     ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
     ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
+    ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_034, "交易订单自提失败,原因:你没有核销该门店订单的权限");
 
     // ========== After Sale 模块 1-011-000-100 ==========
     ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@@ -80,6 +81,7 @@ public interface ErrorCodeConstants {
 
     // ==========  物流 PICK_UP 模块 1-011-006-000 ==========
     ErrorCode PICK_UP_STORE_NOT_EXISTS = new ErrorCode(1_011_006_000, "自提门店不存在");
+    ErrorCode PICK_UP_STORE_STAFF_NOT_EXISTS = new ErrorCode(1_011_006_000, "自提门店店员不存在");
 
     // ========== 分销用户 模块 1-011-007-000 ==========
     ErrorCode BROKERAGE_USER_NOT_EXISTS = new ErrorCode(1_011_007_000, "分销用户不存在");

+ 0 - 3
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java

@@ -11,9 +11,6 @@ public interface MessageTemplateConstants {
 
     String SMS_ORDER_DELIVERY = "order_delivery"; // 短信模版编号
 
-    String SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现(审核通过)
-    String SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现(审核不通过)
-
     // ======================= 小程序订阅消息模版 =======================
 
     String WXA_ORDER_DELIVERY = "订单发货通知";

+ 12 - 1
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/brokerage/BrokerageWithdrawTypeEnum.java

@@ -17,8 +17,9 @@ public enum BrokerageWithdrawTypeEnum implements IntArrayValuable {
 
     WALLET(1, "钱包"),
     BANK(2, "银行卡"),
-    WECHAT(3, "微信"),
+    WECHAT(3, "微信"), // 手动打款
     ALIPAY(4, "支付宝"),
+    WECHAT_API(5, "微信零钱"), // 自动打款,通过微信转账 API
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BrokerageWithdrawTypeEnum::getType).toArray();
@@ -37,4 +38,14 @@ public enum BrokerageWithdrawTypeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    /**
+     * 是否通过支付平台的 API 打款
+     *
+     * @param type 类型
+     * @return 是否
+     */
+    public static boolean isApi(Integer type) {
+        return WECHAT_API.getType().equals(type);
+    }
+
 }

+ 4 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/base/system/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位符,可忽略
+ */
+package cn.iocoder.yudao.module.trade.controller.admin.base.system;

+ 19 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/base/system/user/UserSimpleBaseVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.trade.controller.admin.base.system.user;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "用户精简信息 VO")
+@Data
+public class UserSimpleBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long id;
+
+    @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    private String nickname;
+
+    @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
+    private String avatar;
+
+}

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageRecordController.java

@@ -44,7 +44,7 @@ public class BrokerageRecordController {
     @Operation(summary = "获得佣金记录")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-record:query')")
-    public CommonResult<BrokerageRecordRespVO> getBrokerageRecord(@RequestParam("id") Integer id) {
+    public CommonResult<BrokerageRecordRespVO> getBrokerageRecord(@RequestParam("id") Long id) {
         BrokerageRecordDO brokerageRecord = brokerageRecordService.getBrokerageRecord(id);
         return success(BrokerageRecordConvert.INSTANCE.convert(brokerageRecord));
     }

+ 22 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/BrokerageWithdrawController.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRejectReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO;
@@ -14,6 +15,8 @@ import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageWithdrawService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.security.PermitAll;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -24,11 +27,13 @@ import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 
 @Tag(name = "管理后台 - 佣金提现")
 @RestController
 @RequestMapping("/trade/brokerage-withdraw")
 @Validated
+@Slf4j
 public class BrokerageWithdrawController {
 
     @Resource
@@ -40,8 +45,9 @@ public class BrokerageWithdrawController {
     @PutMapping("/approve")
     @Operation(summary = "通过申请")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')")
-    public CommonResult<Boolean> approveBrokerageWithdraw(@RequestParam("id") Integer id) {
-        brokerageWithdrawService.auditBrokerageWithdraw(id, BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, "");
+    public CommonResult<Boolean> approveBrokerageWithdraw(@RequestParam("id") Long id) {
+        brokerageWithdrawService.auditBrokerageWithdraw(id,
+                BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, "", getClientIP());
         return success(true);
     }
 
@@ -49,7 +55,8 @@ public class BrokerageWithdrawController {
     @Operation(summary = "驳回申请")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')")
     public CommonResult<Boolean> rejectBrokerageWithdraw(@Valid @RequestBody BrokerageWithdrawRejectReqVO reqVO) {
-        brokerageWithdrawService.auditBrokerageWithdraw(reqVO.getId(), BrokerageWithdrawStatusEnum.AUDIT_FAIL, reqVO.getAuditReason());
+        brokerageWithdrawService.auditBrokerageWithdraw(reqVO.getId(),
+                BrokerageWithdrawStatusEnum.AUDIT_FAIL, reqVO.getAuditReason(), getClientIP());
         return success(true);
     }
 
@@ -57,7 +64,7 @@ public class BrokerageWithdrawController {
     @Operation(summary = "获得佣金提现")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:query')")
-    public CommonResult<BrokerageWithdrawRespVO> getBrokerageWithdraw(@RequestParam("id") Integer id) {
+    public CommonResult<BrokerageWithdrawRespVO> getBrokerageWithdraw(@RequestParam("id") Long id) {
         BrokerageWithdrawDO brokerageWithdraw = brokerageWithdrawService.getBrokerageWithdraw(id);
         return success(BrokerageWithdrawConvert.INSTANCE.convert(brokerageWithdraw));
     }
@@ -75,4 +82,15 @@ public class BrokerageWithdrawController {
         return success(BrokerageWithdrawConvert.INSTANCE.convertPage(pageResult, userMap));
     }
 
+    // TODO @luchi:update-transferred,url 改成这个。和 update-paid 、update-refunded 保持一致
+    @PostMapping("/update-transfer")
+    @Operation(summary = "更新转账订单为转账成功") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
+    @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
+    public CommonResult<Boolean> updateBrokerageWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
+        log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
+        brokerageWithdrawService.updateBrokerageWithdrawTransferred(
+                Long.parseLong(notifyReqDTO.getMerchantTransferId()), notifyReqDTO.getPayTransferId());
+        return success(true);
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java

@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
 public class BrokerageRecordRespVO extends BrokerageRecordBaseVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28896")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java

@@ -14,7 +14,7 @@ public class BrokerageWithdrawRejectReqVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161")
     @NotNull(message = "编号不能为空")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "审核驳回原因", example = "不对")
     @NotEmpty(message = "审核驳回原因不能为空")

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java

@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
 public class BrokerageWithdrawRespVO extends BrokerageWithdrawBaseVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161")
-    private Integer id;
+    private Long id;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;

+ 26 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java

@@ -1,8 +1,13 @@
 package cn.iocoder.yudao.module.trade.controller.admin.delivery;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import cn.iocoder.yudao.module.trade.controller.admin.base.system.user.UserSimpleBaseVO;
 import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.*;
 import cn.iocoder.yudao.module.trade.convert.delivery.DeliveryPickUpStoreConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO;
@@ -10,12 +15,12 @@ import cn.iocoder.yudao.module.trade.service.delivery.DeliveryPickUpStoreService
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.Collection;
 import java.util.List;
 
@@ -30,6 +35,9 @@ public class DeliveryPickUpStoreController {
     @Resource
     private DeliveryPickUpStoreService deliveryPickUpStoreService;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @PostMapping("/create")
     @Operation(summary = "创建自提门店")
     @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:create')")
@@ -60,10 +68,16 @@ public class DeliveryPickUpStoreController {
     @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')")
     public CommonResult<DeliveryPickUpStoreRespVO> getDeliveryPickUpStore(@RequestParam("id") Long id) {
         DeliveryPickUpStoreDO deliveryPickUpStore = deliveryPickUpStoreService.getDeliveryPickUpStore(id);
-        return success(DeliveryPickUpStoreConvert.INSTANCE.convert(deliveryPickUpStore));
+        if (deliveryPickUpStore == null) {
+            return success(null);
+        }
+        List<AdminUserRespDTO> verifyUsers = CollUtil.isNotEmpty(deliveryPickUpStore.getVerifyUserIds()) ?
+                adminUserApi.getUserList(deliveryPickUpStore.getVerifyUserIds()) : null;
+        return success(BeanUtils.toBean(deliveryPickUpStore, DeliveryPickUpStoreRespVO.class)
+                .setVerifyUsers(BeanUtils.toBean(verifyUsers, UserSimpleBaseVO.class)));
     }
 
-    @GetMapping("/list-all-simple")
+    @GetMapping("/simple-list")
     @Operation(summary = "获得自提门店精简信息列表")
     public CommonResult<List<DeliveryPickUpStoreSimpleRespVO>> getSimpleDeliveryPickUpStoreList() {
         List<DeliveryPickUpStoreDO> list = deliveryPickUpStoreService.getDeliveryPickUpStoreListByStatus(
@@ -88,4 +102,12 @@ public class DeliveryPickUpStoreController {
         return success(DeliveryPickUpStoreConvert.INSTANCE.convertPage(pageResult));
     }
 
+    @PostMapping("/bind")
+    @Operation(summary = "绑定自提店员")
+    @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:create')")
+    public CommonResult<Boolean> bindDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpBindReqVO bindReqVO) {
+        deliveryPickUpStoreService.bindDeliveryPickUpStore(bindReqVO);
+        return success(true);
+    }
+
 }

+ 24 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpBindReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.ToString;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 自提门店绑定核销人 Request VO")
+@Data
+@ToString(callSuper = true)
+public class DeliveryPickUpBindReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+    @Schema(description = "绑定用户编号组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128")
+    @NotEmpty(message = "绑定用户编号组数不能未空")
+    private List<Long> verifyUserIds;
+
+}

+ 5 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java

@@ -1,11 +1,13 @@
 package cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup;
 
+import cn.iocoder.yudao.module.trade.controller.admin.base.system.user.UserSimpleBaseVO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Schema(description = "管理后台 - 自提门店 Response VO")
 @Data
@@ -19,4 +21,7 @@ public class DeliveryPickUpStoreRespVO extends DeliveryPickUpStoreBaseVO {
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
+    @Schema(description = "核销用户数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<UserSimpleBaseVO> verifyUsers;
+
 }

+ 5 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreSimpleRespVO.java

@@ -5,6 +5,8 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.util.List;
+
 @Schema(description = "管理后台 - 自提门店精简信息 Response VO")
 @Data
 @NoArgsConstructor
@@ -29,4 +31,7 @@ public class DeliveryPickUpStoreSimpleRespVO {
     @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号")
     private String detailAddress;
 
+    @Schema(description = "绑定用户编号组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128")
+    private List<Long> verifyUserIds;
+
 }

+ 4 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.java

@@ -10,19 +10,18 @@ import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO;
-import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderLogService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -31,6 +30,7 @@ import java.util.Set;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 交易订单")
 @RestController
@@ -144,7 +144,7 @@ public class TradeOrderController {
     @Parameter(name = "id", description = "交易订单编号")
     @PreAuthorize("@ss.hasPermission('trade:order:pick-up')")
     public CommonResult<Boolean> pickUpOrderById(@RequestParam("id") Long id) {
-        tradeOrderUpdateService.pickUpOrderByAdmin(id);
+        tradeOrderUpdateService.pickUpOrderByAdmin(getLoginUserId(), id);
         return success(true);
     }
 
@@ -153,7 +153,7 @@ public class TradeOrderController {
     @Parameter(name = "pickUpVerifyCode", description = "自提核销码")
     @PreAuthorize("@ss.hasPermission('trade:order:pick-up')")
     public CommonResult<Boolean> pickUpOrderByVerifyCode(@RequestParam("pickUpVerifyCode") String pickUpVerifyCode) {
-        tradeOrderUpdateService.pickUpOrderByAdmin(pickUpVerifyCode);
+        tradeOrderUpdateService.pickUpOrderByAdmin(getLoginUserId(), pickUpVerifyCode);
         return success(true);
     }
 

+ 1 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/delivery/DeliveryPickUpStoreConvert.java

@@ -26,8 +26,6 @@ public interface DeliveryPickUpStoreConvert {
 
     DeliveryPickUpStoreDO convert(DeliveryPickUpStoreUpdateReqVO bean);
 
-    DeliveryPickUpStoreRespVO convert(DeliveryPickUpStoreDO bean);
-
     List<DeliveryPickUpStoreRespVO> convertList(List<DeliveryPickUpStoreDO> list);
 
     PageResult<DeliveryPickUpStoreRespVO> convertPage(PageResult<DeliveryPickUpStoreDO> page);
@@ -43,14 +41,13 @@ public interface DeliveryPickUpStoreConvert {
 
     default List<AppDeliveryPickUpStoreRespVO> convertList(List<DeliveryPickUpStoreDO> list,
                                                            Double latitude, Double longitude) {
-        List<AppDeliveryPickUpStoreRespVO> voList =  CollectionUtils.convertList(list, store -> {
+        return CollectionUtils.convertList(list, store -> {
             AppDeliveryPickUpStoreRespVO storeVO = convert03(store);
             if (latitude != null && longitude != null) {
                 storeVO.setDistance(NumberUtils.getDistance(latitude, longitude, storeVO.getLatitude(), storeVO.getLongitude()));
             }
             return storeVO;
         });
-        return voList;
     }
     @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName")
     AppDeliveryPickUpStoreRespVO convert03(DeliveryPickUpStoreDO bean);

+ 15 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java

@@ -2,19 +2,23 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.delivery;
 
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
 import java.time.LocalTime;
+import java.util.List;
 
 /**
  * 自提门店 DO
  *
  * @author jason
  */
-@TableName(value ="trade_delivery_pick_up_store")
+@TableName(value ="trade_delivery_pick_up_store", autoResultMap = true)
 @KeySequence("trade_delivery_pick_up_store_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 public class DeliveryPickUpStoreDO extends BaseDO {
@@ -74,6 +78,16 @@ public class DeliveryPickUpStoreDO extends BaseDO {
      */
     private Double longitude;
 
+    /**
+     * 核销员工用户编号数组
+     *
+     * 订单自提核销时,只有对应门店的店员才能核销
+     *
+     * 关联 {@link AdminUserRespDTO#getId()} 管理员编号
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> verifyUserIds;
+
     /**
      * 门店状态
      *

+ 0 - 49
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreStaffDO.java

@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.module.trade.dal.dataobject.delivery;
-
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-// TODO @芋艿:后续再详细 review 一轮
-// TODO @芋艿:可能改成 DeliveryPickUpStoreUserDO
-/**
- * 自提门店店员 DO
- *
- * @author jason
- */
-@TableName(value ="trade_delivery_pick_up_store_staff")
-@KeySequence("trade_delivery_pick_up_store_staff_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
-@Data
-public class DeliveryPickUpStoreStaffDO extends BaseDO {
-
-    /**
-     * 编号,自增
-     */
-    @TableId
-    private Long id;
-
-    /**
-     * 自提门店编号
-     *
-     * 关联 {@link DeliveryPickUpStoreDO#getId()}
-     */
-    private Long storeId;
-
-    /**
-     * 管理员用户id
-     *
-     * 关联 {AdminUserDO#getId()}
-     */
-    private Long adminUserId;
-
-    /**
-     * 状态
-     *
-     * 枚举 {@link CommonStatusEnum}
-     */
-    private Integer status;
-
-}

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java

@@ -35,7 +35,7 @@ public interface BrokerageWithdrawMapper extends BaseMapperX<BrokerageWithdrawDO
                 .orderByAsc(BrokerageWithdrawDO::getStatus).orderByDesc(BrokerageWithdrawDO::getId));
     }
 
-    default int updateByIdAndStatus(Integer id, Integer status, BrokerageWithdrawDO updateObj) {
+    default int updateByIdAndStatus(Long id, Integer status, BrokerageWithdrawDO updateObj) {
         return update(updateObj, new LambdaUpdateWrapper<BrokerageWithdrawDO>()
                 .eq(BrokerageWithdrawDO::getId, id)
                 .eq(BrokerageWithdrawDO::getStatus, status));

+ 0 - 14
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/delivery/DeliveryPickUpStoreStaffMapper.java

@@ -1,14 +0,0 @@
-package cn.iocoder.yudao.module.trade.dal.mysql.delivery;
-
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreStaffDO;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface DeliveryPickUpStoreStaffMapper extends BaseMapperX<DeliveryPickUpStoreStaffDO> {
-
-}
-
-
-
-

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordService.java

@@ -32,7 +32,7 @@ public interface BrokerageRecordService {
      * @param id 编号
      * @return 佣金记录
      */
-    BrokerageRecordDO getBrokerageRecord(Integer id);
+    BrokerageRecordDO getBrokerageRecord(Long id);
 
     /**
      * 获得佣金记录分页

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageRecordServiceImpl.java

@@ -64,7 +64,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
     private ProductSkuApi productSkuApi;
 
     @Override
-    public BrokerageRecordDO getBrokerageRecord(Integer id) {
+    public BrokerageRecordDO getBrokerageRecord(Long id) {
         return brokerageRecordMapper.selectById(id);
     }
 

+ 13 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawService.java

@@ -27,8 +27,9 @@ public interface BrokerageWithdrawService {
      * @param id          佣金编号
      * @param status      审核状态
      * @param auditReason 驳回原因
+     * @param userIp 操作 IP
      */
-    void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason);
+    void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp);
 
     /**
      * 获得佣金提现
@@ -36,7 +37,7 @@ public interface BrokerageWithdrawService {
      * @param id 编号
      * @return 佣金提现
      */
-    BrokerageWithdrawDO getBrokerageWithdraw(Integer id);
+    BrokerageWithdrawDO getBrokerageWithdraw(Long id);
 
     /**
      * 获得佣金提现分页
@@ -55,6 +56,16 @@ public interface BrokerageWithdrawService {
      */
     Long createBrokerageWithdraw(Long userId, AppBrokerageWithdrawCreateReqVO createReqVO);
 
+    /**
+     * 【API】更新佣金提现的转账结果
+     *
+     * 目前用于支付回调,标记提现转账结果
+     *
+     * @param id 提现编号
+     * @param payTransferId 转账订单编号
+     */
+    void updateBrokerageWithdrawTransferred(Long id, Long payTransferId);
+
     /**
      * 按照 userId,汇总每个用户的提现
      *

+ 107 - 39
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java

@@ -1,38 +1,48 @@
 package cn.iocoder.yudao.module.trade.service.brokerage;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.date.LocalDateTimeUtil;
-import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
+import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
-import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
+import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
 import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
-import cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
+import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
 import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO;
 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Validator;
+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 javax.validation.Validator;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
 
 /**
@@ -42,6 +52,7 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
  */
 @Service
 @Validated
+@Slf4j
 public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
     @Resource
@@ -54,13 +65,22 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
     @Resource
     private NotifyMessageSendApi notifyMessageSendApi;
+    @Resource
+    private PayTransferApi payTransferApi;
+    @Resource
+    private SocialUserApi socialUserApi;
+    @Resource
+    private PayWalletApi payWalletApi;
 
     @Resource
     private Validator validator;
 
+    @Resource
+    private TradeOrderProperties tradeOrderProperties;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason) {
+    public void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp) {
         // 1.1 校验存在
         BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id);
         // 1.2 校验状态为审核中
@@ -68,41 +88,67 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
             throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
         }
 
-        // 2. 更新
+        // 2. 更新状态
         int rows = brokerageWithdrawMapper.updateByIdAndStatus(id, BrokerageWithdrawStatusEnum.AUDITING.getStatus(),
                 new BrokerageWithdrawDO().setStatus(status.getStatus()).setAuditReason(auditReason).setAuditTime(LocalDateTime.now()));
         if (rows == 0) {
             throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
         }
 
-        String templateCode;
+        // 3.1 审批通过的后续处理
         if (BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.equals(status)) {
-            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE;
-            // 3.1 通过时佣金转余额
-            if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
-                // todo 疯狂:
-            }
-            // TODO 疯狂:调用转账接口
+            auditBrokerageWithdrawSuccess(withdraw);
+        // 3.2 审批不通过的后续处理
         } else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) {
-            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT;
-            // 3.2 驳回时需要退还用户佣金
             brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
                     String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());
         } else {
             throw new IllegalArgumentException("不支持的提现状态:" + status);
         }
+    }
 
-        // 4. 通知用户
-        Map<String, Object> templateParams = MapUtil.<String, Object>builder()
-                .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
-                .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice()))
-                .put("reason", auditReason)
-                .build();
-        notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO()
-                .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams));
+    private void auditBrokerageWithdrawSuccess(BrokerageWithdrawDO withdraw) {
+        // 1.1 钱包
+        if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
+            payWalletApi.addWalletBalance(new PayWalletAddBalanceReqDTO()
+                    .setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
+                    .setBizType(PayWalletBizTypeEnum.BROKERAGE_WITHDRAW.getType()).setBizId(withdraw.getId().toString())
+                    .setPrice(withdraw.getPrice()));
+        // 1.2 微信 API
+        } else if (BrokerageWithdrawTypeEnum.WECHAT_API.getType().equals(withdraw.getType())) {
+            // TODO @luchi:这里,要加个转账单号的记录;另外,调用 API 转账,是立马成功,还是有延迟的哈?
+            Long payTransferId = createPayTransfer(withdraw);
+        // 1.3 剩余类型,都是手动打款,所以不处理
+        } else {
+            // TODO 可优化:未来可以考虑,接入支付宝、银联等 API 转账,实现自动打款
+            log.info("[auditBrokerageWithdrawSuccess][withdraw({}) 类型({}) 手动打款,无需处理]", withdraw.getId(), withdraw.getType());
+        }
+
+        // 2. 非支付 API,则直接体现成功
+        if (!BrokerageWithdrawTypeEnum.isApi(withdraw.getType())) {
+            brokerageWithdrawMapper.updateByIdAndStatus(withdraw.getId(), BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus(),
+                    new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus()));
+        }
     }
 
-    private BrokerageWithdrawDO validateBrokerageWithdrawExists(Integer id) {
+    private Long createPayTransfer(BrokerageWithdrawDO withdraw) {
+        // 1.1 获取微信 openid
+        SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(
+                UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType());
+        // TODO @luchi:这里,需要校验非空。如果空的话,要有业务异常哈;
+        // 1.2 构建请求
+        PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO()
+                .setAppKey(tradeOrderProperties.getPayAppKey())
+                .setChannelCode("wx_lite").setType(PayTransferTypeEnum.WX_BALANCE.getType())
+                .setMerchantTransferId(withdraw.getId().toString())
+                .setPrice(withdraw.getPrice())
+                .setSubject("佣金提现")
+                .setOpenid(socialUser.getOpenid()).setUserIp(getClientIP());
+        // 2. 发起请求
+        return payTransferApi.createTransfer(payTransferCreateReqDTO);
+    }
+
+    private BrokerageWithdrawDO validateBrokerageWithdrawExists(Long id) {
         BrokerageWithdrawDO withdraw = brokerageWithdrawMapper.selectById(id);
         if (withdraw == null) {
             throw exception(BROKERAGE_WITHDRAW_NOT_EXISTS);
@@ -111,7 +157,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
     }
 
     @Override
-    public BrokerageWithdrawDO getBrokerageWithdraw(Integer id) {
+    public BrokerageWithdrawDO getBrokerageWithdraw(Long id) {
         return brokerageWithdrawMapper.selectById(id);
     }
 
@@ -141,15 +187,6 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
         return withdraw.getId();
     }
 
-    @Override
-    public List<BrokerageWithdrawSummaryRespBO> getWithdrawSummaryListByUserId(Collection<Long> userIds,
-                                                                               BrokerageWithdrawStatusEnum status) {
-        if (CollUtil.isEmpty(userIds)) {
-            return Collections.emptyList();
-        }
-        return brokerageWithdrawMapper.selectCountAndSumPriceByUserIdAndStatus(userIds, status.getStatus());
-    }
-
     /**
      * 计算提现手续费
      *
@@ -157,7 +194,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
      * @param percent       手续费百分比
      * @return 提现手续费
      */
-    Integer calculateFeePrice(Integer withdrawPrice, Integer percent) {
+    private Integer calculateFeePrice(Integer withdrawPrice, Integer percent) {
         Integer feePrice = 0;
         if (percent != null && percent > 0) {
             feePrice = MoneyUtils.calculateRatePrice(withdrawPrice, Double.valueOf(percent));
@@ -171,11 +208,42 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
      * @param withdrawPrice 提现金额
      * @return 分销配置
      */
-    TradeConfigDO validateWithdrawPrice(Integer withdrawPrice) {
+    private TradeConfigDO validateWithdrawPrice(Integer withdrawPrice) {
         TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig();
         if (tradeConfig.getBrokerageWithdrawMinPrice() != null && withdrawPrice < tradeConfig.getBrokerageWithdrawMinPrice()) {
             throw exception(BROKERAGE_WITHDRAW_MIN_PRICE, MoneyUtils.fenToYuanStr(tradeConfig.getBrokerageWithdrawMinPrice()));
         }
         return tradeConfig;
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateBrokerageWithdrawTransferred(Long id, Long payTransferId) {
+        BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id);
+        PayTransferRespDTO transfer = payTransferApi.getTransfer(payTransferId);
+        // TODO @luchi:建议参考支付那,即使成功的情况下,也要各种校验;金额是否匹配、转账单号是否匹配、是否重复调用;
+        if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) {
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus());
+            // TODO @luchi:发送站内信
+        } else if (PayTransferStatusEnum.isPendingStatus(transfer.getStatus())) {
+            // TODO @luchi:这里,是不是不用更新哈?
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus());
+        } else {
+            withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus());
+            // 3.2 驳回时需要退还用户佣金
+            brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
+                    String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());
+        }
+        brokerageWithdrawMapper.updateById(withdraw);
+    }
+
+    @Override
+    public List<BrokerageWithdrawSummaryRespBO> getWithdrawSummaryListByUserId(Collection<Long> userIds,
+                                                                               BrokerageWithdrawStatusEnum status) {
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        return brokerageWithdrawMapper.selectCountAndSumPriceByUserIdAndStatus(userIds, status.getStatus());
+    }
+
 }

+ 13 - 7
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryPickUpStoreService.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.module.trade.service.delivery;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO;
-import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO;
-import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO;
+import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.*;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO;
 
 import javax.validation.Valid;
@@ -55,6 +53,14 @@ public interface DeliveryPickUpStoreService {
      */
     List<DeliveryPickUpStoreDO> getDeliveryPickUpStoreList(Collection<Long> ids);
 
+    /**
+     * 获得指定状态的自提门店列表
+     *
+     * @param status 状态
+     * @return 自提门店列表
+     */
+    List<DeliveryPickUpStoreDO> getDeliveryPickUpStoreListByStatus(Integer status);
+
     /**
      * 获得自提门店分页
      *
@@ -64,10 +70,10 @@ public interface DeliveryPickUpStoreService {
     PageResult<DeliveryPickUpStoreDO> getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO);
 
     /**
-     * 获得指定状态的自提门店列表
+     * 绑定自提店员
      *
-     * @param status 状态
-     * @return 自提门店列表
+     * @param bindReqVO 绑定数据
      */
-    List<DeliveryPickUpStoreDO> getDeliveryPickUpStoreListByStatus(Integer status);
+    void bindDeliveryPickUpStore(DeliveryPickUpBindReqVO bindReqVO);
+
 }

+ 23 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java

@@ -1,16 +1,19 @@
 package cn.iocoder.yudao.module.trade.service.delivery;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpBindReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO;
 import cn.iocoder.yudao.module.trade.convert.delivery.DeliveryPickUpStoreConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.delivery.DeliveryPickUpStoreMapper;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 
@@ -29,6 +32,9 @@ public class DeliveryPickUpStoreServiceImpl implements DeliveryPickUpStoreServic
     @Resource
     private DeliveryPickUpStoreMapper deliveryPickUpStoreMapper;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     public Long createDeliveryPickUpStore(DeliveryPickUpStoreCreateReqVO createReqVO) {
         // 插入
@@ -56,7 +62,8 @@ public class DeliveryPickUpStoreServiceImpl implements DeliveryPickUpStoreServic
     }
 
     private void validateDeliveryPickUpStoreExists(Long id) {
-        if (deliveryPickUpStoreMapper.selectById(id) == null) {
+        DeliveryPickUpStoreDO deliveryPickUpStore = deliveryPickUpStoreMapper.selectById(id);
+        if (deliveryPickUpStore == null) {
             throw exception(PICK_UP_STORE_NOT_EXISTS);
         }
     }
@@ -71,14 +78,26 @@ public class DeliveryPickUpStoreServiceImpl implements DeliveryPickUpStoreServic
         return deliveryPickUpStoreMapper.selectBatchIds(ids);
     }
 
+    @Override
+    public List<DeliveryPickUpStoreDO> getDeliveryPickUpStoreListByStatus(Integer status) {
+        return deliveryPickUpStoreMapper.selectListByStatus(status);
+    }
+
     @Override
     public PageResult<DeliveryPickUpStoreDO> getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO) {
         return deliveryPickUpStoreMapper.selectPage(pageReqVO);
     }
 
     @Override
-    public List<DeliveryPickUpStoreDO> getDeliveryPickUpStoreListByStatus(Integer status) {
-        return deliveryPickUpStoreMapper.selectListByStatus(status);
+    public void bindDeliveryPickUpStore(DeliveryPickUpBindReqVO bindReqVO) {
+        // 1.1 校验门店存在
+        validateDeliveryPickUpStoreExists(bindReqVO.getId());
+        // 1.2 校验用户存在
+        adminUserApi.validateUserList(bindReqVO.getVerifyUserIds());
+
+        // 2. 更新
+        DeliveryPickUpStoreDO updateObj = BeanUtils.toBean(bindReqVO, DeliveryPickUpStoreDO.class);
+        deliveryPickUpStoreMapper.updateById(updateObj);
     }
 
 }

+ 4 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java

@@ -129,16 +129,18 @@ public interface TradeOrderUpdateService {
     /**
      * 【管理员】核销订单
      *
+     * @param userId 管理员编号
      * @param id 订单编号
      */
-    void pickUpOrderByAdmin(Long id);
+    void pickUpOrderByAdmin(Long userId, Long id);
 
     /**
      * 【管理员】核销订单
      *
+     * @param userId 管理员编号
      * @param pickUpVerifyCode 自提核销码
      */
-    void pickUpOrderByAdmin(String pickUpVerifyCode);
+    void pickUpOrderByAdmin(Long userId, String pickUpVerifyCode);
 
     /**
      * 【管理员】根据自提核销码,查询订单

+ 15 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderI
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.cart.CartDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
+import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
@@ -48,6 +49,7 @@ import cn.iocoder.yudao.module.trade.framework.order.core.annotations.TradeOrder
 import cn.iocoder.yudao.module.trade.framework.order.core.utils.TradeOrderLogUtils;
 import cn.iocoder.yudao.module.trade.service.cart.CartService;
 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
+import cn.iocoder.yudao.module.trade.service.delivery.DeliveryPickUpStoreService;
 import cn.iocoder.yudao.module.trade.service.message.TradeMessageService;
 import cn.iocoder.yudao.module.trade.service.message.bo.TradeOrderMessageWhenDeliveryOrderReqBO;
 import cn.iocoder.yudao.module.trade.service.order.handler.TradeOrderHandler;
@@ -104,6 +106,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private DeliveryExpressService deliveryExpressService;
     @Resource
     private TradeMessageService tradeMessageService;
+    @Resource
+    private DeliveryPickUpStoreService pickUpStoreService;
 
     @Resource
     private PayOrderApi payOrderApi;
@@ -718,14 +722,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
 
     @Override
     @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_PICK_UP_RECEIVE)
-    public void pickUpOrderByAdmin(Long id) {
-        getSelf().pickUpOrder(tradeOrderMapper.selectById(id));
+    public void pickUpOrderByAdmin(Long userId, Long id) {
+        getSelf().pickUpOrder(userId, tradeOrderMapper.selectById(id));
     }
 
     @Override
     @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_PICK_UP_RECEIVE)
-    public void pickUpOrderByAdmin(String pickUpVerifyCode) {
-        getSelf().pickUpOrder(tradeOrderMapper.selectOneByPickUpVerifyCode(pickUpVerifyCode));
+    public void pickUpOrderByAdmin(Long userId, String pickUpVerifyCode) {
+        getSelf().pickUpOrder(userId, tradeOrderMapper.selectOneByPickUpVerifyCode(pickUpVerifyCode));
     }
 
     @Override
@@ -734,13 +738,19 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     }
 
     @Transactional(rollbackFor = Exception.class)
-    public void pickUpOrder(TradeOrderDO order) {
+    public void pickUpOrder(Long userId, TradeOrderDO order) {
         if (order == null) {
             throw exception(ORDER_NOT_FOUND);
         }
         if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) {
             throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP);
         }
+        DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId());
+        if (deliveryPickUpStore == null
+            || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) {
+            throw exception(ORDER_PICK_UP_FAIL_NOT_VERIFY_USER);
+        }
+
         receiveOrder0(order);
     }
 

+ 12 - 13
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImplTest.java

@@ -7,14 +7,13 @@ import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.Brok
 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
 import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Validator;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
-import javax.annotation.Resource;
-import javax.validation.Validator;
-
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
@@ -104,15 +103,15 @@ public class BrokerageWithdrawServiceImplTest extends BaseDbUnitTest {
 
     @Test
     public void testCalculateFeePrice() {
-        Integer withdrawPrice = 100;
-        // 测试手续费比例未设置
-        Integer percent = null;
-        assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
-        // 测试手续费给为0
-        percent = 0;
-        assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
-        // 测试手续费
-        percent = 1;
-        assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 1);
+        //Integer withdrawPrice = 100;
+        //// 测试手续费比例未设置
+        //Integer percent = null;
+        //assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
+        //// 测试手续费给为0
+        //percent = 0;
+        //assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
+        //// 测试手续费
+        //percent = 1;
+        //assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 1);
     }
 }

+ 2 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java

@@ -13,6 +13,7 @@ import javax.validation.constraints.NotNull;
 @Data
 public class PayTransferNotifyReqDTO {
 
+    // TODO 芋艿:要不要改成 orderId 待定;
     /**
      * 商户转账单号
      */
@@ -24,4 +25,5 @@ public class PayTransferNotifyReqDTO {
      */
     @NotNull(message = "转账订单编号不能为空")
     private Long payTransferId;
+
 }

+ 10 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApi.java

@@ -2,7 +2,8 @@ package cn.iocoder.yudao.module.pay.api.transfer;
 
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 
-import javax.validation.Valid;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
+import jakarta.validation.Valid;
 
 /**
  * 转账单 API 接口
@@ -19,4 +20,12 @@ public interface PayTransferApi {
      */
     Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
 
+    /**
+     * 获得转账单
+     *
+     * @param id 转账单编号
+     * @return 转账单
+     */
+    PayTransferRespDTO getTransfer(Long id);
+
 }

+ 31 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferRespDTO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.pay.api.transfer.dto;
+
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import lombok.Data;
+
+@Data
+public class PayTransferRespDTO {
+
+    /**
+     * 编号
+     */
+    private Long id;
+
+    /**
+     * 转账单号
+     */
+    private String no;
+
+    /**
+     * 转账金额,单位:分
+     */
+    private Integer price;
+
+    /**
+     * 转账状态
+     *
+     * 枚举 {@link PayTransferStatusEnum}
+     */
+    private Integer status;
+
+}

+ 19 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApi.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.pay.api.wallet;
+
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
+
+/**
+ * 钱包 API 接口
+ *
+ * @author liurulin
+ */
+public interface PayWalletApi {
+
+    /**
+     * 添加钱包余额
+     *
+     * @param reqDTO 增加余额请求
+     */
+    void addWalletBalance(PayWalletAddBalanceReqDTO reqDTO);
+
+}

+ 49 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.pay.api.wallet.dto;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 钱包余额增加 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayWalletAddBalanceReqDTO {
+
+    /**
+     * 用户编号
+     *
+     * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     * 关联 {@link  UserTypeEnum}
+     */
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+
+    /**
+     * 关联业务分类
+     */
+    @NotNull(message = "关联业务分类不能为空")
+    private Integer bizType;
+    /**
+     * 关联业务编号
+     */
+    @NotNull(message = "关联业务编号不能为空")
+    private String bizId;
+
+    /**
+     * 交易金额,单位分
+     *
+     * 正值表示余额增加,负值表示余额减少
+     */
+    @NotNull(message = "交易金额不能为空")
+    private Integer price;
+
+}

+ 7 - 1
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java

@@ -19,7 +19,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     RECHARGE_REFUND(2, "充值退款"),
     PAYMENT(3, "支付"),
     PAYMENT_REFUND(4, "支付退款"),
-    UPDATE_BALANCE(5, "更新余额");
+    UPDATE_BALANCE(5, "更新余额"),
+    BROKERAGE_WITHDRAW(6, "分佣提现");
 
     /**
      * 业务分类
@@ -36,4 +37,9 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     public int[] array() {
          return ARRAYS;
     }
+
+    public static PayWalletBizTypeEnum valueOf(Integer type) {
+        return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null);
+    }
+
 }

+ 10 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/PayTransferApiImpl.java

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.module.pay.api.transfer;
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-
 /**
  * 转账单 API 实现类
  *
@@ -24,4 +26,10 @@ public class PayTransferApiImpl implements PayTransferApi {
         return payTransferService.createTransfer(reqDTO);
     }
 
+    @Override
+    public PayTransferRespDTO getTransfer(Long id) {
+        PayTransferDO transfer = payTransferService.getTransfer(id);
+        return BeanUtils.toBean(transfer, PayTransferRespDTO.class);
+    }
+
 }

+ 33 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/PayWalletApiImpl.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.pay.api.wallet;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
+import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+/**
+ * 钱包 API 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class PayWalletApiImpl implements PayWalletApi {
+
+    @Resource
+    private PayWalletService payWalletService;
+
+    @Override
+    public void addWalletBalance(PayWalletAddBalanceReqDTO reqDTO) {
+        // 创建或获取钱包
+        PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), reqDTO.getUserType());
+        Assert.notNull(wallet, "钱包({}/{})不存在", reqDTO.getUserId(), reqDTO.getUserType());
+
+        // 增加余额
+        PayWalletBizTypeEnum bizType = PayWalletBizTypeEnum.valueOf(reqDTO.getBizType());
+        payWalletService.addWalletBalance(wallet.getId(), reqDTO.getBizId(), bizType, reqDTO.getPrice());
+    }
+
+}

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java

@@ -40,4 +40,8 @@ public class PayAppBaseVO {
     @URL(message = "退款结果的回调地址必须为 URL 格式")
     private String refundNotifyUrl;
 
+    @Schema(description = "转账结果的回调地址", example = "http://127.0.0.1:48080/transfer-callback")
+    @URL(message = "转账结果的回调地址必须为 URL 格式")
+    private String transferNotifyUrl;
+
 }

+ 28 - 4
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java

@@ -6,6 +6,7 @@ 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.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
@@ -18,6 +19,7 @@ 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.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -49,6 +51,8 @@ public class PayNotifyController {
     @Resource
     private PayRefundService refundService;
     @Resource
+    private PayTransferService payTransferService;
+    @Resource
     private PayNotifyService notifyService;
     @Resource
     private PayAppService appService;
@@ -65,7 +69,7 @@ public class PayNotifyController {
         // 1. 校验支付渠道是否存在
         PayClient payClient = channelService.getPayClient(channelId);
         if (payClient == null) {
-            log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
+            log.error("[notifyOrder][渠道编号({}) 找不到对应的支付客户端]", channelId);
             throw exception(CHANNEL_NOT_FOUND);
         }
 
@@ -79,13 +83,13 @@ public class PayNotifyController {
     @Operation(summary = "支付渠道的统一【退款】回调")
     @PermitAll
     public String notifyRefund(@PathVariable("channelId") Long channelId,
-                              @RequestParam(required = false) Map<String, String> params,
-                              @RequestBody(required = false) String body) {
+                               @RequestParam(required = false) Map<String, String> params,
+                               @RequestBody(required = false) String body) {
         log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
         // 1. 校验支付渠道是否存在
         PayClient payClient = channelService.getPayClient(channelId);
         if (payClient == null) {
-            log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
+            log.error("[notifyRefund][渠道编号({}) 找不到对应的支付客户端]", channelId);
             throw exception(CHANNEL_NOT_FOUND);
         }
 
@@ -95,6 +99,26 @@ public class PayNotifyController {
         return "success";
     }
 
+    @PostMapping(value = "/transfer/{channelId}")
+    @Operation(summary = "支付渠道的统一【转账】回调")
+    @PermitAll
+    public String notifyTransfer(@PathVariable("channelId") Long channelId,
+                                 @RequestParam(required = false) Map<String, String> params,
+                                 @RequestBody(required = false) String body) {
+        log.info("[notifyTransfer][channelId({}) 回调数据({}/{})]", channelId, params, body);
+        // 1. 校验支付渠道是否存在
+        PayClient payClient = channelService.getPayClient(channelId);
+        if (payClient == null) {
+            log.error("[notifyTransfer][渠道编号({}) 找不到对应的支付客户端]", channelId);
+            throw exception(CHANNEL_NOT_FOUND);
+        }
+
+        // 2. 解析通知数据
+        PayTransferRespDTO notify = payClient.parseTransferNotify(params, body);
+        payTransferService.notifyTransfer(channelId, notify);
+        return "success";
+    }
+
     @GetMapping("/get-detail")
     @Operation(summary = "获得回调通知的明细")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")

+ 2 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java

@@ -24,7 +24,8 @@ public interface PayTransferConvert {
 
     PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo);
 
-    PayTransferRespVO  convert(PayTransferDO bean);
+    PayTransferRespVO convert(PayTransferDO bean);
 
     PageResult<PayTransferPageItemRespVO> convertPage(PageResult<PayTransferDO> pageResult);
+
 }

+ 7 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java

@@ -23,10 +23,6 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
                     PayTransferDO::getMerchantTransferId, merchantTransferId);
     }
 
-    default PayTransferDO selectByNo(String no){
-        return selectOne(PayTransferDO::getNo, no);
-    }
-
     default PageResult<PayTransferDO> selectPage(PayTransferPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<PayTransferDO>()
                 .eqIfPresent(PayTransferDO::getNo, reqVO.getNo())
@@ -41,9 +37,15 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
                 .orderByDesc(PayTransferDO::getId));
     }
 
-    default List<PayTransferDO> selectListByStatus(Integer status){
+    default List<PayTransferDO> selectListByStatus(Integer status) {
         return selectList(PayTransferDO::getStatus, status);
     }
+
+    default PayTransferDO selectByAppIdAndNo(Long appId, String no) {
+        return selectOne(PayTransferDO::getAppId, appId,
+                PayTransferDO::getNo, no);
+    }
+
 }
 
 

+ 13 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java

@@ -68,6 +68,19 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
         return update(null, lambdaUpdateWrapper);
     }
 
+    /**
+     * 增加余额的时候,更新钱包
+     *
+     * @param id 钱包 id
+     * @param price 钱包金额
+     */
+    default void updateWhenAdd(Long id, Integer price) {
+        LambdaUpdateWrapper<PayWalletDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<PayWalletDO>()
+             .setSql(" balance = balance + " + price)
+             .eq(PayWalletDO::getId, id);
+        update(null, lambdaUpdateWrapper);
+    }
+
     /**
      * 冻结钱包部分余额
      *
@@ -114,7 +127,6 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
         return update(null, lambdaUpdateWrapper);
     }
 
-
 }
 
 

+ 9 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java

@@ -39,6 +39,15 @@ public class PayProperties {
     @URL(message = "支付回调地址的格式必须是 URL")
     private String refundNotifyUrl;
 
+    /**
+     * 转账回调地址
+     *
+     * 实际上,对应的 PayNotifyController 的 notifyTransfer 方法的 URL
+     *
+     * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 transferNotifyUrl 地址 => 业务的 PayAppDO.transferNotifyUrl 地址
+     */
+    private String transferNotifyUrl;
+
     /**
      * 支付订单 no 的前缀
      */

+ 5 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java

@@ -177,6 +177,11 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new IllegalStateException(String.format("支付退款单[%s] 状态不正确", outRefundNo));
     }
 
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     @Override
     public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
         throw new UnsupportedOperationException("待实现");

+ 10 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.pay.service.transfer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
@@ -54,4 +55,13 @@ public interface PayTransferService {
      * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
      */
     int syncTransfer();
+
+    /**
+     * 渠道的转账通知
+     *
+     * @param channelId  渠道编号
+     * @param notify     通知
+     */
+    void notifyTransfer(Long channelId, PayTransferRespDTO notify);
+
 }

+ 21 - 9
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java

@@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
 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.transfer.PayTransferStatusEnum;
+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;
@@ -50,6 +51,9 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private static final String TRANSFER_NO_PREFIX = "T";
 
+    @Resource
+    private PayProperties payProperties;
+
     @Resource
     private PayTransferMapper transferMapper;
     @Resource
@@ -96,13 +100,15 @@ public class PayTransferServiceImpl implements PayTransferService {
             transfer = INSTANCE.convert(reqDTO)
                     .setChannelId(channel.getId())
                     .setNo(no).setStatus(WAITING.getStatus())
-                    .setNotifyUrl(payApp.getTransferNotifyUrl());
+                    .setNotifyUrl(payApp.getTransferNotifyUrl())
+                    .setAppId(channel.getAppId());
             transferMapper.insert(transfer);
         }
         try {
             // 3. 调用三方渠道发起转账
             PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
                     .setOutTransferNo(transfer.getNo());
+            transferUnifiedReq.setNotifyUrl(genChannelTransferNotifyUrl(channel));
             PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
             // 4. 通知转账结果
             getSelf().notifyTransfer(channel, unifiedTransferResp);
@@ -116,6 +122,16 @@ public class PayTransferServiceImpl implements PayTransferService {
         return transfer.getId();
     }
 
+    /**
+     * 根据支付渠道的编码,生成支付渠道的回调地址
+     *
+     * @param channel 支付渠道
+     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
+     */
+    private String genChannelTransferNotifyUrl(PayChannelDO channel) {
+        return payProperties.getTransferNotifyUrl() + "/" + channel.getId();
+    }
+
     private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
         PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
         if (transfer != null) {
@@ -154,7 +170,7 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -172,16 +188,12 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
         }
         log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
-
-        // 3. 插入转账通知记录
-        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
-                transfer.getId());
     }
 
 
     private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -210,7 +222,7 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
-        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
@@ -283,7 +295,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         }
     }
 
-    private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
+    public void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
         // 校验渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(channelId);
         // 通知转账结果给对应的业务

+ 5 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java

@@ -187,7 +187,7 @@ public class PayWalletServiceImpl implements PayWalletService {
 
         // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
         return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
-            // 2. 更新钱包金额
+            // 3. 更新钱包金额
             switch (bizType) {
                 case PAYMENT_REFUND: { // 退款更新
                     walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
@@ -198,15 +198,15 @@ public class PayWalletServiceImpl implements PayWalletService {
                     break;
                 }
                 case UPDATE_BALANCE: // 更新余额
-                    walletMapper.updateWhenRecharge(payWallet.getId(), price);
+                case BROKERAGE_WITHDRAW: // 分佣提现
+                    walletMapper.updateWhenAdd(payWallet.getId(), price);
                     break;
                 default: {
-                    // TODO 其它类型待实现
-                    throw new UnsupportedOperationException("待实现");
+                    throw new UnsupportedOperationException("待实现:" + bizType);
                 }
             }
 
-            // 3. 生成钱包流水
+            // 4. 生成钱包流水
             WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
                     .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
                     .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());

+ 1 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -71,6 +71,7 @@
             <artifactId>yudao-spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+
     </dependencies>
 
 </project>

+ 12 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -79,6 +79,8 @@ public interface PayClient {
      */
     PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
 
+    // ============ 转账相关 ==========
+
     /**
      * 调用渠道,进行转账
      *
@@ -95,4 +97,14 @@ public interface PayClient {
      * @return 转账信息
      */
     PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
+
+    /**
+     * 解析 transfer 回调数据
+     *
+     * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
+     * @param body HTTP 回调接口的 request body
+     * @return 转账信息
+     */
+    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
+
 }

+ 14 - 4
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java

@@ -5,10 +5,12 @@ import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 import lombok.Data;
 import org.hibernate.validator.constraints.Length;
 
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import org.hibernate.validator.constraints.URL;
+
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
@@ -75,4 +77,12 @@ public class PayTransferUnifiedReqDTO {
      * 支付渠道的额外参数
      */
     private Map<String, String> channelExtras;
+
+    /**
+     * 转账结果的 notify 回调地址
+     */
+    @NotEmpty(message = "转账结果的回调地址不能为空")
+    @URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
+    private String notifyUrl;
+
 }

+ 129 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/WxPayTransferPartnerNotifyV3Result.java

@@ -0,0 +1,129 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
+
+import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+// TODO @luchi:这个可以复用 wxjava 里的类么?
+@NoArgsConstructor
+public class WxPayTransferPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayTransferPartnerNotifyV3Result.TransferNotifyResult> {
+
+    private static final long serialVersionUID = -1L;
+
+    /**
+     * 源数据
+     */
+    private OriginNotifyResponse rawData;
+
+    /**
+     * 解密后的数据
+     */
+    private TransferNotifyResult result;
+
+    @Override
+    public void setRawData(OriginNotifyResponse rawData) {
+        this.rawData = rawData;
+    }
+
+    @Override
+    public void setResult(TransferNotifyResult data) {
+        this.result = data;
+    }
+
+    public TransferNotifyResult getResult() {
+        return result;
+    }
+
+    public OriginNotifyResponse getRawData() {
+        return rawData;
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class TransferNotifyResult implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        /*********************** 公共字段 ********************
+
+        /**
+         * 商家批次单号
+         */
+        @SerializedName(value = "out_batch_no")
+        protected String outBatchNo;
+
+        /**
+         * 微信批次单号
+         */
+        @SerializedName(value = "batch_id")
+        protected String batchId;
+
+        /**
+         * 批次状态
+         */
+        @SerializedName(value = "batch_status")
+        protected String batchStatus;
+
+        /**
+         * 批次总笔数
+         */
+        @SerializedName(value = "total_num")
+        protected Integer totalNum;
+
+        /**
+         * 批次总金额
+         */
+        @SerializedName(value = "total_amount")
+        protected Integer totalAmount;
+
+        /**
+         * 批次更新时间
+         */
+        @SerializedName(value = "update_time")
+        private String updateTime;
+
+        /*********************** FINISHED ********************
+
+        /**
+         * 转账成功金额
+         */
+        @SerializedName(value = "success_amount")
+        protected Integer successAmount;
+
+        /**
+         * 转账成功笔数
+         */
+        @SerializedName(value = "success_num")
+        protected Integer successNum;
+
+        /**
+         * 转账失败金额
+         */
+        @SerializedName(value = "fail_amount")
+        protected Integer failAmount;
+
+        /**
+         * 转账失败笔数
+         */
+        @SerializedName(value = "fail_num")
+        protected Integer failNum;
+
+        /*********************** CLOSED ********************
+
+        /**
+         * 商户号
+         */
+        @SerializedName(value = "mchid")
+        protected String mchId;
+
+        /**
+         * 批次关闭原因
+         */
+        @SerializedName(value = "close_reason")
+        protected String closeReason;
+
+    }
+}

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

@@ -219,6 +219,22 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body) {
+        try {
+            return doParseTransferNotify(params, body);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]",
+                    getId(), params, body, ex);
+            throw buildPayException(ex);
+        }
+    }
+
+    protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body)
+            throws Throwable;
+
     @Override
     public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
         try {

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

@@ -325,6 +325,12 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
     }
 
+    // TODO @chihuo:这里是不是也要实现,支付宝的。
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     protected String formatAmount(Integer amount) {

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 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.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -56,4 +57,5 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java

@@ -5,6 +5,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.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -82,4 +83,5 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, "",
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 2 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.Method;
 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.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -66,4 +67,5 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
         return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
                 reqDTO.getOutTradeNo(), response);
     }
+
 }

+ 5 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java

@@ -57,6 +57,11 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
                 outRefundNo, MOCK_RESP_SUCCESS_DATA);
     }
 
+    @Override
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+        throw new UnsupportedOperationException("未实现");
+    }
+
     @Override
     protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
         throw new UnsupportedOperationException("模拟支付无退款回调");

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

@@ -14,6 +14,7 @@ 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.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPartnerNotifyV3Result;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
@@ -23,6 +24,10 @@ import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
 import com.github.binarywang.wxpay.bean.request.*;
 import com.github.binarywang.wxpay.bean.result.*;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
+import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
@@ -31,6 +36,8 @@ import lombok.extern.slf4j.Slf4j;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -349,6 +356,33 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
     }
 
+    @Override
+    public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws WxPayException {
+        switch (config.getApiVersion()) {
+            case API_VERSION_V3:
+                return parseTransferNotifyV3(body);
+            case API_VERSION_V2:
+                throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
+            default:
+                throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+        }
+    }
+
+    private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
+        // 1. 解析回调
+        // TODO @luchi:这个可以复用 wxjava 里的类么?
+        WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
+        WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
+        // 2. 构建结果
+        if (Objects.equals("FINISHED", result.getBatchStatus())) {
+            if (result.getFailNum() <= 0) {
+                return PayTransferRespDTO.successOf(result.getBatchId(), parseDateV3(result.getUpdateTime()),
+                        result.getOutBatchNo(), response);
+            }
+        }
+        return PayTransferRespDTO.closedOf(result.getBatchStatus(), result.getCloseReason(), result.getOutBatchNo(), response);
+    }
+
     @Override
     protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
         try {
@@ -427,13 +461,54 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     }
 
     @Override
-    protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
-       throw new UnsupportedOperationException("待实现");
+    protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
+        // 1. 构建 TransferBatchesRequest 请求
+        List<TransferBatchesRequest.TransferDetail> transferDetailList = Collections.singletonList(
+                TransferBatchesRequest.TransferDetail.newBuilder()
+                        .outDetailNo(reqDTO.getOutTransferNo())
+                        .transferAmount(reqDTO.getPrice())
+                        .transferRemark(reqDTO.getSubject())
+                        .openid(reqDTO.getOpenid())
+                        .build());
+        // TODO @luchi:能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest,这样更简洁一点。
+        TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
+                .appid(this.config.getAppId())
+                .outBatchNo(reqDTO.getOutTransferNo())
+                .batchName(reqDTO.getSubject())
+                .batchRemark(reqDTO.getSubject())
+                .totalAmount(reqDTO.getPrice())
+                .totalNum(transferDetailList.size())
+                .transferDetailList(transferDetailList).build()
+                .setNotifyUrl(reqDTO.getNotifyUrl());
+        // 2.1 执行请求
+        TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
+        // 2.2 创建返回结果
+        return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
     }
 
     @Override
-    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
-        throw new UnsupportedOperationException("待实现");
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws WxPayException {
+        QueryTransferBatchesRequest request = QueryTransferBatchesRequest.newBuilder()
+                .outBatchNo(outTradeNo).needQueryDetail(true).offset(0).limit(20).detailStatus("ALL")
+                .build();
+        QueryTransferBatchesResult response = client.getTransferService().transferBatchesOutBatchNo(request);
+        QueryTransferBatchesResult.TransferBatch transferBatch = response.getTransferBatch();
+        if (Objects.equals("FINISHED", transferBatch.getBatchStatus())) {
+            // 明细中全部成功则成功,任一失败则失败
+            if (response.getTransferDetailList().stream().allMatch(detail -> Objects.equals("SUCCESS", detail.getDetailStatus()))) {
+                return PayTransferRespDTO.successOf(transferBatch.getBatchId(), parseDateV3(transferBatch.getUpdateTime()),
+                        transferBatch.getOutBatchNo(), response);
+            }
+            if (response.getTransferDetailList().stream().anyMatch(detail -> Objects.equals("FAIL", detail.getDetailStatus()))) {
+                return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
+                        transferBatch.getOutBatchNo(), response);
+            }
+        }
+        if (Objects.equals("CLOSED", transferBatch.getBatchStatus())) {
+            return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
+                    transferBatch.getOutBatchNo(), response);
+        }
+        return PayTransferRespDTO.dealingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
     }
 
     // ========== 各种工具方法 ==========

+ 118 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/com/github/binarywang/wxpay/bean/transfer/TransferBatchesRequest.java

@@ -0,0 +1,118 @@
+package com.github.binarywang.wxpay.bean.transfer;
+
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 发起商家转账API参数
+ *
+ * @author zhongjun
+ * created on  2022/6/17
+ **/
+@Data
+@Builder(builderMethodName = "newBuilder")
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransferBatchesRequest implements Serializable {
+    private static final long serialVersionUID = -2175582517588397426L;
+
+    /**
+     * 直连商户的appid
+     */
+    @SerializedName("appid")
+    private String appid;
+
+    /**
+     * 商家批次单号
+     */
+    @SerializedName("out_batch_no")
+    private String outBatchNo;
+
+    /**
+     * 批次名称
+     */
+    @SerializedName("batch_name")
+    private String batchName;
+
+    /**
+     * 批次备注
+     */
+    @SerializedName("batch_remark")
+    private String batchRemark;
+
+    /**
+     * 转账总金额
+     */
+    @SerializedName("total_amount")
+    private Integer totalAmount;
+
+    /**
+     * 转账总笔数
+     */
+    @SerializedName("total_num")
+    private Integer totalNum;
+
+    /**
+     * 转账明细列表
+     */
+    @SpecEncrypt
+    @SerializedName("transfer_detail_list")
+    private List<TransferDetail> transferDetailList;
+
+    /**
+     * 转账场景ID
+     */
+    @SerializedName("transfer_scene_id")
+    private String transferSceneId;
+
+    /**
+     * 通知地址 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。
+     */
+    @SerializedName("notify_url")
+    private String notifyUrl;
+
+    @Data
+    @Builder(builderMethodName = "newBuilder")
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TransferDetail {
+
+        /**
+         * 商家明细单号
+         */
+        @SerializedName("out_detail_no")
+        private String outDetailNo;
+
+        /**
+         * 转账金额
+         */
+        @SerializedName("transfer_amount")
+        private Integer transferAmount;
+
+        /**
+         * 转账备注
+         */
+        @SerializedName("transfer_remark")
+        private String transferRemark;
+
+        /**
+         * 用户在直连商户应用下的用户标示
+         */
+        @SerializedName("openid")
+        private String openid;
+
+        /**
+         * 收款用户姓名
+         */
+        @SpecEncrypt
+        @SerializedName("user_name")
+        private String userName;
+    }
+}

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserPageReqVO.java

@@ -35,4 +35,7 @@ public class UserPageReqVO extends PageParam {
     @Schema(description = "部门编号,同时筛选子部门", example = "1024")
     private Long deptId;
 
+    @Schema(description = "角色编号", example = "1024")
+    private Long roleId;
+
 }

+ 2 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/AdminUserMapper.java

@@ -25,13 +25,14 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
         return selectOne(AdminUserDO::getMobile, mobile);
     }
 
-    default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds) {
+    default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds, Collection<Long> userIds) {
         return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
                 .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
                 .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
                 .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
                 .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
                 .inIfPresent(AdminUserDO::getDeptId, deptIds)
+                .inIfPresent(AdminUserDO::getId, userIds)
                 .orderByDesc(AdminUserDO::getId));
     }
 

+ 7 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java

@@ -46,8 +46,7 @@ import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*;
 
@@ -272,7 +271,12 @@ public class AdminUserServiceImpl implements AdminUserService {
 
     @Override
     public PageResult<AdminUserDO> getUserPage(UserPageReqVO reqVO) {
-        return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()));
+        // 如果有角色编号,查询角色对应的用户编号
+        Set<Long> userIds = reqVO.getRoleId() != null ?
+                permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId())) : null;
+
+        // 分页查询
+        return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds);
     }
 
     @Override

+ 2 - 1
yudao-server/src/main/resources/application-dev.yaml

@@ -167,7 +167,8 @@ yudao:
   pay:
     order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
     refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
-  demo: true # 开启演示模式
+    transfer-notify-url: https://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
+  demo: false # 开启演示模式
   tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
 
 justauth:

+ 1 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -219,6 +219,7 @@ yudao:
   pay:
     order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
     refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
+    transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
   access-log: # 访问日志的配置项
     enable: false
   demo: false # 关闭演示模式