!858 CRM-客户: 完善客户导入

Merge pull request !858 from puhui999/develop
This commit is contained in:
芋道源码 2024-01-30 04:41:14 +00:00 committed by Gitee
commit acb8d3f23b
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
25 changed files with 463 additions and 117 deletions

View File

@ -6,16 +6,15 @@ import cn.iocoder.yudao.framework.common.util.io.IoUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -44,6 +43,15 @@ public class BpmModelController {
return success(model);
}
@GetMapping("/get-by-key")
@Operation(summary = "获得模型")
@Parameter(name = "key", description = "流程标识", required = true, example = "oa_leave")
@PreAuthorize("@ss.hasPermission('bpm:model:query')")
public CommonResult<BpmModelRespVO> getModelByKey(@RequestParam("key") String key) {
BpmModelRespVO model = modelService.getBpmnModelByKey(key);
return success(model);
}
@PostMapping("/create")
@Operation(summary = "新建模型")
@PreAuthorize("@ss.hasPermission('bpm:model:create')")

View File

@ -2,9 +2,8 @@ package cn.iocoder.yudao.module.bpm.service.definition;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import org.flowable.bpmn.model.BpmnModel;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.BpmnModel;
/**
* Flowable流程模型接口
@ -38,6 +37,14 @@ public interface BpmModelService {
*/
BpmModelRespVO getModel(String id);
/**
* 获得流程模块
*
* @param key 流程标识
* @return 流程模型
*/
BpmModelRespVO getBpmnModelByKey(String key);
/**
* 修改流程模型
*

View File

@ -14,6 +14,8 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
@ -29,8 +31,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -134,6 +134,19 @@ public class BpmModelServiceImpl implements BpmModelService {
return modelRespVO;
}
@Override
public BpmModelRespVO getBpmnModelByKey(String key) {
Model model = getModelByKey(key);
if (model == null) {
return null;
}
BpmModelRespVO modelRespVO = BpmModelConvert.INSTANCE.convert(model);
// 拼接 bpmn XML
byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId());
modelRespVO.setBpmnXml(StrUtil.utf8Str(bpmnBytes));
return modelRespVO;
}
@Override
@Transactional(rollbackFor = Exception.class) // 因为进行多个操作所以开启事务
public void updateModel(@Valid BpmModelUpdateReqVO updateReqVO) {

View File

@ -11,6 +11,7 @@ public interface ErrorCodeConstants {
// ========== 合同管理 1-020-000-000 ==========
ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
ErrorCode CONTRACT_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_020_000_001, "更新合同失败,原因:禁止编辑");
// ========== 线索管理 1-020-001-000 ==========
ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
@ -48,6 +49,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, "数据权限不存在");

View File

@ -27,6 +27,11 @@
<artifactId>yudao-module-crm-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-bpm-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
@ -37,6 +42,10 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
@ -70,11 +79,5 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
</dependency>
<!-- TODO @puhui999放的位置要整齐哈。 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -140,4 +140,12 @@ public class CrmContractController {
return success(true);
}
@PutMapping("/approve")
@Operation(summary = "发起合同审批流程")
@PreAuthorize("@ss.hasPermission('crm:contract:update')")
public CommonResult<Boolean> transfer(@RequestParam("id") Long id) {
contractService.handleApprove(id, getLoginUserId());
return success(true);
}
}

View File

@ -7,10 +7,13 @@ import cn.iocoder.yudao.module.crm.framework.operatelog.core.SysAdminUserParseFu
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -35,10 +38,6 @@ public class CrmContractSaveReqVO {
@DiffLogField(name = "商机", function = CrmBusinessParseFunction.NAME)
private Long businessId;
@Schema(description = "工作流编号", example = "1043")
@DiffLogField(name = "工作流编号")
private Long processInstanceId;
@Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
@DiffLogField(name = "下单日期")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ -86,16 +85,33 @@ public class CrmContractSaveReqVO {
@DiffLogField(name = "公司签约人", function = SysAdminUserParseFunction.NAME)
private Long signUserId;
@Schema(description = "最后跟进时间")
@DiffLogField(name = "最后跟进时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime contactLastTime;
@Schema(description = "备注", example = "你猜")
@DiffLogField(name = "备注")
private String remark;
// TODO @dhb52增加一个 status 字段具体有哪些值你来枚举下主要页面上有个草稿提交审核的流程可以看看然后要对接工作流这块也可以看看不确定的地方问我
@Schema(description = "审批状态", example = "1")
private Integer auditStatus;
@Schema(description = "产品列表")
private List<CrmContractProductItem> productItems;
@Schema(description = "商品属性")
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CrmContractProductItem {
@Schema(description = "产品编号", example = "20529")
@NotNull(message = "产品编号不能为空")
private Long id;
@Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
@NotNull(message = "产品数量不能为空")
private Integer count;
@Schema(description = "产品折扣")
private Integer discountPercent;
}
}

View File

@ -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;
@ -198,6 +201,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, getLoginUserId()));
}
@PutMapping("/transfer")
@Operation(summary = "转移客户")
@PreAuthorize("@ss.hasPermission('crm:customer:update')")

View File

@ -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;
}

View File

@ -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;
}

View File

@ -2,13 +2,12 @@ package cn.iocoder.yudao.module.crm.dal.dataobject.business;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
* 商机产品关联表 DO
*
@ -29,14 +28,12 @@ public class CrmBusinessProductDO extends BaseDO {
*/
@TableId
private Long id;
/**
* 商机编号
*
* 关联 {@link CrmBusinessDO#getId()}
*/
private Long businessId;
/**
* 产品编号
*
@ -50,29 +47,27 @@ public class CrmBusinessProductDO extends BaseDO {
private Integer price;
/**
* 销售价格
* 销售价格, 单位
*/
private BigDecimal salesPrice;
private Integer salesPrice;
/**
* 数量
*/
private BigDecimal count;
// TODO @lzxhqs改成 discountPercent
private Integer count;
/**
* 折扣
*/
private BigDecimal discountPercent;
// TODO @lzxhqs改成 totalPrice总计价格和现有项目风格一致
private Integer discountPercent;
/**
* 小计折扣后价格
* 总计价格折扣后价格
*/
private BigDecimal totalPrice;
private Integer totalPrice;
/**
* 单位
*
* 字典 {@link DictTypeConstants#CRM_PRODUCT_UNIT}
*/
private String unit;
private Integer unit;
}

View File

@ -45,7 +45,7 @@ public class CrmContractDO extends BaseDO {
/**
* 工作流编号
*/
private Long processInstanceId;
private String processInstanceId;
/**
* 下单日期
*/

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.product;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import cn.iocoder.yudao.module.crm.enums.product.CrmProductStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@ -38,7 +39,7 @@ public class CrmProductDO extends BaseDO {
/**
* 单位
*
* 字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_PRODUCT_UNIT}
* 字典 {@link DictTypeConstants#CRM_PRODUCT_UNIT}
*/
private Integer unit;
/**

View File

@ -41,7 +41,7 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
default PageResult<CrmContractDO> selectPage(CrmContractPageReqVO pageReqVO, Long userId) {
MPJLambdaWrapperX<CrmContractDO> mpjLambdaWrapperX = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件
CrmQueryWrapperUtils.appendPermissionCondition(mpjLambdaWrapperX, CrmBizTypeEnum.CRM_CONTACT.getType(),
CrmQueryWrapperUtils.appendPermissionCondition(mpjLambdaWrapperX, CrmBizTypeEnum.CRM_CONTRACT.getType(),
CrmContractDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
// 拼接自身的查询条件
mpjLambdaWrapperX.selectAll(CrmContractDO.class)
@ -56,7 +56,7 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
default List<CrmContractDO> selectBatchIds(Collection<Long> ids, Long userId) {
MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>();
// 构建数据权限连表条件
CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTACT.getType(), ids, userId);
CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(), ids, userId);
// 拼接自身的查询条件
query.selectAll(CrmContractDO.class).in(CrmContractDO::getId, ids).orderByDesc(CrmContractDO::getId);
return selectJoinList(CrmContractDO.class, query);

View File

@ -101,11 +101,15 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
return selectJoinPage(pageReqVO, CrmCustomerDO.class, query);
}
default List<CrmCustomerDO> selectListByLockStatusAndOwnerUserIdNotNull(Boolean lockStatus) {
default List<CrmCustomerDO> selectListByLockAndDealStatusAndNotPool(Boolean lockStatus, Boolean dealStatus) {
return selectList(new LambdaQueryWrapper<CrmCustomerDO>()
.eq(CrmCustomerDO::getLockStatus, lockStatus)
// TODO @puhui999not null 可以转化成大于 0
.isNotNull(CrmCustomerDO::getOwnerUserId));
.eq(CrmCustomerDO::getDealStatus, dealStatus)
.gt(CrmCustomerDO::getOwnerUserId, 0));
}
default CrmCustomerDO selectByCustomerName(String name) {
return selectOne(CrmCustomerDO::getName, name);
}
default PageResult<CrmCustomerDO> selectPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,

View File

@ -1,17 +1,9 @@
package cn.iocoder.yudao.module.crm.framework.permission.core.util;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionRoleCodeEnum;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
@ -30,22 +22,6 @@ public class CrmPermissionUtils {
return SingletonManager.getPermissionApi().hasAnyRoles(getLoginUserId(), CrmPermissionRoleCodeEnum.CRM_ADMIN.getCode());
}
// TODO @puhui999这个貌似直接放到 CrmPermissionService 会更好
/**
* 校验权限
*
* @param bizType 数据类型关联 {@link CrmBizTypeEnum}
* @param bizId 数据编号关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
* @param userId 用户编号
* @param levelEnum 权限级别
* @return boolean
*/
public static boolean hasPermission(Integer bizType, Long bizId, Long userId, CrmPermissionLevelEnum levelEnum) {
List<CrmPermissionDO> permissionList = SingletonManager.getCrmPermissionService().getPermissionListByBiz(bizType, bizId);
return anyMatch(permissionList, permission ->
ObjUtil.equal(permission.getUserId(), userId) && ObjUtil.equal(permission.getLevel(), levelEnum.getLevel()));
}
/**
* 静态内部类实现单例获取
*
@ -54,16 +30,11 @@ public class CrmPermissionUtils {
private static class SingletonManager {
private static final PermissionApi PERMISSION_API = SpringUtil.getBean(PermissionApi.class);
private static final CrmPermissionService CRM_PERMISSION_SERVICE = SpringUtil.getBean(CrmPermissionService.class);
public static PermissionApi getPermissionApi() {
return PERMISSION_API;
}
public static CrmPermissionService getCrmPermissionService() {
return CRM_PERMISSION_SERVICE;
}
}
}

View File

@ -20,7 +20,7 @@ public class CrmCustomerAutoPutPoolJob implements JobHandler {
@Override
@TenantJob
public String execute(String param) {
int count = customerService.customerAutoPutPoolBySystem();
int count = customerService.autoPutCustomerPool();
return String.format("掉入公海客户 %s 个", count);
}

View File

@ -57,6 +57,14 @@ public interface CrmContractService {
*/
void updateContractFollowUp(CrmUpdateFollowUpReqBO contractUpdateFollowUpReqBO);
/**
* 发起合同审批流程
*
* @param id 合同编号
* @param userId 用户编号
*/
void handleApprove(Long id, Long userId);
/**
* 获得合同
*
@ -111,9 +119,11 @@ public interface CrmContractService {
Long getContractCountByCustomerId(Long customerId);
/**
* 根据商机ID获取关联客户的合同数量 TODO @lzxhqs1方法注释和参数注释之间要有空行2中英文之间有空格更清晰例如说 商机 ID
* @param businessId 商机ID
* 根据商机ID获取关联客户的合同数量
*
* @param businessId 商机编号
* @return 数量
*/
Long selectCountByBusinessId(Long businessId);
}

View File

@ -2,20 +2,33 @@ package cn.iocoder.yudao.module.crm.service.contract;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.convert.contract.CrmContractConvert;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
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.business.CrmBusinessProductService;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
@ -26,10 +39,14 @@ import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTRACT_NOT_EXISTS;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
/**
* CRM 合同 Service 实现类
@ -40,21 +57,36 @@ import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
@Validated
public class CrmContractServiceImpl implements CrmContractService {
public static final String CONTRACT_APPROVE = "contract-approve"; // 合同审批流程标识
@Resource
private CrmContractMapper contractMapper;
@Resource
private CrmPermissionService crmPermissionService;
@Resource
private CrmBusinessProductService businessProductService;
@Resource
private CrmProductService productService;
@Resource
private BpmProcessInstanceApi bpmProcessInstanceApi;
@Resource
private CrmCustomerService customerService;
@Resource
private CrmContactService contactService;
@Resource
private CrmBusinessService businessService;
@Resource
private AdminUserApi adminUserApi;
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_CREATE_SUB_TYPE, bizNo = "{{#contract.id}}",
success = CRM_CONTRACT_CREATE_SUCCESS)
public Long createContract(CrmContractSaveReqVO createReqVO, Long userId) {
createReqVO.setId(null);
// TODO @合同待定插入合同商品需要搞个 BusinessProductDO
validateRelationDataExists(createReqVO);
// 插入合同
CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class);
CrmContractDO contract = BeanUtils.toBean(createReqVO, CrmContractDO.class).setId(null);
contractMapper.insert(contract);
// 创建数据权限
@ -62,6 +94,9 @@ public class CrmContractServiceImpl implements CrmContractService {
.setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
// 插入商机关联商品
List<CrmBusinessProductDO> businessProduct = convertBusinessProductList(createReqVO);
businessProductService.insertBatch(businessProduct);
// 4. 记录操作日志上下文
LogRecordContext.putVariable("contract", contract);
return contract.getId();
@ -74,12 +109,21 @@ public class CrmContractServiceImpl implements CrmContractService {
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
public void updateContract(CrmContractSaveReqVO updateReqVO) {
// TODO @合同待定只有草稿审批中可以编辑
if (ObjUtil.notEqual(updateReqVO.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus()) ||
ObjUtil.notEqual(updateReqVO.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(CONTRACT_UPDATE_FAIL_EDITING_PROHIBITED);
}
validateRelationDataExists(updateReqVO);
// 校验存在
CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
// 更新合同
CrmContractDO updateObj = BeanUtils.toBean(updateReqVO, CrmContractDO.class);
contractMapper.updateById(updateObj);
// TODO @合同待定插入合同商品需要搞个 BusinessProductDO
// TODO puhui999: @芋艿合同变更关联的商机后商品怎么处理
//List<CrmBusinessProductDO> businessProduct = convertBusinessProductList(updateReqVO);
//businessProductService.selectListByBusinessId()
//diffList()
// 3. 记录操作日志上下文
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class));
@ -88,8 +132,55 @@ public class CrmContractServiceImpl implements CrmContractService {
// TODO @合同待定缺一个取消合同的接口只有草稿审批中可以取消CrmAuditStatusEnum
// TODO @合同待定缺一个发起审批的接口只有草稿可以发起审批CrmAuditStatusEnum
private List<CrmBusinessProductDO> convertBusinessProductList(CrmContractSaveReqVO reqVO) {
// 校验商品存在
Set<Long> productIds = convertSet(reqVO.getProductItems(), CrmContractSaveReqVO.CrmContractProductItem::getId);
List<CrmProductDO> productList = productService.getProductList(productIds);
if (CollUtil.isEmpty(productIds) || productList.size() != productIds.size()) {
throw exception(PRODUCT_NOT_EXISTS);
}
Map<Long, CrmProductDO> productMap = convertMap(productList, CrmProductDO::getId);
return convertList(reqVO.getProductItems(), productItem -> {
CrmBusinessProductDO businessProduct = BeanUtils.toBean(productMap.get(productItem.getId()), CrmBusinessProductDO.class);
businessProduct.setId(null).setBusinessId(reqVO.getBusinessId()).setProductId(productItem.getId())
.setCount(productItem.getCount()).setDiscountPercent(productItem.getDiscountPercent()).setTotalPrice(calculator(businessProduct));
return businessProduct;
});
}
/**
* 计算商品总价
*
* @param businessProduct 关联商品
* @return 商品总价
*/
private Integer calculator(CrmBusinessProductDO businessProduct) {
int price = businessProduct.getPrice() * businessProduct.getCount();
if (businessProduct.getDiscountPercent() == null) {
return price;
}
return MoneyUtils.calculateRatePriceFloor(price, (double) (businessProduct.getDiscountPercent() / 100));
}
/**
* 校验关联数据是否存在
*
* @param reqVO 请求
*/
private void validateRelationDataExists(CrmContractSaveReqVO reqVO) {
// 1. 校验客户
if (reqVO.getCustomerId() != null && customerService.getCustomer(reqVO.getCustomerId()) == null) {
throw exception(CUSTOMER_NOT_EXISTS);
}
// 2. 校验负责人
if (reqVO.getOwnerUserId() != null && adminUserApi.getUser(reqVO.getOwnerUserId()) == null) {
throw exception(USER_NOT_EXISTS);
}
// 4. 如果有关联商机则需要校验存在
if (reqVO.getBusinessId() != null && businessService.getBusiness(reqVO.getBusinessId()) == null) {
throw exception(BUSINESS_NOT_EXISTS);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@ -141,6 +232,18 @@ public class CrmContractServiceImpl implements CrmContractService {
contractMapper.updateById(BeanUtils.toBean(contractUpdateFollowUpReqBO, CrmContractDO.class).setId(contractUpdateFollowUpReqBO.getBizId()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void handleApprove(Long id, Long userId) {
// 创建合同审批流程实例
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(CONTRACT_APPROVE).setBusinessKey(String.valueOf(id)));
// 更新合同工作流编号
contractMapper.updateById(new CrmContractDO().setId(id).setProcessInstanceId(processInstanceId)
.setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
}
//======================= 查询相关 =======================
@Override
@ -182,6 +285,5 @@ public class CrmContractServiceImpl implements CrmContractService {
public Long selectCountByBusinessId(Long businessId) {
return contractMapper.selectCountByBusinessId(businessId);
}
// TODO @合同待定需要新增一个 ContractConfigDO 合同配置重点是到期提醒
}

View File

@ -0,0 +1,5 @@
package cn.iocoder.yudao.module.crm.service.contract.listener;
public class CrmContractResultListener {
// TODO puhui999: @芋艿 艿艿写一下这个没研究明白哈哈
}

View File

@ -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.dal.dataobject.customer.CrmCustomerPoolConfigDO;
import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
@ -109,6 +106,16 @@ public interface CrmCustomerService {
*/
Long createCustomer(CrmCustomerCreateReqBO customerCreateReq, Long userId);
/**
* 批量导入客户
*
* @param importCustomers 导入客户列表
* @param isUpdateSupport 是否支持更新
* @param userId 用户编号
* @return 导入结果
*/
CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, Boolean isUpdateSupport, Long userId);
// ==================== 公海相关操作 ====================
/**
@ -127,13 +134,12 @@ public interface CrmCustomerService {
*/
void receiveCustomer(List<Long> ids, Long ownerUserId, Boolean isReceive);
// TODO @puhui999autoPutCustomerPool注释说明是系统就好哈
/**
* 系统客户自动掉入公海
*
* @return 掉入公海数量
*/
int customerAutoPutPoolBySystem();
int autoPutCustomerPool();
PageResult<CrmCustomerDO> getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO,
CrmCustomerPoolConfigDO poolConfigDO,

View File

@ -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,12 +40,10 @@ 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;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLimitConfigTypeEnum.CUSTOMER_LOCK_LIMIT;
@ -96,7 +93,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
validateCustomerExceedOwnerLimit(createReqVO.getOwnerUserId(), 1);
// 2. 插入客户
CrmCustomerDO customer = BeanUtils.toBean(createReqVO, CrmCustomerDO.class)
CrmCustomerDO customer = BeanUtils.toBean(createReqVO, CrmCustomerDO.class).setOwnerUserId(userId)
.setLockStatus(false).setDealStatus(false).setContactLastTime(LocalDateTime.now());
customerMapper.insert(customer);
@ -235,6 +232,54 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
return customer.getId();
}
@Override
public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, Boolean isUpdateSupport, Long userId) {
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) {
CrmCustomerDO customer = BeanUtils.toBean(importCustomer, CrmCustomerDO.class).setOwnerUserId(userId)
.setLockStatus(false).setDealStatus(false).setContactLastTime(LocalDateTime.now());
customerMapper.insert(customer);
respVO.getCreateCustomerNames().add(importCustomer.getName());
// 创建数据权限
permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
.setBizId(customer.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
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
@ -313,23 +358,22 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
}
@Override
public int customerAutoPutPoolBySystem() {
public int autoPutCustomerPool() {
CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
if (poolConfig == null || !poolConfig.getEnabled()) {
return 0;
}
// 1. 获取没有锁定的不在公海的客户
List<CrmCustomerDO> customerList = customerMapper.selectListByLockStatusAndOwnerUserIdNotNull(Boolean.FALSE);
List<CrmCustomerDO> poolCustomerList = CollectionUtils.filterList(customerList, customer -> {
// TODO @puhui999建议这里作为一个查询条件哈不放内存里过滤
// 1.1 未成交放入公海
if (!customer.getDealStatus()) {
return (poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime())) <= 0;
}
// 1.2 未跟进放入公海
// 1.1 获取没有锁定的不在公海的客户且没有成交的
List<CrmCustomerDO> notDealCustomerList = customerMapper.selectListByLockAndDealStatusAndNotPool(Boolean.FALSE, Boolean.FALSE);
// 1.2 获取没有锁定的不在公海的客户且成交的
List<CrmCustomerDO> dealCustomerList = customerMapper.selectListByLockAndDealStatusAndNotPool(Boolean.FALSE, Boolean.TRUE);
List<CrmCustomerDO> poolCustomerList = new ArrayList<>();
poolCustomerList.addAll(filterList(notDealCustomerList, customer ->
(poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime())) <= 0));
poolCustomerList.addAll(filterList(dealCustomerList, customer -> {
LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
return (poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime)) <= 0;
});
}));
// 2. 逐个放入公海
int count = 0;

View File

@ -33,7 +33,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.FOLLOW_UP_RECORD_DELETE_DENIED;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.FOLLOW_UP_RECORD_NOT_EXISTS;
import static cn.iocoder.yudao.module.crm.framework.permission.core.util.CrmPermissionUtils.hasPermission;
/**
* 跟进记录 Service 实现类
@ -119,7 +118,7 @@ public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
// 校验存在
CrmFollowUpRecordDO followUpRecord = validateFollowUpRecordExists(id);
// 校验权限
if (!hasPermission(followUpRecord.getBizType(), followUpRecord.getBizId(), userId, CrmPermissionLevelEnum.OWNER)) {
if (!permissionService.hasPermission(followUpRecord.getBizType(), followUpRecord.getBizId(), userId, CrmPermissionLevelEnum.OWNER)) {
throw exception(FOLLOW_UP_RECORD_DELETE_DENIED);
}

View File

@ -108,4 +108,15 @@ public interface CrmPermissionService {
*/
List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId);
/**
* 校验权限
*
* @param bizType 数据类型关联 {@link CrmBizTypeEnum}
* @param bizId 数据编号关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
* @param userId 用户编号
* @param levelEnum 权限级别
* @return boolean
*/
boolean hasPermission(Integer bizType, Long bizId, Long userId, CrmPermissionLevelEnum levelEnum);
}

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum.isOwner;
@ -211,4 +212,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
return permissionMapper.selectListByBizTypeAndUserId(bizType, userId);
}
@Override
public boolean hasPermission(Integer bizType, Long bizId, Long userId, CrmPermissionLevelEnum levelEnum) {
List<CrmPermissionDO> permissionList = permissionMapper.selectByBizTypeAndBizId(bizType, bizId);
return anyMatch(permissionList, permission ->
ObjUtil.equal(permission.getUserId(), userId) && ObjUtil.equal(permission.getLevel(), levelEnum.getLevel()));
}
}