Bläddra i källkod

CRM-客户: 完善客户导入

puhui999 1 år sedan
förälder
incheckning
4605e93105

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

@@ -26,8 +26,8 @@ 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_BUSINESS_LINK_NOT_EXISTS = new ErrorCode(1_020_003_001, "联系人商机关联不存在");
+    ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode(1_020_003_002, "联系人已关联合同,不能删除");
 
     // ========== 回款 1-020-004-000 ==========
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款不存在");
@@ -48,6 +48,9 @@ public interface ErrorCodeConstants {
     ErrorCode CUSTOMER_LOCK_EXCEED_LIMIT = new ErrorCode(1_020_006_009, "锁定客户失败,超出锁定规则上限");
     ErrorCode CUSTOMER_OWNER_EXCEED_LIMIT = new ErrorCode(1_020_006_010, "操作失败,超出客户数拥有上限");
     ErrorCode CUSTOMER_DELETE_FAIL_HAVE_REFERENCE = new ErrorCode(1_020_006_011, "删除客户失败,有关联{}");
+    ErrorCode CUSTOMER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_020_006_012, "导入客户数据不能为空!");
+    ErrorCode CUSTOMER_CREATE_NAME_NOT_NULL = new ErrorCode(1_020_006_013, "客户名称不能为空!");
+    ErrorCode CUSTOMER_NAME_EXISTS = new ErrorCode(1_020_006_014, "已存在名为【{}】的客户!");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");

+ 33 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java

@@ -21,6 +21,7 @@ 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 jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletResponse;
@@ -29,9 +30,11 @@ import org.mapstruct.ap.internal.util.Collections;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 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;
@@ -169,6 +172,36 @@ public class CrmCustomerController {
                 BeanUtils.toBean(list, CrmCustomerRespVO.class));
     }
 
+    @GetMapping("/get-import-template")
+    @Operation(summary = "获得导入客户模板")
+    public void importTemplate(HttpServletResponse response) throws IOException {
+        // 手动创建导出 demo
+        List<CrmCustomerImportExcelVO> list = Arrays.asList(
+                CrmCustomerImportExcelVO.builder().name("芋道").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
+                        .website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
+                        .areaId(null).detailAddress("").build(),
+                CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
+                        .website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
+                        .areaId(null).detailAddress("").build()
+        );
+        // 输出
+        ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
+    }
+
+    @PostMapping("/import")
+    @Operation(summary = "导入客户")
+    @Parameters({
+            @Parameter(name = "file", description = "Excel 文件", required = true),
+            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
+    })
+    @PreAuthorize("@ss.hasPermission('system:customer:import')")
+    public CommonResult<CrmCustomerImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
+                                                             @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
+        List<CrmCustomerImportExcelVO> list = ExcelUtils.read(file, CrmCustomerImportExcelVO.class);
+        return success(customerService.importCustomerList(list, updateSupport));
+    }
+
+
     @PutMapping("/transfer")
     @Operation(summary = "转移客户")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")

+ 74 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerImportExcelVO.java

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelProperty;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
+
+/**
+ * 客户 Excel 导入 VO
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
+public class CrmCustomerImportExcelVO {
+
+    @ExcelProperty("客户名称")
+    private String name;
+
+    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
+
+    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_LEVEL)
+    private Integer level;
+
+    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_SOURCE)
+    private Integer source;
+
+    @ExcelProperty("手机")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("网址")
+    private String website;
+
+    @Size(max = 20, message = "QQ长度不能超过 20 个字符")
+    @ExcelProperty("QQ")
+    private String qq;
+
+    @Size(max = 255, message = "微信长度不能超过 255 个字符")
+    @ExcelProperty("微信")
+    private String wechat;
+
+    @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
+    @ExcelProperty("邮箱")
+    private String email;
+
+    @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
+    @ExcelProperty("客户描述")
+    private String description;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("地区编号")
+    private Integer areaId;
+
+    @ExcelProperty("详细地址")
+    private String detailAddress;
+
+}

+ 24 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerImportRespVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+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<String> createCustomerNames;
+
+    @Schema(description = "更新成功的客户名数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> updateCustomerNames;
+
+    @Schema(description = "导入失败的客户集合,key 为客户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Map<String, String> failureCustomerNames;
+
+}

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java

@@ -107,4 +107,8 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
                 .gt(CrmCustomerDO::getOwnerUserId, 0));
     }
 
+    default CrmCustomerDO selectByCustomerName(String name) {
+        return selectOne(CrmCustomerDO::getName, name);
+    }
+
 }

+ 10 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java

@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLockReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerSaveReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerTransferReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
@@ -108,6 +105,15 @@ public interface CrmCustomerService {
      */
     Long createCustomer(CrmCustomerCreateReqBO customerCreateReq, Long userId);
 
+    /**
+     * 批量导入客户
+     *
+     * @param importCustomers 导入客户列表
+     * @param isUpdateSupport 是否支持更新
+     * @return 导入结果
+     */
+    CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, Boolean isUpdateSupport);
+
     // ==================== 公海相关操作 ====================
 
     /**

+ 48 - 9
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java

@@ -3,15 +3,14 @@ package cn.iocoder.yudao.module.crm.service.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLockReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerSaveReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerTransferReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
@@ -41,10 +40,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
@@ -236,7 +232,50 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customer.getId();
     }
 
-// ==================== 公海相关操作 ====================
+    @Override
+    public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, Boolean isUpdateSupport) {
+        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();
+        importCustomers.forEach(importCustomer -> {
+            // 校验,判断是否有不符合的原因
+            try {
+                validateCustomerForCreate(importCustomer);
+            } catch (ServiceException ex) {
+                respVO.getFailureCustomerNames().put(importCustomer.getName(), ex.getMessage());
+                return;
+            }
+            // 判断如果不存在,在进行插入
+            CrmCustomerDO existCustomer = customerMapper.selectByCustomerName(importCustomer.getName());
+            if (existCustomer == null) {
+                customerMapper.insert(BeanUtils.toBean(importCustomer, CrmCustomerDO.class));
+                respVO.getCreateCustomerNames().add(importCustomer.getName());
+                return;
+            }
+            // 如果存在,判断是否允许更新
+            if (!isUpdateSupport) {
+                respVO.getFailureCustomerNames().put(importCustomer.getName(),
+                        StrUtil.format(CUSTOMER_NAME_EXISTS.getMsg(), importCustomer.getName()));
+                return;
+            }
+            CrmCustomerDO updateCustomer = BeanUtils.toBean(importCustomer, CrmCustomerDO.class);
+            updateCustomer.setId(existCustomer.getId());
+            customerMapper.updateById(updateCustomer);
+            respVO.getUpdateCustomerNames().add(importCustomer.getName());
+        });
+        return respVO;
+    }
+
+    private void validateCustomerForCreate(CrmCustomerImportExcelVO importCustomer) {
+        // 校验客户名称不能为空
+        if (StrUtil.isEmptyIfStr(importCustomer.getName())) {
+            throw exception(CUSTOMER_CREATE_NAME_NOT_NULL);
+        }
+    }
+
+    // ==================== 公海相关操作 ====================
 
     @Override
     @Transactional(rollbackFor = Exception.class)