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

This commit is contained in:
anhaohao
2024-01-28 20:11:00 +08:00
157 changed files with 2747 additions and 1090 deletions

View File

@@ -18,7 +18,6 @@
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
crm 包下客户关系管理Customer Relationship Management
例如说:客户、联系人、商机、合同、回款等等

View File

@@ -70,5 +70,11 @@
<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

@@ -41,7 +41,7 @@ import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.E
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
@Tag(name = "管理后台 - 商机")
@Tag(name = "管理后台 - CRM 商机")
@RestController
@RequestMapping("/crm/business")
@Validated

View File

@@ -40,7 +40,7 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Tag(name = "管理后台 - 商机状态类型")
@Tag(name = "管理后台 - CRM 商机状态类型")
@RestController
@RequestMapping("/crm/business-status-type")
@Validated

View File

@@ -99,7 +99,7 @@ public class CrmClueController {
@PostMapping("/transform")
@Operation(summary = "线索转化为客户")
@PreAuthorize("@ss.hasPermission('crm:clue:update')")
public CommonResult<Boolean> translateCustomer(@Valid @RequestBody CrmClueTransformReqVO reqVO) {
public CommonResult<Boolean> translateCustomer(@Valid @RequestBody CrmClueTranslateReqVO reqVO) {
clueService.translateCustomer(reqVO, getLoginUserId());
return success(Boolean.TRUE);
}

View File

@@ -30,4 +30,13 @@ public class CrmCluePageReqVO extends PageParam {
@Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean pool; // null 则表示为不是公海数据
@Schema(description = "所属行业", example = "1")
private Integer industryId;
@Schema(description = "客户等级", example = "1")
private Integer level;
@Schema(description = "客户来源", example = "1")
private Integer source;
}

View File

@@ -77,4 +77,39 @@ public class CrmClueRespVO {
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
@ExcelProperty(value = "所属行业", converter = DictConvert.class)
@DictFormat(cn.iocoder.yudao.module.crm.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.crm.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.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
private Integer source;
@Schema(description = "网址", example = "25682")
@ExcelProperty("网址")
private String website;
@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 = "25682")
@ExcelProperty("客户描述")
private String description;
}

View File

@@ -1,16 +1,25 @@
package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
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.crm.enums.customer.CrmCustomerLevelEnum;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerIndustryParseFunction;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerLevelParseFunction;
import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerSourceParseFunction;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
@Schema(description = "管理后台 - CRM 线索 创建/更新 Request VO")
@Data
@@ -55,4 +64,42 @@ public class CrmClueSaveReqVO {
@DiffLogField(name = "备注")
private String remark;
@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 = "https://www.baidu.com")
@DiffLogField(name = "网址")
private String website;
@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 = "任意文字")
@DiffLogField(name = "客户描述")
@Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
private String description;
}

View File

@@ -8,7 +8,7 @@ import java.util.Set;
@Schema(description = "管理后台 - 线索转化为客户 Request VO")
@Data
public class CrmClueTransformReqVO {
public class CrmClueTranslateReqVO {
@Schema(description = "线索编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 1025]")
@NotEmpty(message = "线索编号不能为空")

View File

@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.crm.controller.admin.operatelog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogV2RespVO;
import cn.iocoder.yudao.module.crm.enums.LogRecordConstants;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2PageReqDTO;
import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2RespDTO;
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;
@@ -51,16 +51,14 @@ public class CrmOperateLogController {
BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), CRM_RECEIVABLE_PLAN_TYPE);
}
// TODO @puhui999还是搞个 VO 出来哈
@GetMapping("/page")
@Operation(summary = "获得操作日志")
@Parameter(name = "id", description = "客户编号", required = true)
@PreAuthorize("@ss.hasPermission('crm:customer:query')")
public CommonResult<PageResult<OperateLogV2RespDTO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
@PreAuthorize("@ss.hasPermission('crm:operate-log:query')")
public CommonResult<PageResult<CrmOperateLogV2RespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
OperateLogV2PageReqDTO reqDTO = new OperateLogV2PageReqDTO();
reqDTO.setPageSize(PAGE_SIZE_NONE); // 默认不分页,需要分页需注释
reqDTO.setBizType(BIZ_TYPE_MAP.get(pageReqVO.getBizType())).setBizId(pageReqVO.getBizId());
return success(operateLogApi.getOperateLogPage(reqDTO));
return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogV2RespVO.class));
}
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - CRM 跟进 Response VO")
@Data
@ExcelIgnoreUnannotated
public class CrmOperateLogV2RespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private Long id;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String userName;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer userType;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private String type;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "修改客户")
private String subType;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
private Long bizId;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "将什么从什么改为了什么")
private String action;
@Schema(description = "编号", example = "{orderId: 1}")
private String extra;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-01")
private LocalDateTime createTime;
}

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.permission;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
@@ -29,13 +30,10 @@ public interface CrmPermissionConvert {
CrmPermissionConvert INSTANCE = Mappers.getMapper(CrmPermissionConvert.class);
// TODO @puhui999这个要不也搞到 copy 里
List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission);
default List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission, List<AdminUserRespDTO> userList,
default List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permissions, List<AdminUserRespDTO> userList,
Map<Long, DeptRespDTO> deptMap, Map<Long, PostRespDTO> postMap) {
Map<Long, AdminUserRespDTO> userMap = CollectionUtils.convertMap(userList, AdminUserRespDTO::getId);
return CollectionUtils.convertList(convert(permission), item -> {
return CollectionUtils.convertList(BeanUtils.toBean(permissions, CrmPermissionRespVO.class), item -> {
findAndThen(userMap, item.getUserId(), user -> {
item.setNickname(user.getNickname());
findAndThen(deptMap, user.getDeptId(), deptRespDTO -> item.setDeptName(deptRespDTO.getName()));

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
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;
@@ -72,16 +73,44 @@ public class CrmClueDO extends BaseDO {
* 备注
*/
private String remark;
/**
* 负责人的用户编号
*
* 关联 AdminUserDO 的 id 字段
*/
private Long ownerUserId;
// TODO 芋艿:客户级别;
// TODO 芋艿:线索来源;
// TODO 芋艿:客户行业;
/**
* 所属行业
* 对应字典 {@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 website;
/**
* QQ
*/
private String qq;
/**
* wechat
*/
private String wechat;
/**
* email
*/
private String email;
/**
* 客户描述
*/
private String description;
}

View File

@@ -114,10 +114,15 @@ public class CrmCustomerDO extends BaseDO {
*/
private String detailAddress;
/**
* 最后接收时间
*/
private LocalDateTime receiveTime;
/**
* 最后跟进时间
*/
private LocalDateTime contactLastTime;
/**
* 最后跟进内容
*/

View File

@@ -37,6 +37,9 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
.likeIfPresent(CrmClueDO::getName, pageReqVO.getName())
.likeIfPresent(CrmClueDO::getTelephone, pageReqVO.getTelephone())
.likeIfPresent(CrmClueDO::getMobile, pageReqVO.getMobile())
.eqIfPresent(CrmClueDO::getIndustryId, pageReqVO.getIndustryId())
.eqIfPresent(CrmClueDO::getLevel, pageReqVO.getLevel())
.eqIfPresent(CrmClueDO::getSource, pageReqVO.getSource())
.orderByDesc(CrmClueDO::getId);
return selectJoinPage(pageReqVO, CrmClueDO.class, query);
}

View File

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageR
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmQueryWrapperUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.lang.Nullable;
@@ -99,4 +100,11 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
return selectJoinPage(pageReqVO, CrmCustomerDO.class, query);
}
default List<CrmCustomerDO> selectListByLockStatusAndOwnerUserIdNotNull(Boolean lockStatus) {
return selectList(new LambdaQueryWrapper<CrmCustomerDO>()
.eq(CrmCustomerDO::getLockStatus, lockStatus)
// TODO @puhui999not null 可以转化成大于 0
.isNotNull(CrmCustomerDO::getOwnerUserId));
}
}

View File

@@ -1,9 +1,17 @@
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;
/**
@@ -22,6 +30,22 @@ 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()));
}
/**
* 静态内部类实现单例获取
*
@@ -30,11 +54,16 @@ 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

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.crm.job.customer;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* 客户自动掉入公海 Job
*
* @author 芋道源码
*/
@Component
public class CrmCustomerAutoPutPoolJob implements JobHandler {
@Resource
private CrmCustomerService customerService;
@Override
@TenantJob
public String execute(String param) {
int count = customerService.customerAutoPutPoolBySystem();
return String.format("掉入公海客户 %s 个", count);
}
}

View File

@@ -0,0 +1,4 @@
/**
* TODO 芋艿:临时占位,后续可删除
*/
package cn.iocoder.yudao.module.crm.job;

View File

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransformReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTranslateReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
import jakarta.validation.Valid;
@@ -88,6 +88,6 @@ public interface CrmClueService {
* @param reqVO 线索编号
* @param userId 用户编号
*/
void translateCustomer(CrmClueTransformReqVO reqVO, Long userId);
void translateCustomer(CrmClueTranslateReqVO reqVO, Long userId);
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.service.clue;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -11,11 +10,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransformReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTranslateReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerSaveReqVO;
import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
@@ -177,7 +175,7 @@ public class CrmClueServiceImpl implements CrmClueService {
@Override
@Transactional(rollbackFor = Exception.class)
@CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
public void translateCustomer(CrmClueTransformReqVO reqVO, Long userId) {
public void translateCustomer(CrmClueTranslateReqVO reqVO, Long userId) {
// 1.1 校验线索都存在
Set<Long> clueIds = reqVO.getIds();
List<CrmClueDO> clues = getClueList(clueIds, userId);
@@ -192,47 +190,44 @@ public class CrmClueServiceImpl implements CrmClueService {
throw exception(CLUE_ANY_CLUE_ALREADY_TRANSLATED, convertSet(translatedClues, CrmClueDO::getId));
}
// 2. 遍历线索(未转化的线索),创建对应的客户
// TODO @puhui999这里不用过滤了
List<CrmClueDO> translateClues = filterList(clues, clue -> ObjUtil.equal(Boolean.FALSE, clue.getTransformStatus()));
List<CrmCustomerDO> customers = customerService.createCustomerBatch(convertList(translateClues, clue ->
BeanUtils.toBean(clue, CrmCustomerCreateReqBO.class)), userId);
// TODO @puhui999这里不用搞一个 clueCustomerIdMap 出来;可以考虑逐个创建,然后把 customerId 设置回 CrmClueDO避免 name 匹配,极端会有问题哈;
// TODO 是不是就直接 foreach 处理好了因为本身量不大for 处理性能 ok可阅读性好
Map<Long, Long> clueCustomerIdMap = new HashMap<>(translateClues.size());
// 2.1 更新线索
clueMapper.updateBatch(convertList(customers, customer -> {
CrmClueDO firstClue = findFirst(translateClues, clue -> ObjUtil.equal(clue.getName(), customer.getName()));
clueCustomerIdMap.put(firstClue.getId(), customer.getId());
return new CrmClueDO().setId(firstClue.getId()).setTransformStatus(Boolean.TRUE).setCustomerId(customer.getId());
}));
// 2.3 复制跟进
updateFollowUpRecords(clueCustomerIdMap);
// 2.1 遍历线索(未转化的线索),创建对应的客户
clues.forEach(clue -> {
Long customerId = customerService.createCustomer(BeanUtils.toBean(clue, CrmCustomerCreateReqBO.class), userId);
clue.setCustomerId(customerId);
});
// 2.2 更新线索
clueMapper.updateBatch(convertList(clues, clue -> new CrmClueDO().setId(clue.getId())
.setTransformStatus(Boolean.TRUE).setCustomerId(clue.getCustomerId())));
// 2.3 复制跟进记录
copyFollowUpRecords(clues);
// 3. 记录操作日志
for (CrmClueDO clue : translateClues) {
// TODO @puhui999这里优化下translate 操作日志
getSelf().receiveClueLog(clue);
for (CrmClueDO clue : clues) {
getSelf().translateCustomerLog(clue);
}
}
private void updateFollowUpRecords(Map<Long, Long> clueCustomerIdMap) {
/**
* 线索被转换客户后,需要将线索的跟进记录,复制到客户上
*
* @param clues 被转化的线索
*/
private void copyFollowUpRecords(List<CrmClueDO> clues) {
List<CrmFollowUpRecordDO> followUpRecords = followUpRecordService.getFollowUpRecordByBiz(
CrmBizTypeEnum.CRM_LEADS.getType(), clueCustomerIdMap.keySet());
CrmBizTypeEnum.CRM_LEADS.getType(), convertSet(clues, CrmClueDO::getId));
if (CollUtil.isEmpty(followUpRecords)) {
return;
}
// 创建跟进
Map<Long, CrmClueDO> clueMap = convertMap(clues, CrmClueDO::getId);
followUpRecordService.createFollowUpRecordBatch(convertList(followUpRecords, followUpRecord ->
BeanUtils.toBean(followUpRecord, CrmFollowUpCreateReqBO.class).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
.setBizId(clueCustomerIdMap.get(followUpRecord.getBizId()))));
.setBizId(clueMap.get(followUpRecord.getBizId()).getCustomerId())));
}
@LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_TRANSLATE_SUB_TYPE, bizNo = "{{#clue.id}}",
success = CRM_LEADS_TRANSLATE_SUCCESS)
public void receiveClueLog(CrmClueDO clue) {
public void translateCustomerLog(CrmClueDO clue) {
// 记录操作日志上下文
LogRecordContext.putVariable("clue", clue);
}

View File

@@ -6,6 +6,9 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
import cn.iocoder.yudao.module.crm.dal.mysql.contactbusinesslink.CrmContactBusinessMapper;
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.CrmBusinessService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@@ -19,7 +22,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
// TODO @puhui999数据权限的校验每个操作
/**
* 联系人与商机的关联 Service 实现类
*
@@ -40,6 +42,7 @@ public class CrmContactBusinessServiceImpl implements CrmContactBusinessService
private CrmContactService contactService;
@Override
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#createReqVO.contactId", level = CrmPermissionLevelEnum.WRITE)
public void createContactBusinessList(CrmContactBusinessReqVO createReqVO) {
CrmContactDO contact = contactService.getContact(createReqVO.getContactId());
if (contact == null) {
@@ -65,6 +68,7 @@ public class CrmContactBusinessServiceImpl implements CrmContactBusinessService
}
@Override
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#deleteReqVO.contactId", level = CrmPermissionLevelEnum.WRITE)
public void deleteContactBusinessList(CrmContactBusinessReqVO deleteReqVO) {
CrmContactDO contact = contactService.getContact(deleteReqVO.getContactId());
if (contact == null) {
@@ -76,11 +80,13 @@ public class CrmContactBusinessServiceImpl implements CrmContactBusinessService
}
@Override
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#contactId", level = CrmPermissionLevelEnum.WRITE)
public void deleteContactBusinessByContactId(Long contactId) {
contactBusinessMapper.delete(CrmContactBusinessDO::getContactId,contactId);
contactBusinessMapper.delete(CrmContactBusinessDO::getContactId, contactId);
}
@Override
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#contactId", level = CrmPermissionLevelEnum.READ)
public List<CrmContactBusinessDO> getContactBusinessListByContactId(Long contactId) {
return contactBusinessMapper.selectListByContactId(contactId);
}

View File

@@ -100,13 +100,13 @@ public interface CrmCustomerService {
void updateCustomerFollowUp(CrmUpdateFollowUpReqBO customerUpdateFollowUpReqBO);
/**
* 批量创建客户
* 创建客户
*
* @param customerCreateReqBOs 请求
* @param userId 用户编号
* @param customerCreateReq 请求信息
* @param userId 用户编号
* @return 客户列表
*/
List<CrmCustomerDO> createCustomerBatch(List<CrmCustomerCreateReqBO> customerCreateReqBOs, Long userId);
Long createCustomer(CrmCustomerCreateReqBO customerCreateReq, Long userId);
// ==================== 公海相关操作 ====================
@@ -126,4 +126,12 @@ public interface CrmCustomerService {
*/
void receiveCustomer(List<Long> ids, Long ownerUserId, Boolean isReceive);
// TODO @puhui999autoPutCustomerPool注释说明是系统就好哈
/**
* 【系统】客户自动掉入公海
*
* @return 掉入公海数量
*/
int customerAutoPutPoolBySystem();
}

View File

@@ -2,9 +2,11 @@ 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.extra.spring.SpringUtil;
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;
@@ -13,6 +15,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerTrans
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;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
@@ -31,6 +34,7 @@ import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,12 +47,10 @@ import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
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;
import static cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLimitConfigTypeEnum.CUSTOMER_OWNER_LIMIT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
/**
@@ -57,6 +59,7 @@ import static java.util.Collections.singletonList;
* @author Wanwan
*/
@Service
@Slf4j
@Validated
public class CrmCustomerServiceImpl implements CrmCustomerService {
@@ -69,6 +72,9 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
private CrmCustomerLimitConfigService customerLimitConfigService;
@Resource
@Lazy
private CrmCustomerPoolConfigService customerPoolConfigService;
@Resource
@Lazy
private CrmContactService contactService;
@Resource
@Lazy
@@ -91,9 +97,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
// 2. 插入客户
CrmCustomerDO customer = BeanUtils.toBean(createReqVO, CrmCustomerDO.class)
.setLockStatus(false).setDealStatus(false)
.setContactLastTime(LocalDateTime.now());
// TODO @puhui999可能要加个 receiveTime 字段,记录最后接收时间
.setLockStatus(false).setDealStatus(false).setContactLastTime(LocalDateTime.now());
customerMapper.insert(customer);
// 3. 创建数据权限
@@ -214,24 +218,24 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
@Override
@Transactional(rollbackFor = Exception.class)
public List<CrmCustomerDO> createCustomerBatch(List<CrmCustomerCreateReqBO> customerCreateReqBOs, Long userId) {
if (CollUtil.isEmpty(customerCreateReqBOs)) {
return emptyList();
}
@LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_CREATE_SUB_TYPE, bizNo = "{{#customer.id}}",
success = CRM_CUSTOMER_CREATE_SUCCESS)
public Long createCustomer(CrmCustomerCreateReqBO customerCreateReq, Long userId) {
// 1. 插入客户
CrmCustomerDO customer = BeanUtils.toBean(customerCreateReq, CrmCustomerDO.class).setOwnerUserId(userId)
.setLockStatus(false).setDealStatus(false).setReceiveTime(LocalDateTime.now());
customerMapper.insert(customer);
// 创建客户
List<CrmCustomerDO> customers = convertList(customerCreateReqBOs, customerBO ->
BeanUtils.toBean(customerBO, CrmCustomerDO.class).setOwnerUserId(userId));
customerMapper.insertBatch(customers);
// 2. 创建数据权限
permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
.setBizId(customer.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
// 创建负责人数据权限
permissionService.createPermissionBatch(convertList(customers, customer -> new CrmPermissionCreateReqBO()
.setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()).setBizId(customer.getId()).setUserId(userId)
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel())));
return customers;
// 3. 记录操作日志上下文
LogRecordContext.putVariable("customer", customer);
return customer.getId();
}
// ==================== 公海相关操作 ====================
// ==================== 公海相关操作 ====================
@Override
@Transactional(rollbackFor = Exception.class)
@@ -249,17 +253,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
// 1.3. 校验客户是否锁定
validateCustomerIsLocked(customer, true);
// 2.1 设置负责人为 NULL
int updateOwnerUserIncr = customerMapper.updateOwnerUserIdById(customer.getId(), null);
if (updateOwnerUserIncr == 0) {
throw exception(CUSTOMER_UPDATE_OWNER_USER_FAIL);
}
// 2.2 删除负责人数据权限
permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), customer.getId(),
CrmPermissionLevelEnum.OWNER.getLevel());
// 3. 联系人的负责人,也要设置为 null。因为因为领取后负责人也要关联过来这块和 receiveCustomer 是对应的
contactService.updateOwnerUserIdByCustomerId(customer.getId(), null);
// 2. 客户放入公海
putCustomerPool(customer);
// 记录操作日志上下文
LogRecordContext.putVariable("customerName", customer.getName());
@@ -317,6 +312,52 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
}
}
@Override
public int customerAutoPutPoolBySystem() {
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 未跟进放入公海
LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
return (poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime)) <= 0;
});
// 2. 逐个放入公海
int count = 0;
for (CrmCustomerDO customer : poolCustomerList) {
try {
getSelf().putCustomerPool(customer);
count++;
} catch (Throwable e) {
log.error("[customerAutoPutPoolBySystem][Customer 客户({}) 放入公海异常]", customer.getId(), e);
}
}
return count;
}
private void putCustomerPool(CrmCustomerDO customer) {
// 1. 设置负责人为 NULL
int updateOwnerUserIncr = customerMapper.updateOwnerUserIdById(customer.getId(), null);
if (updateOwnerUserIncr == 0) {
throw exception(CUSTOMER_UPDATE_OWNER_USER_FAIL);
}
// 2. 删除负责人数据权限
permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), customer.getId(),
CrmPermissionLevelEnum.OWNER.getLevel());
// 3. 联系人的负责人,也要设置为 null。因为因为领取后负责人也要关联过来这块和 receiveCustomer 是对应的
contactService.updateOwnerUserIdByCustomerId(customer.getId(), null);
}
@LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_RECEIVE_SUB_TYPE, bizNo = "{{#customer.id}}",
success = CRM_CUSTOMER_RECEIVE_SUCCESS)
public void receiveCustomerLog(CrmCustomerDO customer, String ownerUserName) {

View File

@@ -28,9 +28,9 @@ public interface CrmFollowUpRecordService {
/**
* 创建更进
*
* @param followUpCreateReqBOs 请求
* @param list 请求
*/
void createFollowUpRecordBatch(List<CrmFollowUpCreateReqBO> followUpCreateReqBOs);
void createFollowUpRecordBatch(List<CrmFollowUpCreateReqBO> list);
/**
* 删除跟进记录 (数据权限基于 bizType、 bizId)

View File

@@ -7,7 +7,6 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.dal.mysql.followup.CrmFollowUpRecordMapper;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
@@ -31,10 +30,10 @@ import java.util.Collections;
import java.util.List;
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.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 实现类
@@ -94,26 +93,25 @@ public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
customerService.updateCustomerFollowUp(updateFollowUpReqBO);
}
// TODO @puhui999这两个不更新 contactLastTime、contactLastContent只更新 nextTime
// 3.1 更新 contactIds 对应的记录
// 3.1 更新 contactIds 对应的记录,不更新 lastTime 和 lastContent
if (CollUtil.isNotEmpty(createReqVO.getContactIds())) {
contactService.updateContactFollowUpBatch(convertList(createReqVO.getContactIds(), updateFollowUpReqBO::setBizId));
contactService.updateContactFollowUpBatch(convertList(createReqVO.getContactIds(),
contactId -> updateFollowUpReqBO.setBizId(contactId).setContactLastTime(null).setContactLastContent(null)));
}
// 3.2 需要更新 businessIds、contactIds 对应的记录
// 3.2 需要更新 businessIds 对应的记录,不更新 lastTime 和 lastContent
if (CollUtil.isNotEmpty(createReqVO.getBusinessIds())) {
businessService.updateBusinessFollowUpBatch(convertList(createReqVO.getBusinessIds(), updateFollowUpReqBO::setBizId));
businessService.updateBusinessFollowUpBatch(convertList(createReqVO.getBusinessIds(),
businessId -> updateFollowUpReqBO.setBizId(businessId).setContactLastTime(null).setContactLastContent(null)));
}
return followUpRecord.getId();
}
@Override
public void createFollowUpRecordBatch(List<CrmFollowUpCreateReqBO> followUpCreateReqBOs) {
if (CollUtil.isEmpty(followUpCreateReqBOs)) {
public void createFollowUpRecordBatch(List<CrmFollowUpCreateReqBO> list) {
if (CollUtil.isEmpty(list)) {
return;
}
List<CrmFollowUpRecordDO> followUpRecords = BeanUtils.toBean(followUpCreateReqBOs, CrmFollowUpRecordDO.class);
crmFollowUpRecordMapper.insertBatch(followUpRecords);
crmFollowUpRecordMapper.insertBatch(BeanUtils.toBean(list, CrmFollowUpRecordDO.class));
}
@Override
@@ -121,12 +119,7 @@ public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
// 校验存在
CrmFollowUpRecordDO followUpRecord = validateFollowUpRecordExists(id);
// 校验权限
// TODO @puhui999是不是封装一个 hasPermission更简介一点
List<CrmPermissionDO> permissionList = permissionService.getPermissionListByBiz(
followUpRecord.getBizType(), followUpRecord.getBizId());
boolean hasPermission = anyMatch(permissionList, permission ->
ObjUtil.equal(permission.getUserId(), userId) && ObjUtil.equal(permission.getLevel(), CrmPermissionLevelEnum.OWNER.getLevel()));
if (!hasPermission) {
if (!hasPermission(followUpRecord.getBizType(), followUpRecord.getBizId(), userId, CrmPermissionLevelEnum.OWNER)) {
throw exception(FOLLOW_UP_RECORD_DELETE_DENIED);
}

View File

@@ -21,7 +21,6 @@ public class CrmUpdateFollowUpReqBO {
@Schema(description = "最后跟进时间")
@DiffLogField(name = "最后跟进时间")
@NotNull(message = "最后跟进时间不能为空")
private LocalDateTime contactLastTime;
@Schema(description = "下次联系时间")
@@ -30,7 +29,6 @@ public class CrmUpdateFollowUpReqBO {
@Schema(description = "最后更进内容")
@DiffLogField(name = "最后更进内容")
@NotNull(message = "最后更进内容不能为空")
private String contactLastContent;
}