From 98aae5ed1a176283f937834b9074eb1f755a5b0f Mon Sep 17 00:00:00 2001
From: dongshanshan <dongshanshan@zjgtsj.wecom.work>
Date: Tue, 31 Oct 2023 16:51:56 +0800
Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |   7 ++
 .../favorite/ProductFavoriteController.java   | 104 ++++++++++++++++++
 .../favorite/vo/ProductFavoriteBaseVO.java    |  21 ++++
 .../vo/ProductFavoriteBatchReqVO.java         |  21 ++++
 .../favorite/vo/ProductFavoritePageReqVO.java |  35 ++++++
 .../favorite/vo/ProductFavoriteReqVO.java     |  17 +++
 .../favorite/vo/ProductFavoriteRespVO.java    |  22 ++++
 .../favorite/ProductFavoriteConvert.java      |  19 ++++
 .../favorite/ProductFavoriteDetailDO.java     |  16 +++
 .../mysql/favorite/ProductFavoriteMapper.java |  36 ++++++
 .../favorite/ProductFavoriteService.java      |  10 +-
 .../favorite/ProductFavoriteServiceImpl.java  |   8 +-
 12 files changed, 314 insertions(+), 2 deletions(-)
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index d70c21626..2654bec52 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.toolkit.Db;
 import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -132,4 +133,10 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         Db.saveOrUpdateBatch(collection);
     }
 
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, @Param("resultTypeClass_Eg1sG") Class<DTO> var2, @Param("ew") MPJBaseJoin<T> queryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, var2, queryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
new file mode 100644
index 000000000..e59c10827
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteBatchReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
+import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 商品收藏")
+@RestController
+@RequestMapping("/product/favorite")
+@Validated
+public class ProductFavoriteController {
+
+    @Resource
+    private ProductFavoriteService productFavoriteService;
+
+    @PostMapping("/create")
+    @Operation(summary = "添加单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
+    public CommonResult<Long> createFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
+    }
+
+    @PostMapping("/create-list")
+    @Operation(summary = "添加多个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
+    public CommonResult<Boolean> createFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
+        // todo @jason:待实现;如果有已经收藏的,不用报错,忽略即可;
+        return success(Boolean.TRUE);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "取消单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
+    public CommonResult<Boolean> deleteFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        productFavoriteService.deleteFavorite(reqVO.getUserId(), reqVO.getSpuId());
+        return success(Boolean.TRUE);
+    }
+
+    @DeleteMapping("/delete-list")
+    @Operation(summary = "取消单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
+    public CommonResult<Boolean> deleteFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
+        // todo @jason:待实现
+//        productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId());
+        return success(Boolean.TRUE);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商品收藏分页")
+    @PreAuthorize("@ss.hasPermission('product:favorite:query')")
+    public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
+        PageResult<ProductFavoriteDetailDO> favoritePage = productFavoriteService.getFavoritePageByFilter(pageVO);
+        if (CollUtil.isEmpty(favoritePage.getList())) {
+            return success(PageResult.empty());
+        }
+
+        // 得到商品 spu 信息
+        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList());
+
+        // 转换 VO 结果
+        PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
+        pageResult.setList(favorites);
+
+        return success(pageResult);
+    }
+
+    @PostMapping(value = "/exits")
+    @Operation(summary = "检查是否收藏过商品")
+    @PreAuthenticated
+    public CommonResult<Boolean> isFavoriteExists(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
+        return success(favorite != null);
+    }
+
+    @GetMapping(value = "/get-count")
+    @Operation(summary = "获得商品收藏数量")
+    @Parameter(name = "userId", description = "用户编号", required = true)
+    @PreAuthenticated
+    public CommonResult<Long> getFavoriteCount(@RequestParam("userId") Long userId) {
+        return success(productFavoriteService.getFavoriteCount(userId));
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
new file mode 100644
index 000000000..72b2613d5
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商品收藏 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ProductFavoriteBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5036")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
new file mode 100644
index 000000000..d779ff3a8
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Schema(description = "管理后台 - 商品收藏的批量 Request VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteBatchReqVO extends ProductFavoriteBaseVO{
+
+    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
+    @NotNull(message = "商品 SPU 编号数组不能为空")
+    private List<Long> spuIds;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
new file mode 100644
index 000000000..37f8cecc3
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductFavoritePageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "5036")
+    private Long userId;
+
+    @Schema(description = "商品 SPU 编号", example = "32734")
+    private Long spuId;
+
+    @Schema(description = "收藏时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "商品名称", example = "5036")
+    private String name;
+
+    @Schema(description = "关键字", example = "5036")
+    private String keyword;
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
new file mode 100644
index 000000000..3c2222643
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商品收藏的单个 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteReqVO extends  ProductFavoriteBaseVO {
+
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32734")
+    @NotNull(message = "商品 SPU 编号不能为空")
+    private Long spuId;
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
new file mode 100644
index 000000000..255fc631b
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商品收藏 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteRespVO  extends ProductSpuRespVO {
+
+    @Schema(description = "userId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    private Long userId;
+
+    @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    private Long spuId;
+
+    @Schema(description = "收藏状态", example = "1")
+    private Integer favoriteStatus;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index b15afacb2..22ffb8577 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.product.convert.favorite;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -34,4 +37,20 @@ public interface ProductFavoriteConvert {
         return resultList;
     }
 
+    @Mapping(target = "id", source = "favorite.id")
+    @Mapping(target = "userId", source = "favorite.userId")
+    @Mapping(target = "spuId", source = "favorite.spuId")
+    @Mapping(target = "createTime", source = "favorite.createTime")
+    @Mapping(target = "favoriteStatus", constant = "1")
+    ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
+
+    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDetailDO> favorites) {
+        List<ProductFavoriteRespVO> resultList = new ArrayList<>(favorites.size());
+        for (ProductFavoriteDetailDO favorite : favorites) {
+            resultList.add(convert2admin(favorite.getSpuDO(), favorite));
+        }
+        return resultList;
+    }
+
+    PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDetailDO> page);
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
new file mode 100644
index 000000000..ba4d5b557
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.product.dal.dataobject.favorite;
+
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
+import lombok.Data;
+
+/**
+ * 商品收藏 DO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ProductFavoriteDetailDO extends ProductFavoriteDO {
+
+    ProductSpuDO spuDO;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index 54d9d2dd6..08a5c3063 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -2,9 +2,15 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -21,6 +27,36 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
                 .orderByDesc(ProductFavoriteDO::getId));
     }
 
+    default PageResult<ProductFavoriteDetailDO> selectPageByUserAndFields(ProductFavoritePageReqVO reqVO) {
+        MPJLambdaWrapper<ProductFavoriteDO> wrapper =  new MPJLambdaWrapper<ProductFavoriteDO>()
+                .selectAll(ProductFavoriteDO.class)
+                .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
+                .selectAssociation(ProductSpuDO.class, ProductFavoriteDetailDO::getSpuDO);
+        if(StringUtils.isNotEmpty(reqVO.getName())){
+            wrapper.likeRight(ProductSpuDO::getName, reqVO.getName());
+        }
+        if(StringUtils.isNotEmpty(reqVO.getName()) && StringUtils.isNotEmpty(reqVO.getKeyword())){
+            wrapper.or();
+        }
+        if(StringUtils.isNotEmpty(reqVO.getKeyword())){
+            wrapper.likeRight(ProductSpuDO::getKeyword, reqVO.getKeyword());
+        }
+
+        if(reqVO.getCreateTime() != null){
+            if (reqVO.getCreateTime()[0] != null && reqVO.getCreateTime()[1] != null) {
+                wrapper.between(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0], reqVO.getCreateTime()[1]);
+            }
+            if (reqVO.getCreateTime()[0] != null) {
+                wrapper.ge(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0]);
+            }
+            if (reqVO.getCreateTime()[1] != null) {
+                wrapper.le(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[1]);
+            }
+        }
+
+        return selectJoinPage(reqVO, ProductFavoriteDetailDO.class, wrapper);
+    }
+
     default Long selectCountByUserId(Long userId) {
         return selectCount(ProductFavoriteDO::getUserId, userId);
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 00aeddb8a..87c9854a9 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.module.product.service.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 
 import javax.validation.Valid;
 
@@ -37,6 +39,13 @@ public interface ProductFavoriteService {
      */
     PageResult<ProductFavoriteDO> getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO);
 
+    /**
+     * 分页查询用户收藏列表
+     *
+     * @param reqVO 请求 vo
+     */
+    PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid ProductFavoritePageReqVO reqVO);
+
     /**
      * 获取收藏过商品
      *
@@ -52,5 +61,4 @@ public interface ProductFavoriteService {
      * @return 数量
      */
     Long getFavoriteCount(Long userId);
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 983cbf83c..0f8d30ec0 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.product.service.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.mysql.favorite.ProductFavoriteMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -33,7 +35,6 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         if (favorite != null) {
             throw exception(FAVORITE_EXISTS);
         }
-
         ProductFavoriteDO entity = ProductFavoriteConvert.INSTANCE.convert(userId, spuId);
         productFavoriteMapper.insert(entity);
         return entity.getId();
@@ -54,6 +55,11 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         return productFavoriteMapper.selectPageByUserAndType(userId, reqVO);
     }
 
+    @Override
+    public PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid  ProductFavoritePageReqVO reqVO) {
+        return productFavoriteMapper.selectPageByUserAndFields(reqVO);
+    }
+
     @Override
     public ProductFavoriteDO getFavorite(Long userId, Long spuId) {
         return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId);

From 989d7c44d0b4c60e64bb49e59132b471f294fc99 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 4 Nov 2023 22:52:41 +0800
Subject: [PATCH 02/11] =?UTF-8?q?code=20review=20=E5=BA=97=E9=93=BA?=
 =?UTF-8?q?=E8=A3=85=E4=BF=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                       |  6 +-
 .../diy/vo/template/DiyTemplateBaseVO.java    |  4 +-
 .../dal/dataobject/diy/DiyPageDO.java         |  2 +
 .../dal/dataobject/diy/DiyTemplateDO.java     |  3 +
 .../service/diy/DiyPageServiceImpl.java       |  8 ++-
 .../service/diy/DiyTemplateServiceImpl.java   |  7 ++-
 yudao-server/pom.xml                          | 62 +++++++++----------
 7 files changed, 54 insertions(+), 38 deletions(-)

diff --git a/pom.xml b/pom.xml
index dae40f3dc..7f7c140bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
         <!-- 各种 module 拓展 -->
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
-<!--        <module>yudao-module-member</module>-->
+        <module>yudao-module-member</module>
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
-<!--        <module>yudao-module-pay</module>-->
-<!--        <module>yudao-module-mall</module>-->
+        <module>yudao-module-pay</module>
+        <module>yudao-module-mall</module>
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
index 7959b6c26..b79c7231f 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import javax.validation.constraints.NotNull;
+import javax.validation.constraints.NotEmpty;
 import java.util.List;
 
 /**
@@ -14,7 +14,7 @@ import java.util.List;
 public class DiyTemplateBaseVO {
 
     @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "默认主题")
-    @NotNull(message = "模板名称不能为空")
+    @NotEmpty(message = "模板名称不能为空")
     private String name;
 
     @Schema(description = "备注", example = "默认主题")
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
index e5e3f4208..7e1044104 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
@@ -32,6 +32,8 @@ public class DiyPageDO extends BaseDO {
     private Long id;
     /**
      * 装修模板编号
+     *
+     * 关联 {@link DiyTemplateDO#getId()}
      */
     private Long templateId;
     /**
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
index a50fd4dde..684a6f9cb 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
@@ -14,6 +14,9 @@ import java.util.List;
 /**
  * 装修模板 DO
  *
+ * 1. 新建一个模版,下面可以包含多个 {@link DiyPageDO} 页面,例如说首页、我的
+ * 2. 如果需要使用某个模版,则将 {@link #used} 设置为 true,表示已使用,有且仅有一个
+ *
  * @author owen
  */
 @TableName(value = "promotion_diy_template", autoResultMap = true)
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
index d82b9b8ed..69f96ad8f 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
@@ -42,7 +42,6 @@ public class DiyPageServiceImpl implements DiyPageService {
         DiyPageDO diyPage = DiyPageConvert.INSTANCE.convert(createReqVO);
         diyPage.setProperty("{}");
         diyPageMapper.insert(diyPage);
-        // 返回
         return diyPage.getId();
     }
 
@@ -57,6 +56,13 @@ public class DiyPageServiceImpl implements DiyPageService {
         diyPageMapper.updateById(updateObj);
     }
 
+    /**
+     * 校验 Page 页面,在一个 template 模版下的名字是唯一的
+     *
+     * @param id Page 编号
+     * @param templateId 模版编号
+     * @param name Page 名字
+     */
     void validateNameUnique(Long id, Long templateId, String name) {
         if (templateId != null || StrUtil.isBlank(name)) {
             return;
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
index 41025c5d6..530b31b94 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
@@ -32,9 +32,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
 
     @Resource
     private DiyTemplateMapper diyTemplateMapper;
+
     @Resource
     private DiyPageService diyPageService;
 
+    // TODO @疯狂:事务;
     @Override
     public Long createDiyTemplate(DiyTemplateCreateReqVO createReqVO) {
         // 校验名称唯一
@@ -120,9 +122,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
     }
 
     @Override
+    // TODO @疯狂:事务;
     public void useDiyTemplate(Long id) {
         // 校验存在
         validateDiyTemplateExists(id);
+        // TODO @疯狂:要不已使用的情况,抛个业务异常?
         // 已使用的更新为未使用
         DiyTemplateDO used = diyTemplateMapper.selectByUsed(true);
         if (used != null) {
@@ -136,8 +140,8 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
         this.updateUsed(id, true, LocalDateTime.now());
     }
 
-    @Transactional(rollbackFor = Exception.class)
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void updateDiyTemplateProperty(DiyTemplatePropertyUpdateRequestVO updateReqVO) {
         // 校验存在
         validateDiyTemplateExists(updateReqVO.getId());
@@ -151,6 +155,7 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
         return diyTemplateMapper.selectByUsed(true);
     }
 
+    // TODO @疯狂:挪到 useDiyTemplate 下面,改名 updateTemplateUsed 会不会好点哈;
     /**
      * 更新模板是否使用
      *
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 87a961778..4463bd7fb 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -37,11 +37,11 @@
         </dependency>
 
         <!-- 会员中心。默认注释,保证编译速度 -->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-member-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-member-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 数据报表。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -56,11 +56,11 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
         <!-- 支付服务。默认注释,保证编译速度 -->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-pay-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-pay-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 微信公众号模块。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -69,27 +69,27 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
-<!--         商城相关模块。默认注释,保证编译速度-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-promotion-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-product-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-trade-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-statistics-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <!-- 商城相关模块。默认注释,保证编译速度-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-promotion-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-product-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-trade-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-statistics-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- spring boot 配置所需依赖 -->
         <dependency>

From 09d45e639348aaf944da43b6628127a0dd05e165 Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Mon, 6 Nov 2023 11:33:08 +0800
Subject: [PATCH 03/11] =?UTF-8?q?=E8=BD=AC=E8=B4=A6=20-=20=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E8=BD=AC=E8=B4=A6=E9=80=9A=E7=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/pay_wallet.sql                      | 10 +++-
 .../notify/dto/PayTransferNotifyReqDTO.java   | 27 +++++++++++
 .../module/pay/enums/ErrorCodeConstants.java  |  7 ++-
 .../pay/enums/notify/PayNotifyTypeEnum.java   |  1 +
 .../admin/demo/PayDemoOrderController.java    |  4 +-
 .../admin/demo/PayDemoTransferController.java | 15 +++++-
 .../{ => order}/PayDemoOrderCreateReqVO.java  |  5 +-
 .../vo/{ => order}/PayDemoOrderRespVO.java    |  2 +-
 .../pay/convert/demo/PayDemoOrderConvert.java |  4 +-
 .../pay/dal/dataobject/app/PayAppDO.java      |  5 ++
 .../dataobject/notify/PayNotifyTaskDO.java    |  4 ++
 .../pay/service/demo/PayDemoOrderService.java |  2 +-
 .../service/demo/PayDemoOrderServiceImpl.java |  2 +-
 .../service/demo/PayDemoTransferService.java  |  8 ++++
 .../demo/PayDemoTransferServiceImpl.java      | 46 +++++++++++++++++++
 .../service/notify/PayNotifyServiceImpl.java  | 13 ++++++
 .../transfer/PayTransferServiceImpl.java      | 36 +++++++--------
 17 files changed, 160 insertions(+), 31 deletions(-)
 create mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
 rename yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/{ => order}/PayDemoOrderCreateReqVO.java (76%)
 rename yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/{ => order}/PayDemoOrderRespVO.java (96%)

diff --git a/sql/mysql/pay_wallet.sql b/sql/mysql/pay_wallet.sql
index 291428ffa..9153948bf 100644
--- a/sql/mysql/pay_wallet.sql
+++ b/sql/mysql/pay_wallet.sql
@@ -245,4 +245,12 @@ INSERT INTO system_menu(
 VALUES (
            '转账订单', '', 2, 3, 1117,
            'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
-       );
\ No newline at end of file
+       );
+
+-- 转账通知脚本
+
+ALTER TABLE `pay_app`
+    ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
+ALTER TABLE  `pay_notify_task`
+    MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
+    ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;
\ No newline at end of file
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
new file mode 100644
index 000000000..14ba64463
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.pay.api.notify.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 转账单的通知 Request DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayTransferNotifyReqDTO {
+
+    /**
+     * 商户转账单号
+     */
+    @NotEmpty(message = "商户转账单号不能为空")
+    private String merchantTransferId;
+
+    /**
+     * 转账订单编号
+     */
+    @NotNull(message = "转账订单编号不能为空")
+    private Long payTransferId;
+}
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
index 95835a4b6..8fc38e61c 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
@@ -65,12 +65,13 @@ public interface ErrorCodeConstants {
 
     // ========== 转账模块 1-007-009-000 ==========
     ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
-    ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账交易单不存在");
+    ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
     ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
     ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
     ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
+
     // ========== 示例订单 1-007-900-000 ==========
     ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");
     ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态");
@@ -84,4 +85,8 @@ public interface ErrorCodeConstants {
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配");
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配");
 
+    // ========== 示例转账订单 1-007-901-001 ==========
+    ErrorCode DEMO_TRANSFER_NOT_FOUND = new ErrorCode(1_007_901_001, "示例转账单不存在");
+    ErrorCode DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_002, "转账失败,转账单编号不匹配");
+    ErrorCode DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "转账失败,转账单金额不匹配");
 }
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
index 8c259d93c..873e015c6 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
@@ -14,6 +14,7 @@ public enum PayNotifyTypeEnum {
 
     ORDER(1, "支付单"),
     REFUND(2, "退款单"),
+    TRANSFER(3, "转账单")
     ;
 
     /**
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
index 1e3a61eec..60c04c290 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
@@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
index f6c7b31c0..ca01a1edb 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
@@ -14,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import javax.annotation.security.PermitAll;
 import javax.validation.Valid;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -33,9 +36,19 @@ public class PayDemoTransferController {
     }
 
     @GetMapping("/page")
-    @Operation(summary = "获得示例订单分页")
+    @Operation(summary = "获得示例转账订单分页")
     public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
         PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
         return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
     }
+
+    @PostMapping("/update-status")
+    @Operation(summary = "更新示例转账订单的转账状态") // 由 pay-module 转账服务,进行回调
+    @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现
+    @OperateLog(enable = false) // 禁用操作日志,因为没有操作人
+    public CommonResult<Boolean> updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
+        demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
+                notifyReqDTO.getPayTransferId());
+        return success(true);
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
similarity index 76%
rename from yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java
rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
index 9960ada48..6c82a0ab6 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
@@ -1,9 +1,8 @@
-package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
+package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
 
-import lombok.*;
 import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
 
-import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 示例订单创建 Request VO")
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
similarity index 96%
rename from yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java
rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
index 3404844dc..cb305631d 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
+package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
index 313e5d266..8fca99791 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.pay.convert.demo;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
index 977eff93a..8f3490fc7 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
@@ -54,4 +54,9 @@ public class PayAppDO extends BaseDO {
      */
     private String refundNotifyUrl;
 
+    /**
+     * 转账结果的回调地址
+     */
+    private String transferNotifyUrl;
+
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
index 181a32802..7bfabad3f 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
@@ -66,6 +66,10 @@ public class PayNotifyTaskDO extends TenantBaseDO {
      * 商户订单编号
      */
     private String merchantOrderId;
+    /**
+     * 商户转账单编号
+     */
+    private String merchantTransferId;
     /**
      * 通知状态
      *
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
index e6822e626..cf870253f 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 
 import javax.validation.Valid;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
index 7e1090248..57ff80637 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
index 9116dcd9a..fb01ec43d 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
@@ -28,4 +28,12 @@ public interface PayDemoTransferService {
      * @param pageVO 分页查询参数
      */
     PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO);
+
+    /**
+     * 更新转账业务示例订单的转账状态
+     *
+     * @param id 编号
+     * @param payTransferId 转账单编号
+     */
+    void updateDemoTransferStatus(Long id, Long payTransferId);
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
index e892e4446..8de98da1e 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
@@ -1,18 +1,25 @@
 package cn.iocoder.yudao.module.pay.service.demo;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import javax.validation.Validator;
+import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
 
 /**
@@ -33,6 +40,8 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
     @Resource
     private PayDemoTransferMapper demoTransferMapper;
     @Resource
+    private PayTransferService payTransferService;
+    @Resource
     private Validator validator;
 
     @Override
@@ -50,4 +59,41 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
     public PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO) {
         return demoTransferMapper.selectPage(pageVO);
     }
+
+    @Override
+    public void updateDemoTransferStatus(Long id, Long payTransferId) {
+        PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId);
+        // 更新示例订单状态
+        if (payTransfer != null) {
+            demoTransferMapper.updateById(new PayDemoTransferDO().setId(id)
+                    .setPayTransferId(payTransferId)
+                    .setPayChannelCode(payTransfer.getChannelCode())
+                    .setTransferStatus(payTransfer.getStatus())
+                    .setTransferTime(payTransfer.getSuccessTime()));
+        }
+    }
+
+    private PayTransferDO validateDemoTransferStatusCanUpdate(Long id, Long payTransferId) {
+        PayDemoTransferDO demoTransfer = demoTransferMapper.selectById(id);
+        if (demoTransfer == null) {
+            throw exception(DEMO_TRANSFER_NOT_FOUND);
+        }
+        if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus())
+                || PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) {
+            // 无需更新返回 null
+            return null;
+        }
+        PayTransferDO transfer = payTransferService.getTransfer(payTransferId);
+        if (transfer == null) {
+            throw exception(PAY_TRANSFER_NOT_FOUND);
+        }
+        if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) {
+            throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH);
+        }
+        if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) {
+            throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR);
+        }
+        // TODO 校验账号
+        return transfer;
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
index f6b17c565..1d356cf49 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
@@ -13,11 +13,13 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
@@ -25,6 +27,7 @@ import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 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 com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
@@ -73,6 +76,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
     @Resource
     @Lazy // 循环依赖,避免报错
     private PayRefundService refundService;
+    @Resource
+    @Lazy // 循环依赖,避免报错
+    private PayTransferService transferService;
 
     @Resource
     private PayNotifyTaskMapper notifyTaskMapper;
@@ -100,6 +106,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
             PayRefundDO refundDO = refundService.getRefund(task.getDataId());
             task.setAppId(refundDO.getAppId())
                     .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
+            PayTransferDO transfer = transferService.getTransfer(task.getDataId());
+            task.setAppId(transfer.getAppId()).setMerchantTransferId(transfer.getMerchantTransferId())
+                    .setNotifyUrl(transfer.getNotifyUrl());
         }
 
         // 执行插入
@@ -214,6 +224,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
         } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
             request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
                     .payRefundId(task.getDataId()).build();
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
+            request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId())
+                    .setPayTransferId(task.getDataId());
         } else {
             throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
         }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
index 014a7aae7..27b9807bd 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
@@ -12,12 +12,15 @@ import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespE
 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;
+import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 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.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
+import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -51,6 +54,8 @@ public class PayTransferServiceImpl implements PayTransferService {
     @Resource
     private PayChannelService channelService;
     @Resource
+    private PayNotifyService notifyService;
+    @Resource
     private PayNoRedisDAO noRedisDAO;
     @Resource
     private Validator validator;
@@ -73,7 +78,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         // 1.1 校验转账单是否可以提交
         validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
         // 1.2 校验 App
-        appService.validPayApp(reqDTO.getAppId());
+        PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
         // 1.3 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
         PayClient client = channelService.getPayClient(channel.getId());
@@ -86,7 +91,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         PayTransferDO transfer = INSTANCE.convert(reqDTO)
                 .setChannelId(channel.getId())
                 .setNo(no).setStatus(WAITING.getStatus())
-                .setNotifyUrl("http://127.0.0.1:48080/admin-api/pay/todo"); // TODO 需要加个transfer Notify url
+                .setNotifyUrl(payApp.getTransferNotifyUrl());
         transferMapper.insert(transfer);
         PayTransferRespDTO unifiedTransferResp = null;
         try {
@@ -145,22 +150,13 @@ public class PayTransferServiceImpl implements PayTransferService {
     }
 
     private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
-        // 1. 更新 PayTransferDO 转账成功
-        Boolean transferred = updateTransferSuccess(channel, notify);
-        if (transferred) {
-            return;
-        }
-        // 2. TODO 插入转账通知记录
-    }
-
-    private Boolean updateTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
         PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
         if (isSuccess(transfer.getStatus())) { // 如果已成功,直接返回,不用重复更新
-            return Boolean.TRUE;
+            return;
         }
         if (!isPendingStatus(transfer.getStatus())) {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
@@ -176,10 +172,13 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
         log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
-        return Boolean.FALSE;
+
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
     }
 
-    private void updateTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
+    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
         PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
         if (transfer == null) {
@@ -192,6 +191,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         if (!isPendingStatus(transfer.getStatus())) {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
+
         // 2.更新
         int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(),
                 CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
@@ -203,11 +203,11 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
         log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
-    }
 
-    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
-        //  更新 PayTransferDO 转账关闭
-        updateTransferClosed(channel, notify);
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
+
     }
 
     /**

From 15313c2992258c248433b73b6d921189ad775d4f Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Tue, 7 Nov 2023 23:11:07 +0800
Subject: [PATCH 04/11] =?UTF-8?q?=E8=BD=AC=E8=B4=A6=20-=20=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E8=BD=AC=E8=B4=A6=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../framework/pay/core/client/PayClient.java  |   9 +
 .../dto/transfer/PayTransferRespDTO.java      |  17 +-
 .../core/client/impl/AbstractPayClient.java   |  22 ++-
 .../impl/alipay/AbstractAlipayPayClient.java  | 108 ++++++++----
 .../core/client/impl/mock/MockPayClient.java  |   6 +
 .../impl/weixin/AbstractWxPayClient.java      |   7 +
 .../transfer/PayTransferStatusRespEnum.java   |   4 +
 .../module/pay/enums/ErrorCodeConstants.java  |   6 +-
 .../enums/transfer/PayTransferStatusEnum.java |   4 +
 .../convert/transfer/PayTransferConvert.java  |   2 +-
 .../dal/mysql/transfer/PayTransferMapper.java |   6 +-
 .../framework/pay/core/WalletPayClient.java   |   6 +
 .../pay/job/transfer/PayTransferSyncJob.java  |  30 ++++
 .../service/transfer/PayTransferService.java  |   6 +
 .../transfer/PayTransferServiceImpl.java      | 164 +++++++++++++-----
 15 files changed, 314 insertions(+), 83 deletions(-)
 create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java

diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
index 18ae017d1..86e3566b2 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
@@ -6,6 +6,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.enums.transfer.PayTransferTypeEnum;
 
 import java.util.Map;
 
@@ -86,4 +87,12 @@ public interface PayClient {
      */
     PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
 
+    /**
+     * 获得转账订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @param type 转账类型
+     * @return 转账信息
+     */
+    PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
 }
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
index da6f22774..0f9b48240 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
@@ -53,11 +53,24 @@ public class PayTransferRespDTO {
     /**
      * 创建【WAITING】状态的转账返回
      */
-    public static PayTransferRespDTO waitingOf(String channelOrderNo,
+    public static PayTransferRespDTO waitingOf(String channelTransferNo,
                                              String outTransferNo, Object rawData) {
         PayTransferRespDTO respDTO = new PayTransferRespDTO();
         respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
-        respDTO.channelTransferNo = channelOrderNo;
+        respDTO.channelTransferNo = channelTransferNo;
+        respDTO.outTransferNo = outTransferNo;
+        respDTO.rawData = rawData;
+        return respDTO;
+    }
+
+    /**
+     * 创建【IN_PROGRESS】状态的转账返回
+     */
+    public static PayTransferRespDTO dealingOf(String channelTransferNo,
+                                               String outTransferNo, Object rawData) {
+        PayTransferRespDTO respDTO = new PayTransferRespDTO();
+        respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+        respDTO.channelTransferNo = channelTransferNo;
         respDTO.outTransferNo = outTransferNo;
         respDTO.rawData = rawData;
         return respDTO;
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
index f06dab22e..82d68b58f 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
@@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 
     @Override
     public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+        validatePayTransferReqDTO(reqDTO);
         PayTransferRespDTO resp;
-        try{
-            validatePayTransferReqDTO(reqDTO);
+        try {
             resp = doUnifiedTransfer(reqDTO);
-        }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
             // 系统异常,则包装成 PayException 异常抛出
@@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        try {
+            return doGetTransfer(outTradeNo, type);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+                    getId(), outTradeNo, type, ex);
+            throw buildPayException(ex);
+        }
+    }
+
     protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
             throws Throwable;
 
+    protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+            throws Throwable;
+
     // ========== 各种工具方法 ==========
 
     private PayException buildPayException(Throwable ex) {
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
index fc9d658ac..4dcf23675 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
 import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.*;
 import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayFundTransUniTransferRequest;
-import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
-import com.alipay.api.request.AlipayTradeQueryRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
-import com.alipay.api.response.AlipayFundTransUniTransferResponse;
-import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
-import com.alipay.api.response.AlipayTradeQueryResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
 import lombok.Getter;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
         // 2.2 解析订单的状态
         Integer status = parseStatus(response.getTradeStatus());
-        Assert.notNull(status,  () -> {
+        Assert.notNull(status, () -> {
             throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
         });
         return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 校验公钥类型 必须使用公钥证书模式
         if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
-            throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
+            throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
         }
         // 1.2 构建 AlipayFundTransUniTransferModel
         AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@@ -238,45 +232,97 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         model.setOutBizNo(reqDTO.getOutTransferNo());
         model.setProductCode("TRANS_ACCOUNT_NO_PWD");    // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
         model.setBizScene("DIRECT_TRANSFER");           // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
-        model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        if (reqDTO.getChannelExtras() != null) {
+            model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        }
+        // ② 个性化的参数
+        Participant payeeInfo = new Participant();
         PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
         switch (transferType) {
             // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
             // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
             case ALIPAY_BALANCE: {
-                // ② 个性化的参数
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
                 payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
                 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
                 model.setPayeeInfo(payeeInfo);
-                // 1.3 构建 AlipayFundTransUniTransferRequest
-                AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
-                request.setBizModel(model);
-                // 执行请求
-                AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
-                // 处理结果
-                if (!response.isSuccess()) {
-                    // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
-                    if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
-                        return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
-                    }
-                    return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
-                            reqDTO.getOutTransferNo(), response);
-                }
-                return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
-                        response.getOutBizNo(), response);
+                break;
             }
             case BANK_CARD: {
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
                 // TODO 待实现
                 throw exception(NOT_IMPLEMENTED);
             }
             default: {
-                throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
+                throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
             }
         }
+        // 1.3 构建 AlipayFundTransUniTransferRequest
+        AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+        request.setBizModel(model);
+        // 执行请求
+        AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
+        // 处理结果
+        if (!response.isSuccess()) {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
+            // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
+            if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    reqDTO.getOutTransferNo(), response);
+        } else {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        reqDTO.getOutTransferNo(), response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING"  处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
+                    response.getOutBizNo(), response);
+        }
+
+    }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
+        // 1.1 构建 AlipayFundTransCommonQueryModel
+        AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
+        model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
+        model.setBizScene("DIRECT_TRANSFER"); //业务场景
+        model.setOutBizNo(outTradeNo);
+        // 1.2 构建 AlipayFundTransCommonQueryRequest
+        AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayFundTransCommonQueryResponse response;
+        if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+            response = client.certificateExecute(request);
+        } else {
+            response = client.execute(request);
+        }
+        // 2.2 处理返回结果
+        if (response.isSuccess()) {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        outTradeNo, response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
+                    response.getOutBizNo(), response);
+        } else {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    outTradeNo, response);
+        }
     }
 
     // ========== 各种工具方法 ==========
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
index 309813697..1ad1ad713 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.time.LocalDateTime;
 import java.util.Map;
@@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
index f4f326a65..bb6feeb04 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 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;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
@@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
        throw new UnsupportedOperationException("待实现");
     }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     static String formatDateV2(LocalDateTime time) {
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
index 63b3a96aa..35ea344da 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
@@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
index 8fc38e61c..8b7a38ecf 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
@@ -66,9 +66,9 @@ public interface ErrorCodeConstants {
     // ========== 转账模块 1-007-009-000 ==========
     ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
     ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
-    ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
-    ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
-    ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
+    ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
+    ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
+    ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
 
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
index 335a470f8..6f2f27c75 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
@@ -40,9 +40,13 @@ public enum PayTransferStatusEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
     public static boolean isWaiting(Integer status) {
         return Objects.equals(status, WAITING.getStatus());
     }
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 
     /**
      * 是否处于待转账或者转账中的状态
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
index 4d5849ddd..4e79548d0 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
@@ -18,7 +18,7 @@ public interface PayTransferConvert {
 
     PayTransferDO convert(PayTransferCreateReqDTO dto);
 
-    PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto);
+    PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
 
     PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);
 
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
index 0f7384526..af4f6debf 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
 
 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.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
                 .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(PayTransferDO::getId));
     }
+
+    default List<PayTransferDO> selectListByStatus(Integer status){
+        return selectList(PayTransferDO::getStatus, status);
+    }
 }
 
 
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
index c1b72ef9e..efd3d0ba1 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
@@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
@@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java
new file mode 100644
index 000000000..191071e10
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.pay.job.transfer;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 转账订单的同步 Job
+ *
+ * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
+ *
+ * @author jason
+ */
+@Component
+public class PayTransferSyncJob implements JobHandler {
+
+    @Resource
+    private PayTransferService transferService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) {
+        int count = transferService.syncTransfer();
+        return StrUtil.format("同步转账订单 {} 个", count);
+    }
+}
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
index 0848dc0ca..9a58cf06a 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
@@ -48,4 +48,10 @@ public interface PayTransferService {
      */
     PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO);
 
+    /**
+     * 同步渠道转账单状态
+     *
+     * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
+     */
+    int syncTransfer();
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
index 27b9807bd..73b726dcd 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
@@ -1,14 +1,16 @@
 package cn.iocoder.yudao.module.pay.service.transfer;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.extra.spring.SpringUtil;
-import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 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.enums.transfer.PayTransferStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 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;
@@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 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.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -27,7 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import javax.validation.Validator;
-import java.util.Objects;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
@@ -75,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     @Override
     public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
-        // 1.1 校验转账单是否可以提交
-        validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
-        // 1.2 校验 App
+        // 1.1 校验 App
         PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
-        // 1.3 校验支付渠道是否有效
+        // 1.2 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
         PayClient client = channelService.getPayClient(channel.getId());
         if (client == null) {
             log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
             throw exception(CHANNEL_NOT_FOUND);
         }
-        // 2.创建转账单
-        String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
-        PayTransferDO transfer = INSTANCE.convert(reqDTO)
-                .setChannelId(channel.getId())
-                .setNo(no).setStatus(WAITING.getStatus())
-                .setNotifyUrl(payApp.getTransferNotifyUrl());
-        transferMapper.insert(transfer);
-        PayTransferRespDTO unifiedTransferResp = null;
+        // 1.3 校验转账单已经发起过转账。
+        PayTransferDO transfer = validateTransferCanCreate(reqDTO);
+
+        if (transfer == null) {
+            // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
+            String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
+            transfer = INSTANCE.convert(reqDTO)
+                    .setChannelId(channel.getId())
+                    .setNo(no).setStatus(WAITING.getStatus())
+                    .setNotifyUrl(payApp.getTransferNotifyUrl());
+            transferMapper.insert(transfer);
+        }
         try {
             // 3. 调用三方渠道发起转账
-            PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO)
-                    .setOutTransferNo(no);
-            unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
-        } catch (ServiceException ex) {
-            // 业务异常.直接返回转账失败的结果
-            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex);
-            unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex);
-        } catch (Throwable e) {
-            // 注意这里仅打印异常,不进行抛出。
-            // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续通过转账回调、或者转账轮询可以拿到。
-            // TODO 需要加转账回调业务接口 和 转账轮询未实现
-            // 最终,在异常的情况下,支付中心会异步回调业务的转账回调接口,提供转账结果
-            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
-        }
-        if (Objects.nonNull(unifiedTransferResp)) {
+            PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
+                    .setOutTransferNo(transfer.getNo());
+            PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
             // 4. 通知转账结果
             getSelf().notifyTransfer(channel, unifiedTransferResp);
+        } catch (Throwable e) {
+            // 注意这里仅打印异常,不进行抛出。
+            // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。
+            // 或者使用相同 no 再次发起转账请求
+            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
         }
+
         return transfer.getId();
     }
 
-    @Override
-    public PayTransferDO getTransfer(Long id) {
-        return transferMapper.selectById(id);
-    }
-
-    @Override
-    public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
-        return transferMapper.selectPage(pageReqVO);
+    private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
+        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
+        if (transfer != null) {
+            // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
+            if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
+                throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
+            }
+            if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
+                throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
+            }
+            if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
+                throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
+            }
+        }
+        // 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等
+        return transfer;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -138,17 +145,40 @@ public class PayTransferServiceImpl implements PayTransferService {
         if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
             notifyTransferClosed(channel, notify);
         }
+        // 转账处理中的回调
+        if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
+            notifyTransferInProgress(channel, notify);
+        }
         // WAITING 状态无需处理
-        // TODO IN_PROGRESS 待处理
     }
 
-    private void validateTransferCanCreate(Long appId, String merchantTransferId) {
-        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId);
-        if (transfer != null) {  // 是否存在
-            throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
+    private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
+        // 1.校验
+        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        if (transfer == null) {
+            throw exception(PAY_TRANSFER_NOT_FOUND);
         }
+        if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
+            return;
+        }
+        if (!isWaiting(transfer.getStatus())) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
+        }
+        // 2.更新
+        int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
+                CollUtil.newArrayList(WAITING.getStatus()),
+                new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
+        if (updateCounts == 0) {
+            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());
@@ -210,6 +240,56 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     }
 
+    @Override
+    public PayTransferDO getTransfer(Long id) {
+        return transferMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
+        return transferMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public int syncTransfer() {
+        List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
+        if (CollUtil.isEmpty(list)) {
+            return 0;
+        }
+        int count = 0;
+        for (PayTransferDO transfer : list) {
+            count += syncTransfer(transfer) ? 1 : 0;
+        }
+        return count;
+    }
+
+    private boolean syncTransfer(PayTransferDO transfer) {
+        try {
+            // 1. 查询转账订单信息
+            PayClient payClient = channelService.getPayClient(transfer.getChannelId());
+            if (payClient == null) {
+                log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId());
+                return false;
+            }
+            PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(),
+                    PayTransferTypeEnum.typeOf(transfer.getType()));
+
+            // 2. 回调转账结果
+            notifyTransfer(transfer.getChannelId(), resp);
+            return true;
+        } catch (Throwable ex) {
+            log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex);
+            return false;
+        }
+    }
+
+    private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
+        // 校验渠道是否有效
+        PayChannelDO channel = channelService.validPayChannel(channelId);
+        // 通知转账结果给对应的业务
+        TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify));
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

From daa7d14b092342b1bd7062873c9c9b7ec79cc2bc Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 14:36:44 +0800
Subject: [PATCH 05/11] =?UTF-8?q?reafactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |  4 +-
 .../favorite/ProductFavoriteController.java   | 41 +++++--------------
 .../favorite/ProductFavoriteConvert.java      | 11 ++---
 .../favorite/ProductFavoriteDetailDO.java     |  1 -
 .../mysql/favorite/ProductFavoriteMapper.java | 33 ++-------------
 .../favorite/ProductFavoriteService.java      |  3 +-
 .../favorite/ProductFavoriteServiceImpl.java  |  5 +--
 7 files changed, 24 insertions(+), 74 deletions(-)

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index 2654bec52..db054e972 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -133,9 +133,9 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         Db.saveOrUpdateBatch(collection);
     }
 
-    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, @Param("resultTypeClass_Eg1sG") Class<DTO> var2, @Param("ew") MPJBaseJoin<T> queryWrapper) {
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
         IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
-        selectJoinPage(mpPage, var2, queryWrapper);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
         // 转换返回
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index e59c10827..9157e8a05 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -4,16 +4,15 @@ import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
-import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteBatchReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
+import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
 import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -24,6 +23,7 @@ import javax.validation.Valid;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 @Tag(name = "管理后台 - 商品收藏")
 @RestController
@@ -34,6 +34,9 @@ public class ProductFavoriteController {
     @Resource
     private ProductFavoriteService productFavoriteService;
 
+    @Resource
+    private ProductSpuService productSpuService;
+
     @PostMapping("/create")
     @Operation(summary = "添加单个商品收藏")
     @PreAuthorize("@ss.hasPermission('product:favorite:create')")
@@ -41,14 +44,6 @@ public class ProductFavoriteController {
         return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
     }
 
-    @PostMapping("/create-list")
-    @Operation(summary = "添加多个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
-    public CommonResult<Boolean> createFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
-        // todo @jason:待实现;如果有已经收藏的,不用报错,忽略即可;
-        return success(Boolean.TRUE);
-    }
-
     @DeleteMapping("/delete")
     @Operation(summary = "取消单个商品收藏")
     @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
@@ -57,26 +52,19 @@ public class ProductFavoriteController {
         return success(Boolean.TRUE);
     }
 
-    @DeleteMapping("/delete-list")
-    @Operation(summary = "取消单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
-    public CommonResult<Boolean> deleteFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
-        // todo @jason:待实现
-//        productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId());
-        return success(Boolean.TRUE);
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
     public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
-        PageResult<ProductFavoriteDetailDO> favoritePage = productFavoriteService.getFavoritePageByFilter(pageVO);
+        PageResult<ProductFavoriteDO> favoritePage = productFavoriteService.getFavoritePage(pageVO);
         if (CollUtil.isEmpty(favoritePage.getList())) {
             return success(PageResult.empty());
         }
 
+        List<ProductSpuDO> list = productSpuService.getSpuList(convertSet(favoritePage.getList(), ProductFavoriteDO::getSpuId));
+
         // 得到商品 spu 信息
-        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList());
+        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList(), list);
 
         // 转换 VO 结果
         PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
@@ -92,13 +80,4 @@ public class ProductFavoriteController {
         ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
         return success(favorite != null);
     }
-
-    @GetMapping(value = "/get-count")
-    @Operation(summary = "获得商品收藏数量")
-    @Parameter(name = "userId", description = "用户编号", required = true)
-    @PreAuthenticated
-    public CommonResult<Long> getFavoriteCount(@RequestParam("userId") Long userId) {
-        return success(productFavoriteService.getFavoriteCount(userId));
-    }
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index 22ffb8577..9adac8c86 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -13,6 +13,7 @@ import org.mapstruct.factory.Mappers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
@@ -41,13 +42,13 @@ public interface ProductFavoriteConvert {
     @Mapping(target = "userId", source = "favorite.userId")
     @Mapping(target = "spuId", source = "favorite.spuId")
     @Mapping(target = "createTime", source = "favorite.createTime")
-    @Mapping(target = "favoriteStatus", constant = "1")
     ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
 
-    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDetailDO> favorites) {
-        List<ProductFavoriteRespVO> resultList = new ArrayList<>(favorites.size());
-        for (ProductFavoriteDetailDO favorite : favorites) {
-            resultList.add(convert2admin(favorite.getSpuDO(), favorite));
+    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDO> favorites, List<ProductSpuDO> spus) {
+        List<ProductFavoriteRespVO> resultList = new ArrayList<>(spus.size());
+        for (ProductFavoriteDO favorite : favorites) {
+            Optional<ProductSpuDO> spu =  spus.stream().filter(e -> e.getId().equals(favorite.getSpuId())).findFirst();
+            resultList.add(convert2admin(spu.get(), favorite));
         }
         return resultList;
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
index ba4d5b557..60e401e11 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
@@ -12,5 +12,4 @@ import lombok.Data;
 public class ProductFavoriteDetailDO extends ProductFavoriteDO {
 
     ProductSpuDO spuDO;
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index 08a5c3063..e116a7c4a 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -5,12 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -27,34 +23,11 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
                 .orderByDesc(ProductFavoriteDO::getId));
     }
 
-    default PageResult<ProductFavoriteDetailDO> selectPageByUserAndFields(ProductFavoritePageReqVO reqVO) {
-        MPJLambdaWrapper<ProductFavoriteDO> wrapper =  new MPJLambdaWrapper<ProductFavoriteDO>()
+    default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
+        return selectPage(reqVO, new MPJLambdaWrapper<ProductFavoriteDO>()
                 .selectAll(ProductFavoriteDO.class)
                 .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
-                .selectAssociation(ProductSpuDO.class, ProductFavoriteDetailDO::getSpuDO);
-        if(StringUtils.isNotEmpty(reqVO.getName())){
-            wrapper.likeRight(ProductSpuDO::getName, reqVO.getName());
-        }
-        if(StringUtils.isNotEmpty(reqVO.getName()) && StringUtils.isNotEmpty(reqVO.getKeyword())){
-            wrapper.or();
-        }
-        if(StringUtils.isNotEmpty(reqVO.getKeyword())){
-            wrapper.likeRight(ProductSpuDO::getKeyword, reqVO.getKeyword());
-        }
-
-        if(reqVO.getCreateTime() != null){
-            if (reqVO.getCreateTime()[0] != null && reqVO.getCreateTime()[1] != null) {
-                wrapper.between(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0], reqVO.getCreateTime()[1]);
-            }
-            if (reqVO.getCreateTime()[0] != null) {
-                wrapper.ge(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0]);
-            }
-            if (reqVO.getCreateTime()[1] != null) {
-                wrapper.le(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[1]);
-            }
-        }
-
-        return selectJoinPage(reqVO, ProductFavoriteDetailDO.class, wrapper);
+                .orderByDesc(ProductFavoriteDO::getCreateTime));
     }
 
     default Long selectCountByUserId(Long userId) {
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 87c9854a9..295af4c94 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 
 import javax.validation.Valid;
 
@@ -44,7 +43,7 @@ public interface ProductFavoriteService {
      *
      * @param reqVO 请求 vo
      */
-    PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid ProductFavoritePageReqVO reqVO);
+    PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO);
 
     /**
      * 获取收藏过商品
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 0f8d30ec0..945201286 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavor
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.mysql.favorite.ProductFavoriteMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -56,8 +55,8 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
     }
 
     @Override
-    public PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid  ProductFavoritePageReqVO reqVO) {
-        return productFavoriteMapper.selectPageByUserAndFields(reqVO);
+    public PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO) {
+        return productFavoriteMapper.selectPageByUserId(reqVO);
     }
 
     @Override

From 5facac733039e924c293ae892d289bd154187e9d Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 14:37:18 +0800
Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../favorite/vo/ProductFavoriteBaseVO.java    |  2 --
 .../vo/ProductFavoriteBatchReqVO.java         | 21 -------------------
 .../favorite/vo/ProductFavoritePageReqVO.java | 18 ----------------
 .../favorite/vo/ProductFavoriteRespVO.java    |  4 ----
 4 files changed, 45 deletions(-)
 delete mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
index 72b2613d5..68b0a0a16 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
@@ -16,6 +16,4 @@ public class ProductFavoriteBaseVO {
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
-
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
deleted file mode 100644
index d779ff3a8..000000000
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.ToString;
-
-import javax.validation.constraints.NotNull;
-import java.util.List;
-
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
-@Schema(description = "管理后台 - 商品收藏的批量 Request VO")
-@Data
-@ToString(callSuper = true)
-public class ProductFavoriteBatchReqVO extends ProductFavoriteBaseVO{
-
-    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
-    @NotNull(message = "商品 SPU 编号数组不能为空")
-    private List<Long> spuIds;
-
-}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
index 37f8cecc3..9c0b33035 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.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
@@ -19,17 +14,4 @@ public class ProductFavoritePageReqVO extends PageParam {
 
     @Schema(description = "用户编号", example = "5036")
     private Long userId;
-
-    @Schema(description = "商品 SPU 编号", example = "32734")
-    private Long spuId;
-
-    @Schema(description = "收藏时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
-    @Schema(description = "商品名称", example = "5036")
-    private String name;
-
-    @Schema(description = "关键字", example = "5036")
-    private String keyword;
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
index 255fc631b..bbf972181 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -15,8 +15,4 @@ public class ProductFavoriteRespVO  extends ProductSpuRespVO {
 
     @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long spuId;
-
-    @Schema(description = "收藏状态", example = "1")
-    private Integer favoriteStatus;
-
 }

From 670c0962a76fbf8e393d347c78ae66b2c4af1ee0 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:03:37 +0800
Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../favorite/ProductFavoriteController.java   | 29 ++-----------------
 1 file changed, 3 insertions(+), 26 deletions(-)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index 9157e8a05..6f66c2062 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.product.controller.admin.favorite;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
@@ -16,7 +14,9 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
@@ -37,21 +37,6 @@ public class ProductFavoriteController {
     @Resource
     private ProductSpuService productSpuService;
 
-    @PostMapping("/create")
-    @Operation(summary = "添加单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
-    public CommonResult<Long> createFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
-    }
-
-    @DeleteMapping("/delete")
-    @Operation(summary = "取消单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
-    public CommonResult<Boolean> deleteFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        productFavoriteService.deleteFavorite(reqVO.getUserId(), reqVO.getSpuId());
-        return success(Boolean.TRUE);
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
@@ -72,12 +57,4 @@ public class ProductFavoriteController {
 
         return success(pageResult);
     }
-
-    @PostMapping(value = "/exits")
-    @Operation(summary = "检查是否收藏过商品")
-    @PreAuthenticated
-    public CommonResult<Boolean> isFavoriteExists(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
-        return success(favorite != null);
-    }
 }

From 92be763c6f35a1735d45e8fae8cce870c8808c10 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:18:28 +0800
Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/product/service/favorite/ProductFavoriteService.java  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 295af4c94..3dd3bd59e 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -60,4 +60,5 @@ public interface ProductFavoriteService {
      * @return 数量
      */
     Long getFavoriteCount(Long userId);
+
 }

From 5a5a42463bbfad1f95a32a6c8ee1c13094f633c3 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:22:00 +0800
Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../product/service/favorite/ProductFavoriteServiceImpl.java     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 945201286..927d030d5 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -34,6 +34,7 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         if (favorite != null) {
             throw exception(FAVORITE_EXISTS);
         }
+
         ProductFavoriteDO entity = ProductFavoriteConvert.INSTANCE.convert(userId, spuId);
         productFavoriteMapper.insert(entity);
         return entity.getId();

From 330380c2d0dd58af81af921e6b781d95395721c4 Mon Sep 17 00:00:00 2001
From: owen <owen@evolsun.com>
Date: Fri, 17 Nov 2023 09:56:52 +0800
Subject: [PATCH 10/11] =?UTF-8?q?=E8=90=A5=E9=94=80=EF=BC=9A=E9=80=82?=
 =?UTF-8?q?=E9=85=8D=E5=95=86=E5=9F=8E=E8=A3=85=E4=BF=AE=E7=BB=84=E4=BB=B6?=
 =?UTF-8?q?=E3=80=90=E5=95=86=E5=93=81=E5=8D=A1=E7=89=87=E3=80=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/spu/vo/ProductSpuDetailRespVO.java  | 14 +------------
 .../admin/spu/vo/ProductSpuRespVO.java        |  2 +-
 .../app/spu/AppProductSpuController.java      | 20 +++++++++++++++++++
 .../app/spu/vo/AppProductSpuPageRespVO.java   |  3 +++
 4 files changed, 25 insertions(+), 14 deletions(-)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
index 1be96632d..336d44467 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
@@ -12,19 +12,7 @@ import java.util.List;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
-
-    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1212")
-    private Long id;
-
-    @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
-    private Integer salesCount;
-
-    @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000")
-    private Integer browseCount;
-
-    @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    private Integer status;
+public class ProductSpuDetailRespVO extends ProductSpuRespVO {
 
     // ========== SKU 相关字段 =========
 
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
index 0148cb2a1..faf8a5572 100755
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
 @ToString(callSuper = true)
 public class ProductSpuRespVO extends ProductSpuBaseVO {
 
-    @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long id;
 
     @Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999")
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
index 8f49e7f74..9d0a1fe9f 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
@@ -30,6 +30,7 @@ import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -75,6 +76,25 @@ public class AppProductSpuController {
         return success(voList);
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得商品 SPU 列表")
+    @Parameters({
+            @Parameter(name = "ids", description = "编号列表", required = true)
+    })
+    public CommonResult<List<AppProductSpuPageRespVO>> getSpuList(@RequestParam("ids") Set<Long> ids) {
+        List<ProductSpuDO> list = productSpuService.getSpuList(ids);
+        if (CollUtil.isEmpty(list)) {
+            return success(Collections.emptyList());
+        }
+
+        // 拼接返回
+        List<AppProductSpuPageRespVO> voList = ProductSpuConvert.INSTANCE.convertListForGetSpuList(list);
+        // 处理 vip 价格
+        MemberLevelRespDTO memberLevel = getMemberLevel();
+        voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
+        return success(voList);
+    }
+
     @GetMapping("/page")
     @Operation(summary = "获得商品 SPU 分页")
     public CommonResult<PageResult<AppProductSpuPageRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
index c4a66afd2..07b0d8e95 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
@@ -15,6 +15,9 @@ public class AppProductSpuPageRespVO {
     @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
+    @Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖简介")
+    private String introduction;
+
     @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED)
     private Long categoryId;
 

From c7a21b2a6447a547c6470a26180cbee6a2bb39bf Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 00:58:12 +0800
Subject: [PATCH 11/11] =?UTF-8?q?mall=EF=BC=9A=E4=BC=98=E5=8C=96=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F=E7=9A=84=E5=88=86=E9=A1=B5=E6=8E=A5?=
 =?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |  1 +
 .../favorite/ProductFavoriteController.java   | 19 +++++----------
 .../favorite/vo/ProductFavoritePageReqVO.java |  1 +
 .../favorite/vo/ProductFavoriteRespVO.java    |  1 +
 .../favorite/ProductFavoriteConvert.java      | 23 ++++++++-----------
 .../favorite/ProductFavoriteDetailDO.java     | 15 ------------
 .../mysql/favorite/ProductFavoriteMapper.java |  9 ++++----
 7 files changed, 23 insertions(+), 46 deletions(-)
 delete mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index db054e972..57e7133fb 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -139,4 +139,5 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         // 转换返回
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index 6f66c2062..721cf19c0 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -41,20 +41,13 @@ public class ProductFavoriteController {
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
     public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
-        PageResult<ProductFavoriteDO> favoritePage = productFavoriteService.getFavoritePage(pageVO);
-        if (CollUtil.isEmpty(favoritePage.getList())) {
+        PageResult<ProductFavoriteDO> pageResult = productFavoriteService.getFavoritePage(pageVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
             return success(PageResult.empty());
         }
-
-        List<ProductSpuDO> list = productSpuService.getSpuList(convertSet(favoritePage.getList(), ProductFavoriteDO::getSpuId));
-
-        // 得到商品 spu 信息
-        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList(), list);
-
-        // 转换 VO 结果
-        PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
-        pageResult.setList(favorites);
-
-        return success(pageResult);
+        // 拼接数据
+        List<ProductSpuDO> spuList = productSpuService.getSpuList(convertSet(pageResult.getList(), ProductFavoriteDO::getSpuId));
+        return success(ProductFavoriteConvert.INSTANCE.convertPage(pageResult, spuList));
     }
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
index 9c0b33035..3d78883ec 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
@@ -14,4 +14,5 @@ public class ProductFavoritePageReqVO extends PageParam {
 
     @Schema(description = "用户编号", example = "5036")
     private Long userId;
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
index bbf972181..3c09aa8fc 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -15,4 +15,5 @@ public class ProductFavoriteRespVO  extends ProductSpuRespVO {
 
     @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long spuId;
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index 9adac8c86..7b419b6a8 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.product.convert.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -13,7 +13,6 @@ import org.mapstruct.factory.Mappers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
@@ -38,20 +37,18 @@ public interface ProductFavoriteConvert {
         return resultList;
     }
 
+    default PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDO> pageResult, List<ProductSpuDO> spuList) {
+        Map<Long, ProductSpuDO> spuMap = convertMap(spuList, ProductSpuDO::getId);
+        List<ProductFavoriteRespVO> voList = CollectionUtils.convertList(pageResult.getList(), favorite -> {
+            ProductSpuDO spu = spuMap.get(favorite.getSpuId());
+            return convert02(spu, favorite);
+        });
+        return new PageResult<>(voList, pageResult.getTotal());
+    }
     @Mapping(target = "id", source = "favorite.id")
     @Mapping(target = "userId", source = "favorite.userId")
     @Mapping(target = "spuId", source = "favorite.spuId")
     @Mapping(target = "createTime", source = "favorite.createTime")
-    ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
+    ProductFavoriteRespVO convert02(ProductSpuDO spu, ProductFavoriteDO favorite);
 
-    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDO> favorites, List<ProductSpuDO> spus) {
-        List<ProductFavoriteRespVO> resultList = new ArrayList<>(spus.size());
-        for (ProductFavoriteDO favorite : favorites) {
-            Optional<ProductSpuDO> spu =  spus.stream().filter(e -> e.getId().equals(favorite.getSpuId())).findFirst();
-            resultList.add(convert2admin(spu.get(), favorite));
-        }
-        return resultList;
-    }
-
-    PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDetailDO> page);
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
deleted file mode 100644
index 60e401e11..000000000
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package cn.iocoder.yudao.module.product.dal.dataobject.favorite;
-
-import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
-import lombok.Data;
-
-/**
- * 商品收藏 DO
- *
- * @author 芋道源码
- */
-@Data
-public class ProductFavoriteDetailDO extends ProductFavoriteDO {
-
-    ProductSpuDO spuDO;
-}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index e116a7c4a..a681d42a7 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
 
 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.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -24,10 +24,9 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
     }
 
     default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
-        return selectPage(reqVO, new MPJLambdaWrapper<ProductFavoriteDO>()
-                .selectAll(ProductFavoriteDO.class)
-                .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
-                .orderByDesc(ProductFavoriteDO::getCreateTime));
+        return selectPage(reqVO, new LambdaQueryWrapperX<ProductFavoriteDO>()
+                .eqIfPresent(ProductFavoriteDO::getUserId, reqVO.getUserId())
+                .orderByDesc(ProductFavoriteDO::getId));
     }
 
     default Long selectCountByUserId(Long userId) {