From ca8ff9dbf78b5372e78ce4864f684c6ac0337948 Mon Sep 17 00:00:00 2001 From: HuangHuiKang Date: Wed, 1 Jul 2026 18:02:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E8=BF=81=E7=A7=BB=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=88=B0mes=E6=A8=A1=E5=9D=97=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/customer/CrmCustomerController.java | 286 ++++++++++++++++++ .../customer/CrmCustomerDistributeReqVO.java | 22 ++ .../vo/customer/CrmCustomerImportExcelVO.java | 70 +++++ .../vo/customer/CrmCustomerImportReqVO.java | 26 ++ .../vo/customer/CrmCustomerImportRespVO.java | 24 ++ .../vo/customer/CrmCustomerLockReqVO.java | 16 + .../vo/customer/CrmCustomerPageReqVO.java | 58 ++++ .../vo/customer/CrmCustomerRespVO.java | 130 ++++++++ .../vo/customer/CrmCustomerSaveReqVO.java | 99 ++++++ .../vo/customer/CrmCustomerTransferReqVO.java | 39 +++ .../dataobject/customer/CrmCustomerDO.java | 127 ++++++++ .../dal/mysql/customer/CrmCustomerMapper.java | 104 +++++++ .../module/mes/enums/DictTypeConstants.java | 19 ++ .../mes/enums/common/CrmSceneTypeEnum.java | 51 ++++ .../enums/customer/CrmCustomerLevelEnum.java | 38 +++ .../permission/CrmPermissionLevelEnum.java | 60 ++++ .../core/AreaExcelColumnSelectFunction.java | 33 ++ .../CrmCustomerIndustryParseFunction.java | 40 +++ .../core/CrmCustomerLevelParseFunction.java | 40 +++ .../core/CrmCustomerSourceParseFunction.java | 40 +++ .../operatelog/core/SysAreaParseFunction.java | 38 +++ .../service/customer/CrmCustomerService.java | 200 ++++++++++++ .../customer/CrmCustomerServiceImpl.java | 249 +++++++++++++++ .../customer/bo/CrmCustomerCreateReqBO.java | 121 ++++++++ 24 files changed, 1930 insertions(+) create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/CrmCustomerController.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerDistributeReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportRespVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerLockReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerRespVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/customer/CrmCustomerDO.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/customer/CrmCustomerMapper.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/common/CrmSceneTypeEnum.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/customer/CrmCustomerLevelEnum.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/permission/CrmPermissionLevelEnum.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/excel/core/AreaExcelColumnSelectFunction.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerIndustryParseFunction.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerLevelParseFunction.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerSourceParseFunction.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/SysAreaParseFunction.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerService.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerServiceImpl.java create mode 100644 yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/bo/CrmCustomerCreateReqBO.java diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/CrmCustomerController.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/CrmCustomerController.java new file mode 100644 index 000000000..2d3e5d95d --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/CrmCustomerController.java @@ -0,0 +1,286 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +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.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer.*; +import cn.iocoder.yudao.module.mes.dal.dataobject.customer.CrmCustomerDO; +import cn.iocoder.yudao.module.mes.service.customer.CrmCustomerService; +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; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +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.*; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static java.util.Collections.singletonList; + +@Tag(name = "管理后台 - CRM 客户") +@RestController +@RequestMapping("/mes/customer") +@Validated +public class CrmCustomerController { + + @Resource + private CrmCustomerService customerService; + + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建客户") + @PreAuthorize("@ss.hasPermission('mes:customer:create')") + public CommonResult createCustomer(@Valid @RequestBody CrmCustomerSaveReqVO createReqVO) { + return success(customerService.createCustomer(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新客户") + @PreAuthorize("@ss.hasPermission('mes:customer:update')") + public CommonResult updateCustomer(@Valid @RequestBody CrmCustomerSaveReqVO updateReqVO) { + customerService.updateCustomer(updateReqVO); + return success(true); + } + + @PutMapping("/update-deal-status") + @Operation(summary = "更新客户的成交状态") + @Parameters({ + @Parameter(name = "id", description = "客户编号", required = true), + @Parameter(name = "dealStatus", description = "成交状态", required = true) + }) + public CommonResult updateCustomerDealStatus(@RequestParam("id") Long id, + @RequestParam("dealStatus") Boolean dealStatus) { + customerService.updateCustomerDealStatus(id, dealStatus); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除客户") + @Parameter(name = "id", description = "客户编号", required = true) + @PreAuthorize("@ss.hasPermission('mes:customer:delete')") + public CommonResult deleteCustomer(@RequestParam("id") Long id) { + customerService.deleteCustomer(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得客户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult getCustomer(@RequestParam("id") Long id) { + // 1. 获取客户 + CrmCustomerDO customer = customerService.getCustomer(id); + // 2. 拼接数据 + return success(buildCustomerDetail(customer)); + } + + public CrmCustomerRespVO buildCustomerDetail(CrmCustomerDO customer) { + if (customer == null) { + return null; + } + return buildCustomerDetailList(singletonList(customer)).get(0); + } + + @GetMapping("/page") + @Operation(summary = "获得客户分页") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) { + // 1. 查询客户分页 + PageResult pageResult = customerService.getCustomerPage(pageVO, getLoginUserId()); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + // 2. 拼接数据 + return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal())); + } + + public List buildCustomerDetailList(List list) { + if (CollUtil.isEmpty(list)) { + return java.util.Collections.emptyList(); + } + // 1.1 获取创建人、负责人列表 + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(list, + contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId()))); + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + // 1.2 获取距离进入公海的时间 + Map poolDayMap = getPoolDayMap(list); + // 2. 转换成 VO + return BeanUtils.toBean(list, CrmCustomerRespVO.class, customerVO -> { + customerVO.setAreaName(AreaUtils.format(customerVO.getAreaId())); + // 2.1 设置创建人、负责人名称 + MapUtils.findAndThen(userMap, NumberUtils.parseLong(customerVO.getCreator()), + user -> customerVO.setCreatorName(user.getNickname())); + MapUtils.findAndThen(userMap, customerVO.getOwnerUserId(), user -> { + customerVO.setOwnerUserName(user.getNickname()); + MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> customerVO.setOwnerUserDeptName(dept.getName())); + }); + // 2.2 设置距离进入公海的时间 + if (customerVO.getOwnerUserId() != null) { + customerVO.setPoolDay(poolDayMap.get(customerVO.getId())); + } + }); + } + + @GetMapping("/put-pool-remind-page") + @Operation(summary = "获得待进入公海客户分页") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult> getPutPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) { + // 1. 查询客户分页 + PageResult pageResult = customerService.getPutPoolRemindCustomerPage(pageVO, getLoginUserId()); + // 2. 拼接数据 + return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/put-pool-remind-count") + @Operation(summary = "获得待进入公海客户数量") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult getPutPoolRemindCustomerCount() { + return success(customerService.getPutPoolRemindCustomerCount(getLoginUserId())); + } + + @GetMapping("/today-contact-count") + @Operation(summary = "获得今日需联系客户数量") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult getTodayContactCustomerCount() { + return success(customerService.getTodayContactCustomerCount(getLoginUserId())); + } + + @GetMapping("/follow-count") + @Operation(summary = "获得分配给我、待跟进的线索数量的客户数量") + @PreAuthorize("@ss.hasPermission('mes:customer:query')") + public CommonResult getFollowCustomerCount() { + return success(customerService.getFollowCustomerCount(getLoginUserId())); + } + + /** + * 获取距离进入公海的时间 Map + * + * @param list 客户列表 + * @return key 客户编号, value 距离进入公海的时间 + */ + private Map getPoolDayMap(List list) { + return MapUtil.empty(); + } + + @GetMapping(value = "/simple-list") + @Operation(summary = "获取客户精简信息列表", description = "只包含有读权限的客户,主要用于前端的下拉选项") + public CommonResult> getCustomerSimpleList() { + CrmCustomerPageReqVO reqVO = new CrmCustomerPageReqVO(); + reqVO.setPageSize(PAGE_SIZE_NONE); // 不分页 + List list = customerService.getCustomerPage(reqVO, getLoginUserId()).getList(); + return success(convertList(list, customer -> // 只返回 id、name 精简字段 + new CrmCustomerRespVO().setId(customer.getId()).setName(customer.getName()))); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出客户 Excel") + @PreAuthorize("@ss.hasPermission('mes:customer:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportCustomerExcel(@Valid CrmCustomerPageReqVO pageVO, + HttpServletResponse response) throws IOException { + pageVO.setPageSize(PAGE_SIZE_NONE); // 不分页 + List list = customerService.getCustomerPage(pageVO, getLoginUserId()).getList(); + // 导出 Excel + ExcelUtils.write(response, "客户.xls", "数据", CrmCustomerRespVO.class, + buildCustomerDetailList(list)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得导入客户模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + CrmCustomerImportExcelVO.builder().name("芋道").industryId(1).level(1).source(1) + .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn") + .areaId(null).detailAddress("").remark("").build(), + CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1) + .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn") + .areaId(null).detailAddress("").remark("").build() + ); + // 输出 + ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list); + } + + @PostMapping("/import") + @Operation(summary = "导入客户") + @PreAuthorize("@ss.hasPermission('mes:customer:import')") + public CommonResult importExcel(@Valid CrmCustomerImportReqVO importReqVO) + throws Exception { + List list = ExcelUtils.read(importReqVO.getFile(), CrmCustomerImportExcelVO.class); + return success(customerService.importCustomerList(list, importReqVO)); + } + + @PutMapping("/transfer") + @Operation(summary = "转移客户") + @PreAuthorize("@ss.hasPermission('mes:customer:update')") + public CommonResult transferCustomer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) { + customerService.transferCustomer(reqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/lock") + @Operation(summary = "锁定/解锁客户") + @PreAuthorize("@ss.hasPermission('mes:customer:update')") + public CommonResult lockCustomer(@Valid @RequestBody CrmCustomerLockReqVO lockReqVO) { + customerService.lockCustomer(lockReqVO, getLoginUserId()); + return success(true); + } + + // ==================== 公海相关操作 ==================== + + @PutMapping("/put-pool") + @Operation(summary = "数据放入公海") + @Parameter(name = "id", description = "客户编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mes:customer:update')") + public CommonResult putCustomerPool(@RequestParam("id") Long id) { + customerService.putCustomerPool(id); + return success(true); + } + + @PutMapping("/receive") + @Operation(summary = "领取公海客户") + @Parameter(name = "ids", description = "编号数组", required = true, example = "1,2,3") + @PreAuthorize("@ss.hasPermission('mes:customer:receive')") + public CommonResult receiveCustomer(@RequestParam(value = "ids") List ids) { + customerService.receiveCustomer(ids, getLoginUserId(), Boolean.TRUE); + return success(true); + } + + @PutMapping("/distribute") + @Operation(summary = "分配公海给对应负责人") + @PreAuthorize("@ss.hasPermission('mes:customer:distribute')") + public CommonResult distributeCustomer(@Valid @RequestBody CrmCustomerDistributeReqVO distributeReqVO) { + customerService.receiveCustomer(distributeReqVO.getIds(), distributeReqVO.getOwnerUserId(), Boolean.FALSE); + return success(true); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerDistributeReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerDistributeReqVO.java new file mode 100644 index 000000000..8d9691ee3 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerDistributeReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - CRM 客户分配公海给对应负责人 Request VO") +@Data +public class CrmCustomerDistributeReqVO { + + @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024]") + @NotEmpty(message = "客户编号不能为空") + private List ids; + + @Schema(description = "负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "负责人不能为空") + private Long ownerUserId; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java new file mode 100644 index 000000000..7d8f6b560 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; +import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.mes.framework.excel.core.AreaExcelColumnSelectFunction; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import static cn.iocoder.yudao.module.mes.enums.DictTypeConstants.*; + +/** + * 客户 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题 +public class CrmCustomerImportExcelVO { + + @ExcelProperty("客户名称") + private String name; + + @ExcelProperty("手机") + private String mobile; + + @ExcelProperty("电话") + private String telephone; + + @ExcelProperty("QQ") + private String qq; + + @ExcelProperty("微信") + private String wechat; + + @ExcelProperty("邮箱") + private String email; + + @ExcelProperty(value = "地区", converter = AreaConvert.class) + @ExcelColumnSelect(functionName = AreaExcelColumnSelectFunction.NAME) + private Integer areaId; + + @ExcelProperty("详细地址") + private String detailAddress; + + @ExcelProperty(value = "所属行业", converter = DictConvert.class) + @DictFormat(CRM_CUSTOMER_INDUSTRY) + @ExcelColumnSelect(dictType = CRM_CUSTOMER_INDUSTRY) + private Integer industryId; + + @ExcelProperty(value = "客户等级", converter = DictConvert.class) + @DictFormat(CRM_CUSTOMER_LEVEL) + @ExcelColumnSelect(dictType = CRM_CUSTOMER_LEVEL) + private Integer level; + + @ExcelProperty(value = "客户来源", converter = DictConvert.class) + @DictFormat(CRM_CUSTOMER_SOURCE) + @ExcelColumnSelect(dictType = CRM_CUSTOMER_SOURCE) + private Integer source; + + @ExcelProperty("备注") + private String remark; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportReqVO.java new file mode 100644 index 000000000..8d0b0acc7 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 客户导入 Request VO") +@Data +@Builder +public class CrmCustomerImportReqVO { + + @Schema(description = "Excel 文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "Excel 文件不能为空") + private MultipartFile file; + + @Schema(description = "是否支持更新", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否支持更新不能为空") + private Boolean updateSupport; + + @Schema(description = "负责人", example = "1") + private Long ownerUserId; // 为 null 则客户进入公海 + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportRespVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportRespVO.java new file mode 100644 index 000000000..443159119 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerImportRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 客户导入 Response VO") +@Data +@Builder +public class CrmCustomerImportRespVO { + + @Schema(description = "创建成功的客户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createCustomerNames; + + @Schema(description = "更新成功的客户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateCustomerNames; + + @Schema(description = "导入失败的客户集合,key 为客户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureCustomerNames; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerLockReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerLockReqVO.java new file mode 100644 index 000000000..199c45bfa --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerLockReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - CRM 客户锁定/解锁 Request VO") +@Data +public class CrmCustomerLockReqVO { + + @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + private Long id; + + @Schema(description = "客户锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Boolean lockStatus; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java new file mode 100644 index 000000000..f0d4df162 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.mes.enums.common.CrmSceneTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - CRM 客户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CrmCustomerPageReqVO extends PageParam { + + /** + * 联系状态 - 今日需联系 + */ + public static final int CONTACT_TODAY = 1; + /** + * 联系状态 - 已逾期 + */ + public static final int CONTACT_EXPIRED = 2; + /** + * 联系状态 - 已联系 + */ + public static final int CONTACT_ALREADY = 3; + + @Schema(description = "客户名称", example = "赵六") + private String name; + + @Schema(description = "手机", example = "18000000000") + private String mobile; + + @Schema(description = "所属行业", example = "1") + private Integer industryId; + + @Schema(description = "客户等级", example = "1") + private Integer level; + + @Schema(description = "客户来源", example = "1") + private Integer source; + + @Schema(description = "场景类型", example = "1") + @InEnum(CrmSceneTypeEnum.class) + private Integer sceneType; // 场景类型,为 null 时则表示全部 + + @Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean pool; // null 则表示为不是公海数据 + + @Schema(description = "联系状态", example = "1") + private Integer contactStatus; // backlog 查询条件 + + @Schema(description = "跟进状态", example = "true") + private Boolean followUpStatus; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerRespVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerRespVO.java new file mode 100644 index 000000000..20423e941 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerRespVO.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - CRM 客户 Response VO") +@Data +@ExcelIgnoreUnannotated +public class CrmCustomerRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty("客户名称") + private String name; + + @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "跟进状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean followUpStatus; + + @Schema(description = "最后跟进时间") + @ExcelProperty("最后跟进时间") + private LocalDateTime contactLastTime; + + @Schema(description = "最后跟进内容", example = "吃饭、睡觉、打逗逗") + @ExcelProperty("最后跟进内容") + private String contactLastContent; + + @Schema(description = "下次联系时间") + @ExcelProperty("下次联系时间") + private LocalDateTime contactNextTime; + + @Schema(description = "负责人的用户编号", example = "25682") + @ExcelProperty("负责人的用户编号") + private Long ownerUserId; + @Schema(description = "负责人名字", example = "25682") + @ExcelProperty("负责人名字") + private String ownerUserName; + @Schema(description = "负责人部门") + @ExcelProperty("负责人部门") + private String ownerUserDeptName; + + @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "锁定状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean lockStatus; + + @Schema(description = "成交状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "成交状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean dealStatus; + + @Schema(description = "手机", example = "25682") + @ExcelProperty("手机") + private String mobile; + + @Schema(description = "电话", example = "25682") + @ExcelProperty("电话") + private String telephone; + + @Schema(description = "QQ", example = "25682") + @ExcelProperty("QQ") + private String qq; + + @Schema(description = "wechat", example = "25682") + @ExcelProperty("wechat") + private String wechat; + + @Schema(description = "email", example = "25682") + @ExcelProperty("email") + private String email; + + @Schema(description = "地区编号", example = "1024") + @ExcelProperty("地区编号") + private Integer areaId; + @Schema(description = "地区名称", example = "北京市") + @ExcelProperty("地区名称") + private String areaName; + @Schema(description = "详细地址", example = "北京市成华大道") + @ExcelProperty("详细地址") + private String detailAddress; + + @Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "所属行业", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY) + private Integer industryId; + + @Schema(description = "客户等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "客户等级", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL) + private Integer level; + + @Schema(description = "客户来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @ExcelProperty(value = "客户来源", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE) + private Integer source; + + @Schema(description = "负责人的用户编号", example = "25682") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("更新时间") + private LocalDateTime updateTime; + + @Schema(description = "创建人", example = "1024") + @ExcelProperty("创建人") + private String creator; + @Schema(description = "创建人名字", example = "芋道源码") + @ExcelProperty("创建人名字") + private String creatorName; + + @Schema(description = "距离加入公海时间", example = "1") + private Long poolDay; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java new file mode 100644 index 000000000..8bcfc3013 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.framework.common.validation.Telephone; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.module.mes.enums.customer.CrmCustomerLevelEnum; +import cn.iocoder.yudao.module.mes.framework.operatelog.core.CrmCustomerIndustryParseFunction; +import cn.iocoder.yudao.module.mes.framework.operatelog.core.CrmCustomerLevelParseFunction; +import cn.iocoder.yudao.module.mes.framework.operatelog.core.CrmCustomerSourceParseFunction; +import cn.iocoder.yudao.module.mes.framework.operatelog.core.SysAreaParseFunction; +import com.mzt.logapi.starter.annotation.DiffLogField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY; + +@Schema(description = "管理后台 - CRM 客户新增/修改 Request VO") +@Data +public class CrmCustomerSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + private Long id; + + @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @DiffLogField(name = "客户名称") + @NotEmpty(message = "客户名称不能为空") + private String name; + + @Schema(description = "下次联系时间") + @DiffLogField(name = "下次联系时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime contactNextTime; + + @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563") + @NotNull(message = "负责人的用户编号不能为空") + private Long ownerUserId; + + @Schema(description = "手机", example = "18000000000") + @DiffLogField(name = "手机") + @Mobile + private String mobile; + + @Schema(description = "电话", example = "18000000000") + @DiffLogField(name = "电话") + @Telephone + private String telephone; + + @Schema(description = "QQ", example = "123456789") + @DiffLogField(name = "QQ") + @Size(max = 20, message = "QQ长度不能超过 20 个字符") + private String qq; + + @Schema(description = "微信", example = "123456789") + @DiffLogField(name = "微信") + @Size(max = 255, message = "微信长度不能超过 255 个字符") + private String wechat; + + @Schema(description = "邮箱", example = "123456789@qq.com") + @DiffLogField(name = "邮箱") + @Email(message = "邮箱格式不正确") + @Size(max = 255, message = "邮箱长度不能超过 255 个字符") + private String email; + + @Schema(description = "地区编号", example = "20158") + @DiffLogField(name = "地区编号", function = SysAreaParseFunction.NAME) + private Integer areaId; + + @Schema(description = "详细地址", example = "北京市海淀区") + @DiffLogField(name = "详细地址") + private String detailAddress; + + @Schema(description = "所属行业", example = "1") + @DiffLogField(name = "所属行业", function = CrmCustomerIndustryParseFunction.NAME) + @DictFormat(CRM_CUSTOMER_INDUSTRY) + private Integer industryId; + + @Schema(description = "客户等级", example = "2") + @DiffLogField(name = "客户等级", function = CrmCustomerLevelParseFunction.NAME) + @InEnum(CrmCustomerLevelEnum.class) + private Integer level; + + @Schema(description = "客户来源", example = "3") + @DiffLogField(name = "客户来源", function = CrmCustomerSourceParseFunction.NAME) + private Integer source; + + @Schema(description = "备注", example = "随便") + @DiffLogField(name = "备注") + private String remark; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java new file mode 100644 index 000000000..59641919d --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer; + +import cn.iocoder.yudao.module.mes.enums.permission.CrmPermissionLevelEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - CRM 客户转移 Request VO") +@Data +public class CrmCustomerTransferReqVO { + + @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430") + @NotNull(message = "客户编号不能为空") + private Long id; + + /** + * 新负责人的用户编号 + */ + @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430") + @NotNull(message = "新负责人的用户编号不能为空") + private Long newOwnerUserId; + + /** + * 老负责人加入团队后的权限级别。如果 null 说明移除 + * + * 关联 {@link CrmPermissionLevelEnum} + */ + @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer oldOwnerPermissionLevel; + + /** + * 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择。选中时,也一起转移 + */ + @Schema(description = "同时转移", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430") + private List toBizTypes; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/customer/CrmCustomerDO.java new file mode 100644 index 000000000..2100cf788 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/customer/CrmCustomerDO.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.module.mes.dal.dataobject.customer; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.mes.enums.DictTypeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * CRM 客户 DO + * + * @author Wanwan + */ +@TableName(value = "crm_customer") +@KeySequence("crm_customer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CrmCustomerDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 客户名称 + */ + private String name; + + /** + * 跟进状态 + */ + private Boolean followUpStatus; + /** + * 最后跟进时间 + */ + private LocalDateTime contactLastTime; + /** + * 最后跟进内容 + */ + private String contactLastContent; + /** + * 下次联系时间 + */ + private LocalDateTime contactNextTime; + + /** + * 负责人的用户编号 + * + * 关联 AdminUserDO 的 id 字段 + */ + private Long ownerUserId; + /** + * 成为负责人的时间 + */ + private LocalDateTime ownerTime; + + /** + * 锁定状态 + */ + private Boolean lockStatus; + /** + * 成交状态 + */ + private Boolean dealStatus; + + /** + * 手机 + */ + private String mobile; + /** + * 电话 + */ + private String telephone; + /** + * QQ + */ + private String qq; + /** + * wechat + */ + private String wechat; + /** + * email + */ + private String email; + /** + * 所在地 + * + * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} 字段 + */ + private Integer areaId; + /** + * 详细地址 + */ + private String detailAddress; + /** + * 所属行业 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_INDUSTRY} + */ + private Integer industryId; + /** + * 客户等级 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_LEVEL} + */ + private Integer level; + /** + * 客户来源 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_SOURCE} + */ + private Integer source; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/customer/CrmCustomerMapper.java new file mode 100644 index 000000000..ea0146648 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/customer/CrmCustomerMapper.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.mes.dal.mysql.customer; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ObjUtil; +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.mes.controller.admin.customer.vo.customer.CrmCustomerPageReqVO; +import cn.iocoder.yudao.module.mes.dal.dataobject.customer.CrmCustomerDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.Assert; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface CrmCustomerMapper extends BaseMapperX { + + default CrmCustomerDO selectByCustomerName(String name) { + return selectOne(CrmCustomerDO::getName, name); + } + + default List selectListByIds(Collection ids) { + return selectList(CrmCustomerDO::getId, ids); + } + + default int updateOwnerUserIdById(Long id, Long ownerUserId) { + return update(new LambdaUpdateWrapper() + .eq(CrmCustomerDO::getId, id) + .set(CrmCustomerDO::getOwnerUserId, ownerUserId) + .set(CrmCustomerDO::getOwnerTime, LocalDateTime.now())); + } + + default PageResult selectPage(CrmCustomerPageReqVO pageReqVO) { + LambdaQueryWrapperX query = buildQuery(pageReqVO) + .orderByDesc(CrmCustomerDO::getId); + return selectPage(pageReqVO, query); + } + + default Long selectCountByTodayContact(Long ownerUserId) { + LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); + LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now()); + return selectCount(new LambdaQueryWrapperX() + .eq(CrmCustomerDO::getOwnerUserId, ownerUserId) + .between(CrmCustomerDO::getContactNextTime, beginOfToday, endOfToday)); + } + + default Long selectCountByFollow(Long ownerUserId) { + return selectCount(new LambdaQueryWrapperX() + .eq(CrmCustomerDO::getOwnerUserId, ownerUserId) + .eq(CrmCustomerDO::getFollowUpStatus, false)); + } + + default Long selectCountByPutPoolRemind(CrmCustomerPageReqVO pageReqVO, Long ownerUserId) { + return selectCount(buildQuery(pageReqVO).eq(CrmCustomerDO::getOwnerUserId, ownerUserId)); + } + + default PageResult selectPutPoolRemindPage(CrmCustomerPageReqVO pageReqVO, Long ownerUserId) { + return selectPage(pageReqVO, buildQuery(pageReqVO).eq(CrmCustomerDO::getOwnerUserId, ownerUserId) + .orderByDesc(CrmCustomerDO::getId)); + } + + default List selectListByAutoPool() { + return selectList(new LambdaQueryWrapperX() + .isNotNull(CrmCustomerDO::getOwnerUserId) + .eq(CrmCustomerDO::getLockStatus, false) + .eq(CrmCustomerDO::getDealStatus, false)); + } + + static LambdaQueryWrapperX buildQuery(CrmCustomerPageReqVO pageReqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName()) + .eqIfPresent(CrmCustomerDO::getMobile, pageReqVO.getMobile()) + .eqIfPresent(CrmCustomerDO::getIndustryId, pageReqVO.getIndustryId()) + .eqIfPresent(CrmCustomerDO::getLevel, pageReqVO.getLevel()) + .eqIfPresent(CrmCustomerDO::getSource, pageReqVO.getSource()) + .eqIfPresent(CrmCustomerDO::getFollowUpStatus, pageReqVO.getFollowUpStatus()); + + if (pageReqVO.getPool() != null) { + if (Boolean.TRUE.equals(pageReqVO.getPool())) { + query.isNull(CrmCustomerDO::getOwnerUserId); + } else { + query.isNotNull(CrmCustomerDO::getOwnerUserId); + } + } + if (ObjUtil.isNotNull(pageReqVO.getContactStatus())) { + Assert.isNull(pageReqVO.getPool(), "pool 必须是 null"); + LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); + LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now()); + if (pageReqVO.getContactStatus().equals(CrmCustomerPageReqVO.CONTACT_TODAY)) { + query.between(CrmCustomerDO::getContactNextTime, beginOfToday, endOfToday); + } else if (pageReqVO.getContactStatus().equals(CrmCustomerPageReqVO.CONTACT_EXPIRED)) { + query.lt(CrmCustomerDO::getContactNextTime, beginOfToday); + } else if (pageReqVO.getContactStatus().equals(CrmCustomerPageReqVO.CONTACT_ALREADY)) { + query.between(CrmCustomerDO::getContactLastTime, beginOfToday, endOfToday); + } else { + throw new IllegalArgumentException("未知联系状态:" + pageReqVO.getContactStatus()); + } + } + return query; + } +} \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java new file mode 100644 index 000000000..ac25c34b7 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.mes.enums; + +/** + * CRM 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String CRM_CUSTOMER_INDUSTRY = "crm_customer_industry"; // CRM 客户所属行业 + String CRM_CUSTOMER_LEVEL = "crm_customer_level"; // CRM 客户等级 + String CRM_CUSTOMER_SOURCE = "crm_customer_source"; // CRM 客户来源 + String CRM_AUDIT_STATUS = "crm_audit_status"; // CRM 审批状态 + 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 回款方式 + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/common/CrmSceneTypeEnum.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/common/CrmSceneTypeEnum.java new file mode 100644 index 000000000..4a6085d28 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/common/CrmSceneTypeEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.mes.enums.common; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * CRM 列表检索场景 + * + * @author HUIHUI + */ +@Getter +@AllArgsConstructor +public enum CrmSceneTypeEnum implements IntArrayValuable { + + OWNER(1, "我负责的"), + INVOLVED(2, "我参与的"), + SUBORDINATE(3, "下属负责的"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmSceneTypeEnum::getType).toArray(); + + /** + * 场景类型 + */ + private final Integer type; + /** + * 场景名称 + */ + private final String name; + + public static boolean isOwner(Integer type) { + return ObjUtil.equal(OWNER.getType(), type); + } + + public static boolean isInvolved(Integer type) { + return ObjUtil.equal(INVOLVED.getType(), type); + } + + public static boolean isSubordinate(Integer type) { + return ObjUtil.equal(SUBORDINATE.getType(), type); + } + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/customer/CrmCustomerLevelEnum.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/customer/CrmCustomerLevelEnum.java new file mode 100644 index 000000000..8a6c4068f --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/customer/CrmCustomerLevelEnum.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.mes.enums.customer; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * CRM 客户等级 + * + * @author Wanwan + */ +@Getter +@AllArgsConstructor +public enum CrmCustomerLevelEnum implements IntArrayValuable { + + IMPORTANT(1, "A(重点客户)"), + GENERAL(2, "B(普通客户)"), + LOW_PRIORITY(3, "C(非优先客户)"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getLevel).toArray(); + + /** + * 状态 + */ + private final Integer level; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/permission/CrmPermissionLevelEnum.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/permission/CrmPermissionLevelEnum.java new file mode 100644 index 000000000..b469de627 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/enums/permission/CrmPermissionLevelEnum.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.mes.enums.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * CRM 数据权限级别枚举 + * + * OWNER > WRITE > READ + * + * @author HUIHUI + */ +@Getter +@AllArgsConstructor +public enum CrmPermissionLevelEnum implements IntArrayValuable { + + OWNER(1, "负责人"), + READ(2, "只读"), + WRITE(3, "读写"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmPermissionLevelEnum::getLevel).toArray(); + + /** + * 级别 + */ + private final Integer level; + /** + * 级别名称 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + + public static boolean isOwner(Integer level) { + return ObjUtil.equal(OWNER.level, level); + } + + public static boolean isRead(Integer level) { + return ObjUtil.equal(READ.level, level); + } + + public static boolean isWrite(Integer level) { + return ObjUtil.equal(WRITE.level, level); + } + + public static String getNameByLevel(Integer level) { + CrmPermissionLevelEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmPermissionLevelEnum.values()), + item -> ObjUtil.equal(item.level, level)); + return typeEnum == null ? null : typeEnum.getName(); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/excel/core/AreaExcelColumnSelectFunction.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/excel/core/AreaExcelColumnSelectFunction.java new file mode 100644 index 000000000..9b8860e3c --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/excel/core/AreaExcelColumnSelectFunction.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.mes.framework.excel.core; + +import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; +import cn.iocoder.yudao.framework.ip.core.Area; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 地区下拉框数据源的 {@link ExcelColumnSelectFunction} 实现类 + * + * @author HUIHUI + */ +@Service +public class AreaExcelColumnSelectFunction implements ExcelColumnSelectFunction { + + public static final String NAME = "getCrmAreaNameList"; // 防止和别的模块重名 + + @Override + public String getName() { + return NAME; + } + + @Override + public List getOptions() { + // 获取地区下拉数据 + // TODO @puhui999:嘿嘿,这里改成省份、城市、区域,三个选项,难度大么? + Area area = AreaUtils.getArea(Area.ID_CHINA); + return AreaUtils.getAreaNodePathList(area.getChildren()); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerIndustryParseFunction.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerIndustryParseFunction.java new file mode 100644 index 000000000..1b6afe8cc --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerIndustryParseFunction.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.mes.framework.operatelog.core; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import com.mzt.logapi.service.IParseFunction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY; + +/** + * 行业的 {@link IParseFunction} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class CrmCustomerIndustryParseFunction implements IParseFunction { + + public static final String NAME = "getCustomerIndustry"; + + @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_CUSTOMER_INDUSTRY, value.toString()); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerLevelParseFunction.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerLevelParseFunction.java new file mode 100644 index 000000000..05eb8f53d --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerLevelParseFunction.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.mes.framework.operatelog.core; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import com.mzt.logapi.service.IParseFunction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL; + +/** + * 客户等级的 {@link IParseFunction} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class CrmCustomerLevelParseFunction implements IParseFunction { + + public static final String NAME = "getCustomerLevel"; + + @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_CUSTOMER_LEVEL, value.toString()); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerSourceParseFunction.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerSourceParseFunction.java new file mode 100644 index 000000000..0a02270b1 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/CrmCustomerSourceParseFunction.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.mes.framework.operatelog.core; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import com.mzt.logapi.service.IParseFunction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.module.mes.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE; + +/** + * CRM 客户来源的 {@link IParseFunction} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class CrmCustomerSourceParseFunction implements IParseFunction { + + public static final String NAME = "getCustomerSource"; + + @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_CUSTOMER_SOURCE, value.toString()); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/SysAreaParseFunction.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/SysAreaParseFunction.java new file mode 100644 index 000000000..63305191e --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/framework/operatelog/core/SysAreaParseFunction.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.mes.framework.operatelog.core; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import com.mzt.logapi.service.IParseFunction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 地名的 {@link IParseFunction} 实现类 + * + * @author HUIHUI + */ +@Slf4j +@Component +public class SysAreaParseFunction implements IParseFunction { + + public static final String NAME = "getArea"; + + @Override + public boolean executeBefore() { + return true; // 先转换值后对比 + } + + @Override + public String functionName() { + return NAME; + } + + @Override + public String apply(Object value) { + if (StrUtil.isEmptyIfStr(value)) { + return ""; + } + return AreaUtils.format(Integer.parseInt(value.toString())); + } + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerService.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerService.java new file mode 100644 index 000000000..5c876b467 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerService.java @@ -0,0 +1,200 @@ +package cn.iocoder.yudao.module.mes.service.customer; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer.*; +import cn.iocoder.yudao.module.mes.dal.dataobject.customer.CrmCustomerDO; +import cn.iocoder.yudao.module.mes.service.customer.bo.CrmCustomerCreateReqBO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 客户 Service 接口 + * + * @author Wanwan + */ +public interface CrmCustomerService { + + /** + * 创建客户 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 编号 + */ + Long createCustomer(@Valid CrmCustomerSaveReqVO createReqVO, Long userId); + + /** + * 更新客户 + * + * @param updateReqVO 更新信息 + */ + void updateCustomer(@Valid CrmCustomerSaveReqVO updateReqVO); + + /** + * 更新客户的跟进状态 + * + * @param id 编号 + * @param dealStatus 跟进状态 + */ + void updateCustomerDealStatus(Long id, Boolean dealStatus); + + /** + * 更新客户相关的跟进信息 + * + * @param id 编号 + * @param contactNextTime 下次联系时间 + * @param contactLastContent 最后联系内容 + */ + void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent); + + /** + * 删除客户 + * + * @param id 编号 + */ + void deleteCustomer(Long id); + + /** + * 获得客户 + * + * @param id 编号 + * @return 客户 + */ + CrmCustomerDO getCustomer(Long id); + + /** + * 获得客户列表 + * + * @param ids 客户编号数组 + * @return 客户列表 + * @author ljlleo + */ + List getCustomerList(Collection ids); + + /** + * 获得客户 Map + * + * @param ids 客户编号数组 + * @return 客户 Map + */ + default Map getCustomerMap(Collection ids) { + if(ids.isEmpty())return new HashMap<>(); + return convertMap(getCustomerList(ids), CrmCustomerDO::getId); + } + + /** + * 获得客户分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 客户分页 + */ + PageResult getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId); + + /** + * 获得放入公海提醒的客户分页 + * + * @param pageVO 分页查询 + * @param userId 用户编号 + * @return 客户分页 + */ + PageResult getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId); + + /** + * 获得待进入公海的客户数量 + * + * @param userId 用户编号 + * @return 提醒数量 + */ + Long getPutPoolRemindCustomerCount(Long userId); + + /** + * 获得今日需联系客户数量 + * + * @param userId 用户编号 + * @return 提醒数量 + */ + Long getTodayContactCustomerCount(Long userId); + + /** + * 获得分配给我的客户数量 + * + * @param userId 用户编号 + * @return 提醒数量 + */ + Long getFollowCustomerCount(Long userId); + + /** + * 校验客户是否存在 + * + * @param id 编号 + */ + void validateCustomer(Long id); + + /** + * 客户转移 + * + * @param reqVO 请求 + * @param userId 用户编号 + */ + void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId); + + /** + * 锁定/解锁客户 + * + * @param lockReqVO 更新信息 + * @param userId 用户编号 + */ + void lockCustomer(@Valid CrmCustomerLockReqVO lockReqVO, Long userId); + + /** + * 创建客户 + * + * @param customerCreateReq 请求信息 + * @param userId 用户编号 + * @return 客户列表 + */ + Long createCustomer(CrmCustomerCreateReqBO customerCreateReq, Long userId); + + /** + * 批量导入客户 + * + * @param importCustomers 导入客户列表 + * @param importReqVO 请求 + * @return 导入结果 + */ + CrmCustomerImportRespVO importCustomerList(List importCustomers, CrmCustomerImportReqVO importReqVO); + + // ==================== 公海相关操作 ==================== + + /** + * 客户放入公海 + * + * @param id 客户编号 + */ + void putCustomerPool(Long id); + + /** + * 领取公海客户 + * + * @param ids 要领取的客户编号数组 + * @param ownerUserId 负责人 + * @param isReceive 是/否领取;true - 领取;false - 分配 + */ + void receiveCustomer(List ids, Long ownerUserId, Boolean isReceive); + + /** + * 【系统】客户自动掉入公海 + * + * @return 掉入公海数量 + */ + int autoPutCustomerPool(); + +} diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerServiceImpl.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerServiceImpl.java new file mode 100644 index 000000000..cd8a18f87 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/CrmCustomerServiceImpl.java @@ -0,0 +1,249 @@ +package cn.iocoder.yudao.module.mes.service.customer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.mes.controller.admin.customer.vo.customer.*; +import cn.iocoder.yudao.module.mes.dal.dataobject.customer.CrmCustomerDO; +import cn.iocoder.yudao.module.mes.dal.mysql.customer.CrmCustomerMapper; +import cn.iocoder.yudao.module.mes.service.customer.bo.CrmCustomerCreateReqBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; + +@Service +@Slf4j +@Validated +public class CrmCustomerServiceImpl implements CrmCustomerService { + + @Resource + private CrmCustomerMapper customerMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCustomer(CrmCustomerSaveReqVO createReqVO, Long userId) { + createReqVO.setId(null); + validateCustomerForCreate(createReqVO.getName()); + CrmCustomerDO customer = initCustomer(createReqVO, userId); + customerMapper.insert(customer); + return customer.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCustomer(CrmCustomerSaveReqVO updateReqVO) { + validateCustomerExists(updateReqVO.getId()); + validateCustomerNameUnique(updateReqVO.getName(), updateReqVO.getId()); + CrmCustomerDO updateObj = BeanUtils.toBean(updateReqVO, CrmCustomerDO.class); + if (updateObj.getOwnerUserId() != null) { + updateObj.setOwnerTime(LocalDateTime.now()); + } + customerMapper.updateById(updateObj); + } + + @Override + public void updateCustomerDealStatus(Long id, Boolean dealStatus) { + CrmCustomerDO customer = validateCustomerExists(id); + if (Objects.equals(customer.getDealStatus(), dealStatus)) { + throw exception(CUSTOMER_UPDATE_DEAL_STATUS_FAIL); + } + customerMapper.updateById(new CrmCustomerDO().setId(id).setDealStatus(dealStatus)); + } + + @Override + public void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) { + validateCustomerExists(id); + customerMapper.updateById(new CrmCustomerDO().setId(id) + .setContactNextTime(contactNextTime) + .setContactLastTime(LocalDateTime.now()) + .setContactLastContent(contactLastContent) + .setFollowUpStatus(true)); + } + + @Override + public void deleteCustomer(Long id) { + validateCustomerExists(id); + customerMapper.deleteById(id); + } + + @Override + public CrmCustomerDO getCustomer(Long id) { + return customerMapper.selectById(id); + } + + @Override + public List getCustomerList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return customerMapper.selectListByIds(ids); + } + + @Override + public PageResult getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId) { + return customerMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId) { + return customerMapper.selectPutPoolRemindPage(pageVO, userId); + } + + @Override + public Long getPutPoolRemindCustomerCount(Long userId) { + return customerMapper.selectCountByPutPoolRemind(new CrmCustomerPageReqVO(), userId); + } + + @Override + public Long getTodayContactCustomerCount(Long userId) { + return customerMapper.selectCountByTodayContact(userId); + } + + @Override + public Long getFollowCustomerCount(Long userId) { + return customerMapper.selectCountByFollow(userId); + } + + @Override + public void validateCustomer(Long id) { + validateCustomerExists(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId) { + validateCustomerExists(reqVO.getId()); + customerMapper.updateById(new CrmCustomerDO().setId(reqVO.getId()) + .setOwnerUserId(reqVO.getNewOwnerUserId()) + .setOwnerTime(LocalDateTime.now())); + } + + @Override + public void lockCustomer(CrmCustomerLockReqVO lockReqVO, Long userId) { + CrmCustomerDO customer = validateCustomerExists(lockReqVO.getId()); + if (Objects.equals(customer.getLockStatus(), lockReqVO.getLockStatus())) { + throw exception(Boolean.TRUE.equals(lockReqVO.getLockStatus()) ? CUSTOMER_LOCK_FAIL_IS_LOCK : CUSTOMER_UNLOCK_FAIL_IS_UNLOCK); + } + customerMapper.updateById(new CrmCustomerDO().setId(lockReqVO.getId()).setLockStatus(lockReqVO.getLockStatus())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCustomer(CrmCustomerCreateReqBO createReqBO, Long userId) { + validateCustomerForCreate(createReqBO.getName()); + CrmCustomerDO customer = initCustomer(createReqBO, userId); + customerMapper.insert(customer); + return customer.getId(); + } + + @Override + public CrmCustomerImportRespVO importCustomerList(List importCustomers, + CrmCustomerImportReqVO importReqVO) { + importCustomers = CollectionUtils.filterList(importCustomers, item -> Objects.nonNull(item.getName())); + if (CollUtil.isEmpty(importCustomers)) { + throw exception(CUSTOMER_IMPORT_LIST_IS_EMPTY); + } + CrmCustomerImportRespVO respVO = CrmCustomerImportRespVO.builder() + .createCustomerNames(new ArrayList<>()) + .updateCustomerNames(new ArrayList<>()) + .failureCustomerNames(new LinkedHashMap<>()) + .build(); + for (CrmCustomerImportExcelVO importCustomer : importCustomers) { + try { + validateCustomerForCreate(importCustomer.getName()); + } catch (ServiceException ex) { + CrmCustomerDO existCustomer = customerMapper.selectByCustomerName(importCustomer.getName()); + if (existCustomer == null || !Boolean.TRUE.equals(importReqVO.getUpdateSupport())) { + respVO.getFailureCustomerNames().put(importCustomer.getName(), ex.getMessage()); + continue; + } + CrmCustomerDO updateCustomer = BeanUtils.toBean(importCustomer, CrmCustomerDO.class) + .setId(existCustomer.getId()); + customerMapper.updateById(updateCustomer); + respVO.getUpdateCustomerNames().add(importCustomer.getName()); + continue; + } + CrmCustomerDO customer = initCustomer(importCustomer, importReqVO.getOwnerUserId()); + customerMapper.insert(customer); + respVO.getCreateCustomerNames().add(importCustomer.getName()); + } + return respVO; + } + + @Override + public void putCustomerPool(Long id) { + CrmCustomerDO customer = validateCustomerExists(id); + if (customer.getOwnerUserId() == null) { + throw exception(CUSTOMER_IN_POOL, customer.getName()); + } + if (Boolean.TRUE.equals(customer.getLockStatus())) { + throw exception(CUSTOMER_LOCKED_PUT_POOL_FAIL, customer.getName()); + } + customerMapper.updateById(new CrmCustomerDO().setId(id).setOwnerUserId(null).setOwnerTime(null)); + } + + @Override + public void receiveCustomer(List ids, Long ownerUserId, Boolean isReceive) { + if (CollUtil.isEmpty(ids)) { + return; + } + for (Long id : ids) { + validateCustomerExists(id); + customerMapper.updateById(new CrmCustomerDO().setId(id) + .setOwnerUserId(ownerUserId) + .setOwnerTime(LocalDateTime.now())); + } + } + + @Override + public int autoPutCustomerPool() { + List customers = customerMapper.selectListByAutoPool(); + for (CrmCustomerDO customer : customers) { + putCustomerPool(customer.getId()); + } + return customers.size(); + } + + private CrmCustomerDO initCustomer(Object source, Long userId) { + CrmCustomerDO customer = BeanUtils.toBean(source, CrmCustomerDO.class); + customer.setOwnerUserId(customer.getOwnerUserId() != null ? customer.getOwnerUserId() : userId); + customer.setOwnerTime(customer.getOwnerUserId() != null ? LocalDateTime.now() : null); + customer.setFollowUpStatus(Boolean.FALSE); + customer.setLockStatus(Boolean.FALSE); + customer.setDealStatus(Boolean.FALSE); + return customer; + } + + private CrmCustomerDO validateCustomerExists(Long id) { + CrmCustomerDO customer = customerMapper.selectById(id); + if (customer == null) { + throw exception(CUSTOMER_NOT_EXISTS); + } + return customer; + } + + private void validateCustomerForCreate(String name) { + if (StrUtil.isBlank(name)) { + throw exception(CUSTOMER_CREATE_NAME_NOT_NULL); + } + validateCustomerNameUnique(name, null); + } + + private void validateCustomerNameUnique(String name, Long id) { + CrmCustomerDO customer = customerMapper.selectByCustomerName(name); + if (customer != null && !Objects.equals(customer.getId(), id)) { + throw exception(CUSTOMER_NAME_EXISTS, name); + } + } +} \ No newline at end of file diff --git a/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/bo/CrmCustomerCreateReqBO.java b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/bo/CrmCustomerCreateReqBO.java new file mode 100644 index 000000000..d046fb503 --- /dev/null +++ b/yudao-module-mes/yudao-module-mes-biz/src/main/java/cn/iocoder/yudao/module/mes/service/customer/bo/CrmCustomerCreateReqBO.java @@ -0,0 +1,121 @@ +package cn.iocoder.yudao.module.mes.service.customer.bo; + +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.framework.common.validation.Telephone; +import cn.iocoder.yudao.module.mes.enums.DictTypeConstants; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 客户创建 Create Req BO + * + * @author HUIHUI + */ +@Data +public class CrmCustomerCreateReqBO { + + /** + * 客户名称 + */ + @NotEmpty(message = "客户名称不能为空") + private String name; + /** + * 跟进状态 + */ + private Boolean followUpStatus; + /** + * 锁定状态 + */ + private Boolean lockStatus; + /** + * 成交状态 + */ + private Boolean dealStatus; + /** + * 所属行业 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_INDUSTRY} + */ + private Integer industryId; + /** + * 客户等级 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_LEVEL} + */ + private Integer level; + /** + * 客户来源 + * + * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_SOURCE} + */ + private Integer source; + + /** + * 手机 + */ + @Mobile + private String mobile; + /** + * 电话 + */ + @Telephone + private String telephone; + /** + * QQ + */ + private String qq; + /** + * wechat + */ + private String wechat; + + /** + * 邮箱 + */ + @Email(message = "邮箱格式不正确") + private String email; + + /** + * 客户描述 + */ + @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符") + private String description; + /** + * 备注 + */ + private String remark; + /** + * 负责人的用户编号 + * + * 关联 AdminUserDO 的 id 字段 + */ + private Long ownerUserId; + /** + * 所在地 + * + * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} 字段 + */ + private Integer areaId; + /** + * 详细地址 + */ + private String detailAddress; + + /** + * 最后跟进时间 + */ + private LocalDateTime contactLastTime; + /** + * 最后跟进内容 + */ + private String contactLastContent; + /** + * 下次联系时间 + */ + private LocalDateTime contactNextTime; + +}