Browse Source

Merge remote-tracking branch 'yudao/develop' into develop

puhui999 1 year ago
parent
commit
dc695e3cda
29 changed files with 465 additions and 313 deletions
  1. 1 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
  2. 4 5
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  3. 26 11
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
  4. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
  5. 3 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/CrmOperateLogController.java
  6. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java
  7. 44 24
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
  8. 46 33
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
  9. 15 8
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java
  10. 2 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanSaveReqVO.java
  11. 23 13
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
  12. 24 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
  13. 0 44
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java
  14. 0 48
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java
  15. 9 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
  16. 1 5
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
  17. 0 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java
  18. 25 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  19. 10 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
  20. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java
  21. 44 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmReceivablePlanParseFunction.java
  22. 40 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmReceivableReturnTypeParseFunction.java
  23. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
  24. 5 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
  25. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
  26. 40 35
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
  27. 18 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
  28. 55 40
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
  29. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/listener/CrmReceivableResultListener.java

+ 1 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java

@@ -14,5 +14,6 @@ public interface DictTypeConstants {
     String CRM_PRODUCT_UNIT = "crm_product_unit"; // CRM 产品单位
     String CRM_PRODUCT_STATUS = "crm_product_status"; // CRM 产品状态
     String CRM_FOLLOW_UP_TYPE = "crm_follow_up_type"; // CRM 跟进方式
+    String CRM_RECEIVABLE_RETURN_TYPE = "crm_receivable_return_type"; // CRM 回款方式
 
 }

+ 4 - 5
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java

@@ -28,7 +28,6 @@ public interface ErrorCodeConstants {
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
-    ErrorCode CONTACT_BUSINESS_LINK_NOT_EXISTS = new ErrorCode(1_020_003_001, "联系人商机关联不存在");
     ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode(1_020_003_002, "联系人已关联合同,不能删除");
     ErrorCode CONTACT_UPDATE_OWNER_USER_FAIL = new ErrorCode(1_020_003_003, "更新联系人负责人失败");
 
@@ -38,10 +37,13 @@ public interface ErrorCodeConstants {
     ErrorCode RECEIVABLE_DELETE_FAIL = new ErrorCode(1_020_004_002, "删除回款失败,原因: 被回款计划所使用,不允许删除");
     ErrorCode RECEIVABLE_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_004_003, "回款提交审核失败,原因:回款没处在未提交状态");
     ErrorCode RECEIVABLE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_004_004, "更新回款审核状态失败,原因:回款不是审核中状态");
+    ErrorCode RECEIVABLE_NO_EXISTS = new ErrorCode(1_020_004_005, "生成回款序列号重复,请重试");
+    ErrorCode RECEIVABLE_CREATE_FAIL_CONTRACT_NOT_APPROVE = new ErrorCode(1_020_004_006, "创建回款失败,原因:合同不是审核通过状态");
 
     // ========== 回款计划 1-020-005-000 ==========
     ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
-    ErrorCode RECEIVABLE_PLAN_UPDATE_FAIL = new ErrorCode(1_020_006_000, "更想回款计划失败,原因:{}");
+    ErrorCode RECEIVABLE_PLAN_UPDATE_FAIL = new ErrorCode(1_020_006_000, "更想回款计划失败,原因:已经有对应的还款");
+    ErrorCode RECEIVABLE_PLAN_EXISTS_RECEIVABLE = new ErrorCode(1_020_006_001, "回款计划已经有对应的回款,不能使用");
 
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
@@ -64,10 +66,8 @@ public interface ErrorCodeConstants {
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
     ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
-    ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
     ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:转移对象已经是该负责人");
     ErrorCode CRM_PERMISSION_DELETE_FAIL = new ErrorCode(1_020_007_004, "删除数据权限失败,原因:批量删除权限的时候,只能属于同一个 bizId 下");
-    ErrorCode CRM_PERMISSION_DELETE_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_005, "删除数据权限失败,原因:存在负责人");
     ErrorCode CRM_PERMISSION_DELETE_DENIED = new ErrorCode(1_020_007_006, "删除数据权限失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_DELETE_SELF_PERMISSION_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_007, "删除数据权限失败,原因:不能删除负责人");
     ErrorCode CRM_PERMISSION_CREATE_FAIL = new ErrorCode(1_020_007_008, "创建数据权限失败,原因:所加用户已有权限");
@@ -93,7 +93,6 @@ public interface ErrorCodeConstants {
     ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_010_003, "商机状态不存在");
 
     // ========== 客户公海规则设置 1_020_012_000 ==========
-    ErrorCode CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_020_012_000, "客户公海配置不存在或未启用");
     ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_001, "客户限制配置不存在");
 
     // ========== 跟进记录 1_020_013_000 ==========

+ 26 - 11
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 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.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
@@ -24,6 +25,7 @@ import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -39,6 +41,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +70,8 @@ public class CrmContractController {
     private CrmBusinessService businessService;
     @Resource
     private CrmProductService productService;
+    @Resource
+    private CrmReceivableService receivableService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -174,12 +179,6 @@ public class CrmContractController {
         return success(true);
     }
 
-    /**
-     * 构建详细的合同结果
-     *
-     * @param contractList 原始合同信息
-     * @return 细的合同结果
-     */
     private List<CrmContractRespVO> buildContractDetailList(List<CrmContractDO> contractList) {
         if (CollUtil.isEmpty(contractList)) {
             return Collections.emptyList();
@@ -197,6 +196,9 @@ public class CrmContractController {
         // 1.4 获取商机
         Map<Long, CrmBusinessDO> businessMap = businessService.getBusinessMap(
                 convertSet(contractList, CrmContractDO::getBusinessId));
+        // 1.5 获得已回款金额
+        Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
+                convertSet(contractList, CrmContractDO::getId));
         // 2. 拼接数据
         return BeanUtils.toBean(contractList, CrmContractRespVO.class, contractVO -> {
             // 2.1 设置客户信息
@@ -212,6 +214,8 @@ public class CrmContractController {
             findAndThen(contactMap, contractVO.getSignContactId(), contact -> contractVO.setSignContactName(contact.getName()));
             // 2.4 设置商机信息
             findAndThen(businessMap, contractVO.getBusinessId(), business -> contractVO.setBusinessName(business.getName()));
+            // 2.5 设置已回款金额
+            contractVO.setTotalReceivablePrice(receivablePriceMap.getOrDefault(contractVO.getId(), BigDecimal.ZERO));
         });
     }
 
@@ -229,13 +233,24 @@ public class CrmContractController {
         return success(contractService.getRemindContractCount(getLoginUserId()));
     }
 
-    @GetMapping("/list-all-simple-by-customer")
-    @Operation(summary = "获得合同精简列表", description = "只包含有读权限的客户的合同,主要用于前端的下拉选项")
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得合同精简列表", description = "只包含的合同,主要用于前端的下拉选项")
     @Parameter(name = "customerId", description = "客户编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:contract:query')")
-    public CommonResult<List<CrmContractRespVO>> getListAllSimpleByCustomer(@RequestParam("customerId") Long customerId) {
-        PageResult<CrmContractDO> result = contractService.getContractPageByCustomerId(new CrmContractPageReqVO().setCustomerId(customerId));
-        return success(BeanUtils.toBean(result.getList(), CrmContractRespVO.class));
+    public CommonResult<List<CrmContractRespVO>> getContractSimpleList(@RequestParam("customerId") Long customerId) {
+        CrmContractPageReqVO pageReqVO = new CrmContractPageReqVO().setCustomerId(customerId);
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); // 不分页
+        PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageReqVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(Collections.emptyList());
+        }
+        // 拼接数据
+        Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
+                convertSet(pageResult.getList(), CrmContractDO::getId));
+        return success(convertList(pageResult.getList(), contract -> new CrmContractRespVO() // 只返回 id、name 等精简字段
+                .setId(contract.getId()).setName(contract.getName()).setAuditStatus(contract.getAuditStatus())
+                .setTotalPrice(contract.getTotalPrice())
+                .setTotalReceivablePrice(receivablePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))));
     }
 
 }

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java

@@ -88,6 +88,10 @@ public class CrmContractRespVO {
     @ExcelProperty("合同金额")
     private BigDecimal totalPrice;
 
+    @Schema(description = "已回款金额", example = "5617")
+    @ExcelProperty("已回款金额")
+    private BigDecimal totalReceivablePrice;
+
     @Schema(description = "客户签约人编号", example = "18546")
     private Long signContactId;
     @Schema(description = "客户签约人", example = "小豆")

+ 3 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/CrmOperateLogController.java

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogV2RespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogRespVO;
 import cn.iocoder.yudao.module.crm.enums.LogRecordConstants;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
@@ -54,11 +54,11 @@ public class CrmOperateLogController {
     @GetMapping("/page")
     @Operation(summary = "获得操作日志")
     @PreAuthorize("@ss.hasPermission('crm:operate-log:query')")
-    public CommonResult<PageResult<CrmOperateLogV2RespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
+    public CommonResult<PageResult<CrmOperateLogRespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
         OperateLogV2PageReqDTO reqDTO = new OperateLogV2PageReqDTO();
         reqDTO.setPageSize(PAGE_SIZE_NONE); // 默认不分页,需要分页需注释
         reqDTO.setBizType(BIZ_TYPE_MAP.get(pageReqVO.getBizType())).setBizId(pageReqVO.getBizId());
-        return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogV2RespVO.class));
+        return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogRespVO.class));
     }
 
 }

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogV2RespVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java

@@ -6,10 +6,10 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - CRM 跟进 Response VO")
+@Schema(description = "管理后台 - CRM 操作日志 Response VO")
 @Data
 @ExcelIgnoreUnannotated
-public class CrmOperateLogV2RespVO {
+public class CrmOperateLogRespVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;

+ 44 - 24
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java

@@ -4,20 +4,23 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -31,14 +34,15 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -57,6 +61,8 @@ public class CrmReceivableController {
 
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建回款")
@@ -88,7 +94,14 @@ public class CrmReceivableController {
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
     public CommonResult<CrmReceivableRespVO> getReceivable(@RequestParam("id") Long id) {
         CrmReceivableDO receivable = receivableService.getReceivable(id);
-        return success(BeanUtils.toBean(receivable, CrmReceivableRespVO.class));
+        return success(buildReceivableDetail(receivable));
+    }
+
+    private CrmReceivableRespVO buildReceivableDetail(CrmReceivableDO receivable) {
+        if (receivable == null) {
+            return null;
+        }
+        return buildReceivableDetailList(Collections.singletonList(receivable)).get(0);
     }
 
     @GetMapping("/page")
@@ -96,7 +109,7 @@ public class CrmReceivableController {
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePage(@Valid CrmReceivablePageReqVO pageReqVO) {
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(pageReqVO, getLoginUserId());
-        return success(buildReceivableDetailPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -104,7 +117,7 @@ public class CrmReceivableController {
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePageByCustomer(@Valid CrmReceivablePageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePageByCustomerId(pageReqVO);
-        return success(buildReceivableDetailPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/export-excel")
@@ -114,36 +127,43 @@ public class CrmReceivableController {
     public void exportReceivableExcel(@Valid CrmReceivablePageReqVO exportReqVO,
                                       HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
-        PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(exportReqVO, getLoginUserId());
+        List<CrmReceivableDO> list = receivableService.getReceivablePage(exportReqVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "回款.xls", "数据", CrmReceivableRespVO.class,
-                buildReceivableDetailPage(pageResult).getList());
+                buildReceivableDetailList(list));
     }
 
-    /**
-     * 构建详细的回款分页结果
-     *
-     * @param pageResult 简单的回款分页结果
-     * @return 详细的回款分页结果
-     */
-    private PageResult<CrmReceivableRespVO> buildReceivableDetailPage(PageResult<CrmReceivableDO> pageResult) {
-        List<CrmReceivableDO> receivableList = pageResult.getList();
+    private List<CrmReceivableRespVO> buildReceivableDetailList(List<CrmReceivableDO> receivableList) {
         if (CollUtil.isEmpty(receivableList)) {
-            return PageResult.empty(pageResult.getTotal());
+            return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(receivableList, CrmReceivableDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivableList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 获得合同列表
-        List<CrmContractDO> contractList = contractService.getContractList(
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 获得合同列表
+        Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
                 convertSet(receivableList, CrmReceivableDO::getContractId));
-        return CrmReceivableConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList);
+        // 2. 拼接结果
+        return BeanUtils.toBean(receivableList, CrmReceivableRespVO.class, (receivableVO) -> {
+            // 2.1 拼接客户名称
+            findAndThen(customerMap, receivableVO.getCustomerId(), customer -> receivableVO.setCustomerName(customer.getName()));
+            // 2.2 拼接负责人、创建人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(receivableVO.getCreator()),
+                    user -> receivableVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, receivableVO.getOwnerUserId(), user -> {
+                receivableVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> receivableVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.3 拼接合同信息
+            findAndThen(contractMap, receivableVO.getContractId(), contract ->
+                    receivableVO.setContract(BeanUtils.toBean(contract, CrmContractRespVO.class)));
+        });
     }
 
-
     @GetMapping("/check-receivables-count")
     @Operation(summary = "获得待审核回款数量")
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")

+ 46 - 33
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java

@@ -11,7 +11,7 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
@@ -34,14 +34,15 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -93,7 +94,14 @@ public class CrmReceivablePlanController {
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
     public CommonResult<CrmReceivablePlanRespVO> getReceivablePlan(@RequestParam("id") Long id) {
         CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(id);
-        return success(BeanUtils.toBean(receivablePlan, CrmReceivablePlanRespVO.class));
+        return success(buildReceivablePlanDetail(receivablePlan));
+    }
+
+    private CrmReceivablePlanRespVO buildReceivablePlanDetail(CrmReceivablePlanDO receivablePlan) {
+        if (receivablePlan == null) {
+            return null;
+        }
+        return buildReceivableDetailList(Collections.singletonList(receivablePlan)).get(0);
     }
 
     @GetMapping("/page")
@@ -101,7 +109,7 @@ public class CrmReceivablePlanController {
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
     public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPage(@Valid CrmReceivablePlanPageReqVO pageReqVO) {
         PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(pageReqVO, getLoginUserId());
-        return success(convertDetailReceivablePlanPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -109,10 +117,9 @@ public class CrmReceivablePlanController {
     public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPageByCustomer(@Valid CrmReceivablePlanPageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPageByCustomerId(pageReqVO);
-        return success(convertDetailReceivablePlanPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
-    // TODO 芋艿:后面在优化导出
     @GetMapping("/export-excel")
     @Operation(summary = "导出回款计划 Excel")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:export')")
@@ -120,39 +127,42 @@ public class CrmReceivablePlanController {
     public void exportReceivablePlanExcel(@Valid CrmReceivablePlanPageReqVO exportReqVO,
                                           HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
-        PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(exportReqVO, getLoginUserId());
+        List<CrmReceivablePlanDO> list = receivablePlanService.getReceivablePlanPage(exportReqVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "回款计划.xls", "数据", CrmReceivablePlanRespVO.class,
-                convertDetailReceivablePlanPage(pageResult).getList());
+                buildReceivableDetailList(list));
     }
 
-    /**
-     * 构建详细的回款计划分页结果
-     *
-     * @param pageResult 简单的回款计划分页结果
-     * @return 详细的回款计划分页结果
-     */
-    private PageResult<CrmReceivablePlanRespVO> convertDetailReceivablePlanPage(PageResult<CrmReceivablePlanDO> pageResult) {
-        List<CrmReceivablePlanDO> receivablePlanList = pageResult.getList();
+    private List<CrmReceivablePlanRespVO> buildReceivableDetailList(List<CrmReceivablePlanDO> receivablePlanList) {
         if (CollUtil.isEmpty(receivablePlanList)) {
-            return PageResult.empty(pageResult.getTotal());
+            return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+        // 1.1 获取客户 Map
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivablePlanList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 获得合同列表
-        List<CrmContractDO> contractList = contractService.getContractList(
+        // 1.3 获得合同 Map
+        Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getContractId));
-        // 4. 获得还款列表
-        List<CrmReceivableDO> receivableList = receivableService.getReceivableList(
+        // 1.4 获得回款 Map
+        Map<Long, CrmReceivableDO> receivableMap = receivableService.getReceivableMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getReceivableId));
-        return CrmReceivablePlanConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList, receivableList);
+        // 2. 拼接数据
+        return BeanUtils.toBean(receivablePlanList, CrmReceivablePlanRespVO.class, (receivablePlanVO) -> {
+            // 2.1 拼接客户信息
+            findAndThen(customerMap, receivablePlanVO.getCustomerId(), customer -> receivablePlanVO.setCustomerName(customer.getName()));
+            // 2.2 拼接用户信息
+            findAndThen(userMap, receivablePlanVO.getOwnerUserId(), user -> receivablePlanVO.setOwnerUserName(user.getNickname()));
+            findAndThen(userMap, Long.parseLong(receivablePlanVO.getCreator()), user -> receivablePlanVO.setCreatorName(user.getNickname()));
+            // 2.3 拼接合同信息
+            findAndThen(contractMap, receivablePlanVO.getContractId(), contract -> receivablePlanVO.setContractNo(contract.getNo()));
+            // 2.4 拼接回款信息
+            receivablePlanVO.setReceivable(BeanUtils.toBean(receivableMap.get(receivablePlanVO.getReceivableId()), CrmReceivableRespVO.class));
+        });
     }
 
-
     @GetMapping("/remind-receivable-plan-count")
     @Operation(summary = "获得待回款提醒数量")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
@@ -160,18 +170,21 @@ public class CrmReceivablePlanController {
         return success(receivablePlanService.getRemindReceivablePlanCount(getLoginUserId()));
     }
 
-    @GetMapping("/list-all-simple-by-customer")
+    @GetMapping("/simple-list")
     @Operation(summary = "获得回款计划精简列表", description = "获得回款计划精简列表,主要用于前端的下拉选项")
     @Parameters({
             @Parameter(name = "customerId", description = "客户编号", required = true),
             @Parameter(name = "contractId", description = "合同编号", required = true)
     })
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
-    public CommonResult<List<CrmReceivablePlanRespVO>> getListAllSimpleByCustomer(@RequestParam("customerId") Long customerId,
-                                                                                  @RequestParam("contractId") Long contractId) {
-        PageResult<CrmReceivablePlanDO> result = receivablePlanService.getReceivablePlanPageByCustomerId(
-                new CrmReceivablePlanPageReqVO().setCustomerId(customerId).setContractId(contractId));
-        return success(BeanUtils.toBean(result.getList(), CrmReceivablePlanRespVO.class));
+    public CommonResult<List<CrmReceivablePlanRespVO>> getReceivablePlanSimpleList(@RequestParam("customerId") Long customerId,
+                                                                                   @RequestParam("contractId") Long contractId) {
+        CrmReceivablePlanPageReqVO pageReqVO = new CrmReceivablePlanPageReqVO().setCustomerId(customerId).setContractId(contractId);
+        pageReqVO.setPageNo(PAGE_SIZE_NONE);
+        PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPageByCustomerId(pageReqVO);
+        return success(convertList(pageResult.getList(), receivablePlan -> new CrmReceivablePlanRespVO() // 只返回 id、period 等信息
+                .setId(receivablePlan.getId()).setPeriod(receivablePlan.getPeriod()).setReceivableId(receivablePlan.getReceivableId())
+                .setPrice(receivablePlan.getPrice()).setReturnType(receivablePlan.getReturnType())));
     }
 
 }

+ 15 - 8
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
 
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
+import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -35,17 +37,16 @@ public class CrmReceivablePlanRespVO {
     @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
     private LocalDateTime returnTime;
 
-    @Schema(description = "回款方式", example = "1")
-    private Integer returnType; // 来自 Receivable 的 returnType 字段
+    @Schema(description = "计划回款方式", example = "1")
+    private Integer returnType;
 
     @Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
     private BigDecimal price;
 
     @Schema(description = "回款编号", example = "19852")
     private Long receivableId;
-
-    @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    private Boolean finishStatus;
+    @Schema(description = "回款信息")
+    private CrmReceivableRespVO receivable;
 
     @Schema(description = "提前几天提醒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer remindDays;
@@ -57,12 +58,18 @@ public class CrmReceivablePlanRespVO {
     private String remark;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
     private LocalDateTime createTime;
 
-    @Schema(description = "创建人", example = "25682")
-    private String creator;
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
+    private LocalDateTime updateTime;
 
-    @Schema(description = "创建人名字", example = "test")
+    @Schema(description = "创建人", example = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
     private String creatorName;
 
 }

+ 2 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanSaveReqVO.java

@@ -14,9 +14,8 @@ public class CrmReceivablePlanSaveReqVO {
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Long id;
 
-    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    @NotNull(message = "客户编号不能为空")
-    private Long customerId;
+    @Schema(description = "客户编号", hidden = true, example = "2")
+    private Long customerId; // 该字段不通过前端传递,而是 contractId 查询出来设置进去
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @NotNull(message = "合同编号不能为空")
@@ -37,10 +36,6 @@ public class CrmReceivablePlanSaveReqVO {
     @NotNull(message = "计划回款金额不能为空")
     private BigDecimal price;
 
-    @Schema(description = "提醒日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @NotNull(message = "提醒日期不能为空")
-    private LocalDateTime remindTime;
-
     @Schema(description = "提前几天提醒", example = "1")
     private Integer remindDays;
 

+ 23 - 13
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
 
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
+import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -31,12 +33,30 @@ public class CrmReceivableRespVO {
 
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Long customerId;
+    @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
+    private String customerName;
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Long contractId;
+    @Schema(description = "合同信息")
+    private CrmContractRespVO contract;
 
-    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
     private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "工作流编号", example = "1043")
+    @ExcelProperty("工作流编号")
+    private String processInstanceId;
+
+    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    private Integer auditStatus;
 
     @Schema(description = "备注", example = "备注")
     private String remark;
@@ -44,21 +64,11 @@ public class CrmReceivableRespVO {
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
-    @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
-    private String customerName;
-
-    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
-    private Integer auditStatus;
-
-    @Schema(description = "合同编号", example = "Q110")
-    private String contractNo;
-
-    @Schema(description = "负责人", example = "test")
-    private String ownerUserName;
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime updateTime;
 
     @Schema(description = "创建人", example = "25682")
     private String creator;
-
     @Schema(description = "创建人名字", example = "test")
     private String creatorName;
 

+ 24 - 18
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
 
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.operatelog.core.*;
+import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -16,37 +18,41 @@ public class CrmReceivableSaveReqVO {
     @Schema(description = "编号", example = "25787")
     private Long id;
 
-    @Schema(description = "回款编号", example = "31177")
-    private String no;
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
+    @NotNull(message = "负责人编号不能为空")
+    private Long ownerUserId;
 
-    @Schema(description = "回款计划编号", example = "1024")
-    private Long planId; // 不是通过回款计划创建的回款没有回款计划编号
+    @Schema(description = "客户编号", example = "2")
+    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
+    private Long customerId; // 该字段不通过前端传递,而是 contractId 查询出来设置进去
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @DiffLogField(name = "合同", function = CrmContractParseFunction.NAME)
+    @NotNull(message = "合同编号不能为空")
+    private Long contractId;
+
+    @Schema(description = "回款计划编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @DiffLogField(name = "合同", function = CrmReceivablePlanParseFunction.NAME)
+    private Long planId;
 
     @Schema(description = "回款方式", example = "2")
+    @DiffLogField(name = "回款方式", function = CrmReceivableReturnTypeParseFunction.NAME)
     @InEnum(CrmReceivableReturnTypeEnum.class)
     private Integer returnType;
 
     @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    @DiffLogField(name = "回款金额")
     @NotNull(message = "回款金额不能为空")
     private BigDecimal price;
 
-    @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
-    @NotNull(message = "计划回款日期不能为空")
+    @Schema(description = "回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @NotNull(message = "回款日期不能为空")
+    @DiffLogField(name = "回款日期")
     private LocalDateTime returnTime;
 
-    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    @NotNull(message = "客户编号不能为空")
-    private Long customerId;
-
-    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    @NotNull(message = "合同编号不能为空")
-    private Long contractId;
-
-    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    @NotNull(message = "负责人编号不能为空")
-    private Long ownerUserId;
-
     @Schema(description = "备注", example = "备注")
+    @DiffLogField(name = "备注")
     private String remark;
 
 }

+ 0 - 44
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java

@@ -1,44 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.receivable;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
-import java.util.Map;
-
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
-
-/**
- * 回款 Convert
- *
- * @author 赤焰
- */
-@Mapper
-public interface CrmReceivableConvert {
-
-    CrmReceivableConvert INSTANCE = Mappers.getMapper(CrmReceivableConvert.class);
-
-    default PageResult<CrmReceivableRespVO> convertPage(PageResult<CrmReceivableDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
-                                                        List<CrmCustomerDO> customerList, List<CrmContractDO> contractList) {
-        PageResult<CrmReceivableRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmReceivableRespVO.class);
-        // 拼接关联字段
-        Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
-        Map<Long, CrmContractDO> contractMap = convertMap(contractList, CrmContractDO::getId);
-        voPageResult.getList().forEach(receivable -> {
-            findAndThen(userMap, receivable.getOwnerUserId(), user -> receivable.setOwnerUserName(user.getNickname()));
-            findAndThen(userMap, Long.parseLong(receivable.getCreator()), user -> receivable.setCreatorName(user.getNickname()));
-            findAndThen(customerMap, receivable.getCustomerId(), customer -> receivable.setCustomerName(customer.getName()));
-            findAndThen(contractMap, receivable.getContractId(), contract -> receivable.setContractNo(contract.getNo()));
-        });
-        return voPageResult;
-    }
-
-}

+ 0 - 48
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java

@@ -1,48 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.receivable;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanRespVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
-import java.util.Map;
-
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
-
-/**
- * 回款计划 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface CrmReceivablePlanConvert {
-
-    CrmReceivablePlanConvert INSTANCE = Mappers.getMapper(CrmReceivablePlanConvert.class);
-
-    default PageResult<CrmReceivablePlanRespVO> convertPage(PageResult<CrmReceivablePlanDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
-                                                            List<CrmCustomerDO> customerList, List<CrmContractDO> contractList,
-                                                            List<CrmReceivableDO> receivableList) {
-        PageResult<CrmReceivablePlanRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmReceivablePlanRespVO.class);
-        // 拼接关联字段
-        Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
-        Map<Long, CrmContractDO> contractMap = convertMap(contractList, CrmContractDO::getId);
-        Map<Long, CrmReceivableDO> receivableMap = convertMap(receivableList, CrmReceivableDO::getId);
-        voPageResult.getList().forEach(receivablePlan -> {
-            findAndThen(userMap, receivablePlan.getOwnerUserId(), user -> receivablePlan.setOwnerUserName(user.getNickname()));
-            findAndThen(userMap, Long.parseLong(receivablePlan.getCreator()), user -> receivablePlan.setCreatorName(user.getNickname()));
-            findAndThen(customerMap, receivablePlan.getCustomerId(), customer -> receivablePlan.setCustomerName(customer.getName()));
-            findAndThen(contractMap, receivablePlan.getContractId(), contract -> receivablePlan.setContractNo(contract.getNo()));
-            findAndThen(receivableMap, receivablePlan.getReceivableId(), receivable -> receivablePlan.setReturnType(receivable.getReturnType()));
-        });
-        return voPageResult;
-    }
-
-}

+ 9 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java

@@ -39,15 +39,21 @@ public class CrmReceivableDO extends BaseDO {
      */
     private String no;
     /**
-     * 回款计划编号,关联 {@link CrmReceivablePlanDO#getId()}
+     * 回款计划编号
+     *
+     * 关联 {@link CrmReceivablePlanDO#getId()},非必须
      */
     private Long planId;
     /**
-     * 客户编号,关联 {@link CrmCustomerDO#getId()}
+     * 客户编号
+     *
+     * 关联 {@link CrmCustomerDO#getId()}
      */
     private Long customerId;
     /**
-     * 合同编号,关联 {@link CrmContractDO#getId()}
+     * 合同编号
+     *
+     * 关联 {@link CrmContractDO#getId()}
      */
     private Long contractId;
     /**

+ 1 - 5
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java

@@ -61,7 +61,7 @@ public class CrmReceivablePlanDO extends BaseDO {
      */
     private LocalDateTime returnTime;
     /**
-     * 回款类型
+     * 计划回款类型
      *
      * 枚举 {@link CrmReceivableReturnTypeEnum}
      */
@@ -75,10 +75,6 @@ public class CrmReceivablePlanDO extends BaseDO {
      * 回款编号,关联 {@link CrmReceivableDO#getId()}
      */
     private Long receivableId;
-    /**
-     * 完成状态
-     */
-    private Boolean finishStatus;
 
     /**
      * 提前几天提醒

+ 0 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 回款
- */
-package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;

+ 25 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
 
+import cn.hutool.core.collection.CollUtil;
 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;
@@ -11,10 +12,16 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.math.BigDecimal;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
  * 回款 Mapper
@@ -24,6 +31,10 @@ import java.util.List;
 @Mapper
 public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
 
+    default CrmReceivableDO selectByNo(String no) {
+        return selectOne(CrmReceivableDO::getNo, no);
+    }
+
     default PageResult<CrmReceivableDO> selectPageByCustomerId(CrmReceivablePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmReceivableDO>()
                 .eq(CrmReceivableDO::getCustomerId, reqVO.getCustomerId()) // 必须传递
@@ -67,4 +78,18 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
         return selectCount(query);
     }
 
+    default Map<Long, BigDecimal> selectReceivablePriceMapByContractId(Collection<Long> contractIds) {
+        if (CollUtil.isEmpty(contractIds)) {
+            return Collections.emptyMap();
+        }
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmReceivableDO>()
+                .select("contract_id, SUM(price) AS total_price")
+                .eq("audit_status", CrmAuditStatusEnum.APPROVE.getStatus())
+                .groupBy("contract_id")
+                .in("contract_id", contractIds));
+        // 获得金额
+        return convertMap(result, obj -> (Long) obj.get("contract_id"), obj -> (BigDecimal) obj.get("total_price"));
+    }
+
 }

+ 10 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java

@@ -25,6 +25,13 @@ import java.util.Objects;
 @Mapper
 public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> {
 
+    default CrmReceivablePlanDO selectMaxPeriodByContractId(Long contractId) {
+        return selectOne(new MPJLambdaWrapperX<CrmReceivablePlanDO>()
+                .eq(CrmReceivablePlanDO::getContractId, contractId)
+                .orderByDesc(CrmReceivablePlanDO::getPeriod)
+                .last("LIMIT 1"));
+    }
+
     default PageResult<CrmReceivablePlanDO> selectPageByCustomerId(CrmReceivablePlanPageReqVO reqVO) {
         MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>();
         if (Objects.nonNull(reqVO.getContractNo())) { // 根据合同编号检索
@@ -47,7 +54,6 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
                 .eqIfPresent(CrmReceivablePlanDO::getCustomerId, pageReqVO.getCustomerId())
                 .eqIfPresent(CrmReceivablePlanDO::getContractId, pageReqVO.getContractId())
                 .orderByDesc(CrmReceivablePlanDO::getPeriod);
-
         if (Objects.nonNull(pageReqVO.getContractNo())) { // 根据合同编号检索
             query.innerJoin(CrmContractDO.class, on -> on.like(CrmContractDO::getNo, pageReqVO.getContractNo())
                     .eq(CrmContractDO::getId, CrmReceivablePlanDO::getContractId));
@@ -67,9 +73,7 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
         } else if (CrmReceivablePlanPageReqVO.REMIND_TYPE_RECEIVED.equals(pageReqVO.getRemindType())) { // 已回款
             query.isNotNull(CrmReceivablePlanDO::getReceivableId)
                     .between(CrmReceivablePlanDO::getReturnTime, beginOfToday, endOfToday.plusDays(REMIND_DAYS));
-            ;
         }
-
         return selectJoinPage(pageReqVO, CrmReceivablePlanDO.class, query);
     }
 
@@ -93,6 +97,9 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
         LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
         query.isNull(CrmReceivablePlanDO::getReceivableId)
                 .between(CrmReceivablePlanDO::getReturnTime, beginOfToday, endOfToday.plusDays(REMIND_DAYS));
+        // TODO return_time 小于现在;
+        // TODO 未回款
+        // TODO remind_time 大于现在;
         return selectCount(query);
     }
 

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/redis/no/CrmNoRedisDAO.java

@@ -25,7 +25,7 @@ public class CrmNoRedisDAO {
     public static final String CONTRACT_NO_PREFIX = "HT";
 
     /**
-     * 款 {@link cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO}
+     * 款 {@link cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO}
      */
     public static final String RECEIVABLE_PREFIX = "HK";
 

+ 44 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmReceivablePlanParseFunction.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivablePlanService;
+import com.mzt.logapi.service.IParseFunction;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * CRM 回款计划的 {@link IParseFunction} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class CrmReceivablePlanParseFunction implements IParseFunction {
+
+    public static final String NAME = "getReceivablePlanServiceById";
+
+    @Resource
+    private CrmReceivablePlanService receivablePlanService;
+
+    @Override
+    public boolean executeBefore() {
+        return true; // 先转换值后对比
+    }
+
+    @Override
+    public String functionName() {
+        return NAME;
+    }
+
+    @Override
+    public String apply(Object value) {
+        if (StrUtil.isEmptyIfStr(value)) {
+            return "";
+        }
+        CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(Long.parseLong(value.toString()));
+        return receivablePlan == null ? "" : receivablePlan.getPeriod().toString();
+    }
+
+}

+ 40 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmReceivableReturnTypeParseFunction.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
+import com.mzt.logapi.service.IParseFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_RECEIVABLE_RETURN_TYPE;
+
+/**
+ * CRM 回款方式的 {@link IParseFunction} 实现类
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+@Component
+public class CrmReceivableReturnTypeParseFunction implements IParseFunction {
+
+    public static final String NAME = "getReceivableReturnType";
+
+    @Override
+    public boolean executeBefore() {
+        return true; // 先转换值后对比
+    }
+
+    @Override
+    public String functionName() {
+        return NAME;
+    }
+
+    @Override
+    public String apply(Object value) {
+        if (StrUtil.isEmptyIfStr(value)) {
+            return "";
+        }
+        return DictFrameworkUtils.getDictDataLabel(CRM_RECEIVABLE_RETURN_TYPE, value.toString());
+    }
+
+}

+ 21 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java

@@ -13,6 +13,9 @@ import jakarta.validation.Valid;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
  * CRM 合同 Service 接口
@@ -85,6 +88,14 @@ public interface CrmContractService {
      */
     CrmContractDO getContract(Long id);
 
+    /**
+     * 校验合同是否合法
+     *
+     * @param id 编号
+     * @return 合同
+     */
+    CrmContractDO validateContract(Long id);
+
     /**
      * 获得合同列表
      *
@@ -93,6 +104,16 @@ public interface CrmContractService {
      */
     List<CrmContractDO> getContractList(Collection<Long> ids);
 
+    /**
+     * 获得合同 Map
+     *
+     * @param ids 编号
+     * @return 合同 Map
+     */
+    default Map<Long, CrmContractDO> getContractMap(Collection<Long> ids) {
+        return convertMap(getContractList(ids), CrmContractDO::getId);
+    }
+
     /**
      * 获得合同分页
      *

+ 5 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java

@@ -323,6 +323,11 @@ public class CrmContractServiceImpl implements CrmContractService {
         return contractMapper.selectById(id);
     }
 
+    @Override
+    public CrmContractDO validateContract(Long id) {
+        return validateContractExists(id);
+    }
+
     @Override
     public List<CrmContractDO> getContractList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java

@@ -38,7 +38,7 @@ public interface CrmReceivablePlanService {
      * @param id           编号
      * @param receivableId 回款编号
      */
-    void updateReceivableId(Long id, Long receivableId);
+    void updateReceivablePlanReceivableId(Long id, Long receivableId);
 
     /**
      * 删除回款计划

+ 40 - 35
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java

@@ -2,20 +2,17 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanSaveReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
-import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -33,7 +30,8 @@ import java.util.List;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_PLAN_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_PLAN_UPDATE_FAIL;
 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 
 /**
@@ -54,8 +52,6 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
     @Resource
     private CrmContractService contractService;
     @Resource
-    private CrmCustomerService customerService;
-    @Resource
     private CrmPermissionService permissionService;
 
     @Resource
@@ -66,15 +62,16 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
     @LogRecord(type = CRM_RECEIVABLE_PLAN_TYPE, subType = CRM_RECEIVABLE_PLAN_CREATE_SUB_TYPE, bizNo = "{{#receivablePlan.id}}",
             success = CRM_RECEIVABLE_PLAN_CREATE_SUCCESS)
     public Long createReceivablePlan(CrmReceivablePlanSaveReqVO createReqVO) {
-        // 1.1 校验关联数据是否存在
+        // 1. 校验关联数据是否存在
         validateRelationDataExists(createReqVO);
-        // 1.2 查验关联合同回款数量
-        Long count = receivableService.getReceivableCountByContractId(createReqVO.getContractId());
-        int period = (int) (count + 1);
 
-        // 2. 插入还款计划
-        CrmReceivablePlanDO receivablePlan = BeanUtils.toBean(createReqVO, CrmReceivablePlanDO.class)
-                .setPeriod(period).setFinishStatus(false);
+        // 2. 插入回款计划
+        CrmReceivablePlanDO maxPeriodReceivablePlan = receivablePlanMapper.selectMaxPeriodByContractId(createReqVO.getContractId());
+        int period = maxPeriodReceivablePlan == null ? 1 : maxPeriodReceivablePlan.getPeriod() + 1;
+        CrmReceivablePlanDO receivablePlan = BeanUtils.toBean(createReqVO, CrmReceivablePlanDO.class).setPeriod(period);
+        if (createReqVO.getReturnTime() != null && createReqVO.getRemindDays() != null) {
+            receivablePlan.setRemindTime(createReqVO.getReturnTime().minusDays(createReqVO.getRemindDays()));
+        }
         receivablePlanMapper.insert(receivablePlan);
 
         // 3. 创建数据权限
@@ -93,15 +90,21 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
             success = CRM_RECEIVABLE_PLAN_UPDATE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE_PLAN, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateReceivablePlan(CrmReceivablePlanSaveReqVO updateReqVO) {
-        // 1. 校验存在
+        updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null); // 防止修改这些字段
+        // 1.1 校验存在
         validateRelationDataExists(updateReqVO);
+        // 1.2 校验关联数据是否存在
         CrmReceivablePlanDO oldReceivablePlan = validateReceivablePlanExists(updateReqVO.getId());
-        if (Objects.nonNull(oldReceivablePlan.getReceivableId())) { // 如果已经有对应的还款,则不允许编辑;
-            throw exception(RECEIVABLE_PLAN_UPDATE_FAIL, "已经有对应的还款");
+        // 1.3 如果已经有对应的回款,则不允许编辑
+        if (Objects.nonNull(oldReceivablePlan.getReceivableId())) {
+            throw exception(RECEIVABLE_PLAN_UPDATE_FAIL);
         }
 
-        // 2. 更新
+        // 2. 更新回款计划
         CrmReceivablePlanDO updateObj = BeanUtils.toBean(updateReqVO, CrmReceivablePlanDO.class);
+        if (updateReqVO.getReturnTime() != null && updateReqVO.getRemindDays() != null) {
+            updateObj.setRemindTime(updateReqVO.getReturnTime().minusDays(updateReqVO.getRemindDays()));
+        }
         receivablePlanMapper.updateById(updateObj);
 
         // 3. 记录操作日志上下文
@@ -109,24 +112,24 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
         LogRecordContext.putVariable("receivablePlan", oldReceivablePlan);
     }
 
+    private void validateRelationDataExists(CrmReceivablePlanSaveReqVO reqVO) {
+        // 校验负责人存在
+        if (reqVO.getOwnerUserId() != null) {
+            adminUserApi.validateUser(reqVO.getOwnerUserId());
+        }
+        // 校验合同存在
+        if (reqVO.getContractId() != null) {
+            CrmContractDO contract = contractService.getContract(reqVO.getContractId());
+            reqVO.setCustomerId(contract.getCustomerId());
+        }
+    }
+
     @Override
-    public void updateReceivableId(Long id, Long receivableId) {
+    public void updateReceivablePlanReceivableId(Long id, Long receivableId) {
         // 校验存在
         validateReceivablePlanExists(id);
         // 更新回款计划
-        receivablePlanMapper.updateById(new CrmReceivablePlanDO().setReceivableId(receivableId).setFinishStatus(true));
-    }
-
-    private void validateRelationDataExists(CrmReceivablePlanSaveReqVO reqVO) {
-        adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
-        CrmContractDO contract = contractService.getContract(reqVO.getContractId());
-        if (ObjectUtil.isNull(contract)) { // 合同不存在
-            throw exception(CONTRACT_NOT_EXISTS);
-        }
-        CrmCustomerDO customer = customerService.getCustomer(reqVO.getCustomerId());
-        if (ObjectUtil.isNull(customer)) { // 客户不存在
-            throw exception(CUSTOMER_NOT_EXISTS);
-        }
+        receivablePlanMapper.updateById(new CrmReceivablePlanDO().setId(id).setReceivableId(receivableId));
     }
 
     @Override
@@ -135,13 +138,15 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
             success = CRM_RECEIVABLE_PLAN_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE_PLAN, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteReceivablePlan(Long id) {
-        // 校验存在
+        // 1. 校验存在
         CrmReceivablePlanDO receivablePlan = validateReceivablePlanExists(id);
-        // 删除
+
+        // 2. 删除
         receivablePlanMapper.deleteById(id);
-        // 删除数据权限
+        // 3. 删除数据权限
         permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
-        // 记录操作日志上下文
+
+        // 4. 记录操作日志上下文
         LogRecordContext.putVariable("receivablePlan", receivablePlan);
     }
 

+ 18 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java

@@ -7,8 +7,12 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
 import jakarta.validation.Valid;
 
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
  * CRM 回款 Service 接口
@@ -71,6 +75,16 @@ public interface CrmReceivableService {
      */
     List<CrmReceivableDO> getReceivableList(Collection<Long> ids);
 
+    /**
+     * 获得回款 Map
+     *
+     * @param ids 编号
+     * @return 回款 Map
+     */
+    default Map<Long, CrmReceivableDO> getReceivableMap(Collection<Long> ids) {
+        return convertMap(getReceivableList(ids), CrmReceivableDO::getId);
+    }
+
     /**
      * 获得回款分页
      *
@@ -101,11 +115,11 @@ public interface CrmReceivableService {
     Long getCheckReceivablesCount(Long userId);
 
     /**
-     * 根据合同编号,获取合同关联的回款数量
+     * 获得合同已回款金额 Map
      *
-     * @param contractId 合同编号
-     * @return 数量
+     * @param contractIds 合同编号
+     * @return 回款金额 Map
      */
-    Long getReceivableCountByContractId(Long contractId);
+    Map<Long, BigDecimal> getReceivablePriceMapByContractId(Collection<Long> contractIds);
 
 }

+ 55 - 40
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java

@@ -13,16 +13,15 @@ import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
+import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
-import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -36,9 +35,10 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.List;
-import java.util.Objects;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
@@ -56,17 +56,18 @@ import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmRes
 public class CrmReceivableServiceImpl implements CrmReceivableService {
 
     /**
-     * BPM 回款审批流程标识
+     * BPM 合同审批流程标识
      */
-    public static final String RECEIVABLE_APPROVE = "receivable-approve";
+    public static final String BPM_PROCESS_DEFINITION_KEY = "crm-receivable-audit";
 
     @Resource
     private CrmReceivableMapper receivableMapper;
 
     @Resource
-    private CrmContractService contractService;
+    private CrmNoRedisDAO noRedisDAO;
+
     @Resource
-    private CrmCustomerService customerService;
+    private CrmContractService contractService;
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private CrmReceivablePlanService receivablePlanService;
@@ -78,48 +79,63 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @Resource
     private BpmProcessInstanceApi bpmProcessInstanceApi;
 
+    // TODO @puhui999:操作日志没记录上
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
             success = CRM_RECEIVABLE_CREATE_SUCCESS)
     public Long createReceivable(CrmReceivableSaveReqVO createReqVO) {
-        // 1. 校验关联数据存在
+        // 1.1 校验关联数据存在
         validateRelationDataExists(createReqVO);
-        // 2. 插入还款
-        CrmReceivableDO receivable = BeanUtils.toBean(createReqVO, CrmReceivableDO.class).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
+        // 1.2 生成回款编号
+        String no = noRedisDAO.generate(CrmNoRedisDAO.RECEIVABLE_PREFIX);
+        if (receivableMapper.selectByNo(no) != null) {
+            throw exception(RECEIVABLE_NO_EXISTS);
+        }
+
+        // 2. 插入回款
+        CrmReceivableDO receivable = BeanUtils.toBean(createReqVO, CrmReceivableDO.class)
+                .setNo(no).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
         receivableMapper.insert(receivable);
+
         // 3. 创建数据权限
         permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_RECEIVABLE.getType())
-                .setBizId(receivable.getId()).setUserId(createReqVO.getOwnerUserId()).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+                .setBizId(receivable.getId()).setUserId(createReqVO.getOwnerUserId())
+                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
         // 4. 更新关联的回款计划
-        if (Objects.nonNull(createReqVO.getPlanId())) {
-            receivablePlanService.updateReceivableId(receivable.getPlanId(), receivable.getId());
+        if (createReqVO.getPlanId() != null) {
+            receivablePlanService.updateReceivablePlanReceivableId(receivable.getPlanId(), receivable.getId());
         }
+
         // 5. 记录操作日志上下文
         LogRecordContext.putVariable("receivable", receivable);
         return receivable.getId();
     }
 
     private void validateRelationDataExists(CrmReceivableSaveReqVO reqVO) {
-        adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
-        CrmContractDO contract = contractService.getContract(reqVO.getContractId());
-        if (ObjectUtil.isNull(contract)) {
-            throw exception(CONTRACT_NOT_EXISTS);
-        }
-        CrmCustomerDO customer = customerService.getCustomer(reqVO.getCustomerId());
-        if (ObjectUtil.isNull(customer)) {
-            throw exception(CUSTOMER_NOT_EXISTS);
+        if (reqVO.getOwnerUserId() != null) {
+            adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
         }
-        if (Objects.isNull(reqVO.getPlanId())) { // 没有回款计划编号则不校验
-            return;
+        if (reqVO.getContractId() != null) {
+            CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
+            if (ObjectUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+                throw exception(RECEIVABLE_CREATE_FAIL_CONTRACT_NOT_APPROVE);
+            }
+            reqVO.setCustomerId(contract.getCustomerId()); // 设置客户编号
         }
-        CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(reqVO.getPlanId());
-        if (ObjectUtil.isNull(receivablePlan)) {
-            throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+        if (reqVO.getPlanId() != null) {
+            CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(reqVO.getPlanId());
+            if (receivablePlan == null) {
+                throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+            }
+            if (receivablePlan.getReceivableId() != null) {
+                throw exception(RECEIVABLE_PLAN_EXISTS_RECEIVABLE);
+            }
         }
-
     }
 
+    // TODO @puhui999:操作日志没记录上
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
@@ -127,6 +143,7 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateReceivable(CrmReceivableSaveReqVO updateReqVO) {
         Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
+        updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
         // 1.1 校验存在
         CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId());
         // 1.2 只有草稿、审批中,可以编辑;
@@ -135,7 +152,7 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
             throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
         }
 
-        // 2. 更新
+        // 2. 更新
         CrmReceivableDO updateObj = BeanUtils.toBean(updateReqVO, CrmReceivableDO.class);
         receivableMapper.updateById(updateObj);
 
@@ -160,27 +177,25 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         receivableMapper.updateById(new CrmReceivableDO().setId(id).setAuditStatus(auditStatus));
     }
 
-    // TODO @liuhongfeng:缺一个取消回款的接口;只有草稿、审批中可以取消;CrmAuditStatusEnum
-
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_DELETE_SUB_TYPE, bizNo = "{{#id}}",
             success = CRM_RECEIVABLE_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteReceivable(Long id) {
-        // 校验存在
+        // 1.1 校验存在
         CrmReceivableDO receivable = validateReceivableExists(id);
-        // 如果被 CrmReceivablePlanDO 所使用,则不允许删除
-        if (Objects.nonNull(receivable.getPlanId()) && receivablePlanService.getReceivablePlan(receivable.getPlanId()) != null) {
+        // 1.2 如果被 CrmReceivablePlanDO 所使用,则不允许删除
+        if (receivable.getPlanId() != null && receivablePlanService.getReceivablePlan(receivable.getPlanId()) != null) {
             throw exception(RECEIVABLE_DELETE_FAIL);
         }
-        // 删除
-        receivableMapper.deleteById(id);
 
-        // 删除数据权限
+        // 2. 删除
+        receivableMapper.deleteById(id);
+        // 3. 删除数据权限
         permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE.getType(), id);
 
-        // 记录操作日志上下文
+        // 4. 记录操作日志上下文
         LogRecordContext.putVariable("receivable", receivable);
     }
 
@@ -197,7 +212,7 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
 
         // 2. 创建回款审批流程实例
         String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
-                .setProcessDefinitionKey(RECEIVABLE_APPROVE).setBusinessKey(String.valueOf(id)));
+                .setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
 
         // 3. 更新回款工作流编号
         receivableMapper.updateById(new CrmReceivableDO().setId(id).setProcessInstanceId(processInstanceId)
@@ -246,8 +261,8 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     }
 
     @Override
-    public Long getReceivableCountByContractId(Long contractId) {
-        return receivableMapper.selectCount(CrmReceivableDO::getContractId, contractId);
+    public Map<Long, BigDecimal> getReceivablePriceMapByContractId(Collection<Long> contractIds) {
+        return receivableMapper.selectReceivablePriceMapByContractId(contractIds);
     }
 
 }

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/listener/CrmReceivableResultListener.java

@@ -20,7 +20,7 @@ public class CrmReceivableResultListener extends BpmProcessInstanceResultEventLi
 
     @Override
     public String getProcessDefinitionKey() {
-        return CrmReceivableServiceImpl.RECEIVABLE_APPROVE;
+        return CrmReceivableServiceImpl.BPM_PROCESS_DEFINITION_KEY;
     }
 
     @Override