feat: clue owner history

This commit is contained in:
AgAngle
2025-03-12 16:55:26 +08:00
committed by Craftsman
parent e35eb6e2be
commit 47225d652c
12 changed files with 376 additions and 10 deletions

View File

@@ -0,0 +1,35 @@
package io.cordys.crm.clue.controller;
import io.cordys.common.constants.PermissionConstants;
import io.cordys.context.OrganizationContext;
import io.cordys.crm.clue.dto.response.ClueOwnerListResponse;
import io.cordys.crm.clue.service.ClueOwnerHistoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author jianxing
* @date 2025-02-08 17:42:41
*/
@Tag(name = "线索责任人历史")
@RestController
@RequestMapping("/clue/owner/history")
public class ClueOwnerHistoryController {
@Resource
private ClueOwnerHistoryService clueOwnerHistoryService;
@GetMapping("/list/{clueId}")
@RequiresPermissions(PermissionConstants.CUSTOMER_MANAGEMENT_READ)
@Operation(summary = "线索责任人历史列表")
public List<ClueOwnerListResponse> list(@PathVariable String clueId) {
return clueOwnerHistoryService.list(clueId, OrganizationContext.getOrganizationId());
}
}

View File

@@ -0,0 +1,37 @@
package io.cordys.crm.clue.domain;
import jakarta.persistence.Table;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 线索历史责任人
*
* @author jianxing
* @date 2025-03-12 15:46:27
*/
@Data
@Table(name = "clue_owner")
public class ClueOwner {
@Schema(description = "id")
private String id;
@Schema(description = "线索id")
private String clueId;
@Schema(description = "责任人")
private String owner;
@Schema(description = "领取时间")
private Long collectionTime;
@Schema(description = "结束时间")
private Long endTime;
@Schema(description = "操作人")
private String operator;
}

View File

@@ -0,0 +1,30 @@
package io.cordys.crm.clue.dto.response;
import io.cordys.crm.clue.domain.ClueOwner;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
*
* @author jianxing
* @date 2025-02-08 16:24:22
*/
@Data
public class ClueOwnerListResponse extends ClueOwner {
@Schema(description = "ID")
private String id;
@Schema(description = "操作人名称")
private String operatorName;
@Schema(description = "责任人名称")
private String ownerName;
@Schema(description = "归属部门")
private String departmentId;
@Schema(description = "归属部门名称")
private String departmentName;
}

View File

@@ -5,7 +5,7 @@
<update id="batchTransfer">
update `clue`
set owner = #{request.owner}
where id in
where owner != #{request.owner} and id in
<foreach collection="request.ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>

View File

@@ -0,0 +1,14 @@
package io.cordys.crm.clue.mapper;
import io.cordys.crm.clue.dto.request.ClueBatchTransferRequest;
import org.apache.ibatis.annotations.Param;
/**
*
* @author jianxing
* @date 2025-02-24 11:06:10
*/
public interface ExtClueOwnerMapper {
void batchAdd(@Param("request") ClueBatchTransferRequest transferRequest, @Param("userId") String userId);
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.cordys.crm.clue.mapper.ExtClueOwnerMapper">
<update id="batchAdd">
insert into clue_owner (id, owner, operator, clue_id, collection_time, end_time)
select UUID_SHORT() as id, clue.owner, clue.id as clue_id, '${userId}' as operator, clue.collection_time, UNIX_TIMESTAMP() * 1000 as end_time
from clue
where clue.owner != #{request.owner} and id in
<foreach collection="request.ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
</mapper>

View File

@@ -0,0 +1,96 @@
package io.cordys.crm.clue.service;
import io.cordys.common.dto.UserDeptDTO;
import io.cordys.common.service.BaseService;
import io.cordys.common.uid.IDGenerator;
import io.cordys.common.util.BeanUtils;
import io.cordys.crm.clue.domain.Clue;
import io.cordys.crm.clue.domain.ClueOwner;
import io.cordys.crm.clue.dto.request.ClueBatchTransferRequest;
import io.cordys.crm.clue.dto.response.ClueOwnerListResponse;
import io.cordys.crm.clue.mapper.ExtClueOwnerMapper;
import io.cordys.mybatis.BaseMapper;
import io.cordys.mybatis.lambda.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* @author jianxing
* @date 2025-02-08 16:24:22
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class ClueOwnerHistoryService {
@Resource
private BaseMapper<ClueOwner> clueOwnerMapper;
@Resource
private BaseService baseService;
@Resource
private ExtClueOwnerMapper extClueOwnerMapper;
public List<ClueOwnerListResponse> list(String clueId, String orgId) {
ClueOwner example = new ClueOwner();
example.setClueId(clueId);
List<ClueOwner> owners = clueOwnerMapper.select(example);
return buildListData(orgId, owners);
}
private List<ClueOwnerListResponse> buildListData(String orgId, List<ClueOwner> owners) {
if (CollectionUtils.isEmpty(owners)) {
return List.of();
}
Set<String> userIds = new HashSet<>();
Set<String> ownerIds = new HashSet<>();
for (ClueOwner owner : owners) {
userIds.add(owner.getOwner());
userIds.add(owner.getOperator());
ownerIds.add(owner.getOwner());
}
Map<String, UserDeptDTO> userDeptMap = baseService.getUserDeptMapByUserIds(new ArrayList<>(ownerIds), orgId);
Map<String, String> userNameMap = baseService.getUserNameMap(userIds);
return owners
.stream()
.map(item -> {
ClueOwnerListResponse clueOwner =
BeanUtils.copyBean(new ClueOwnerListResponse(), item);
UserDeptDTO userDeptDTO = userDeptMap.get(clueOwner.getOwner());
if (userDeptMap != null) {
clueOwner.setDepartmentId(userDeptDTO.getDeptId());
clueOwner.setDepartmentName(userDeptDTO.getDeptName());
}
clueOwner.setOwnerName(userNameMap.get(clueOwner.getOwner()));
clueOwner.setOperatorName(userNameMap.get(clueOwner.getOperator()));
return clueOwner;
}).toList();
}
public ClueOwner add(Clue clue, String userId) {
ClueOwner clueOwner = new ClueOwner();
clueOwner.setOwner(clue.getOwner());
clueOwner.setOperator(userId);
clueOwner.setClueId(clue.getId());
clueOwner.setCollectionTime(clue.getCollectionTime());
clueOwner.setEndTime(System.currentTimeMillis());
clueOwner.setId(IDGenerator.nextStr());
clueOwnerMapper.insert(clueOwner);
return clueOwner;
}
public void batchAdd(ClueBatchTransferRequest transferRequest, String userId) {
extClueOwnerMapper.batchAdd(transferRequest, userId);
}
public void deleteByClueIds(List<String> clueIds) {
LambdaQueryWrapper<ClueOwner> wrapper = new LambdaQueryWrapper();
wrapper.in(ClueOwner::getClueId, clueIds);
clueOwnerMapper.deleteByLambda(wrapper);
}
}

View File

@@ -18,6 +18,7 @@ import io.cordys.crm.clue.mapper.ExtClueMapper;
import io.cordys.mybatis.BaseMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -41,6 +42,8 @@ public class ClueService {
private BaseService baseService;
@Resource
private ClueFieldService clueFieldService;
@Resource
private ClueOwnerHistoryService clueOwnerHistoryService;
public List<ClueListResponse> list(CluePageRequest request, String userId, String orgId,
DeptDataPermissionDTO deptDataPermission) {
@@ -137,6 +140,14 @@ public class ClueService {
// 校验名称重复
checkUpdateExist(clue);
if (StringUtils.isNotBlank(request.getOwner())) {
Clue originCustomer = clueMapper.selectByPrimaryKey(request.getId());
if (!StringUtils.equals(request.getOwner(), originCustomer.getOwner())) {
// 如果责任人有修改,则添加责任人历史
clueOwnerHistoryService.add(originCustomer, userId);
}
}
clueMapper.update(clue);
// 更新模块字段
@@ -179,6 +190,8 @@ public class ClueService {
clueMapper.deleteByPrimaryKey(id);
// 删除客户模块字段
clueFieldService.deleteByResourceId(id);
// 删除责任人历史
clueOwnerHistoryService.deleteByClueIds(List.of(id));
}
public void batchTransfer(ClueBatchTransferRequest request) {
@@ -190,5 +203,7 @@ public class ClueService {
clueMapper.deleteByIds(ids);
// 删除客户模块字段
clueFieldService.deleteByResourceIds(ids);
// 删除责任人历史
clueOwnerHistoryService.deleteByClueIds(ids);
}
}

View File

@@ -150,7 +150,23 @@ ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE INDEX resource_id ON clue_field_blob(resource_id ASC);
CREATE INDEX idx_resource_id ON clue_field_blob(resource_id ASC);
CREATE TABLE clue_owner(
`id` VARCHAR(32) NOT NULL COMMENT 'id' ,
`clue_id` VARCHAR(32) NOT NULL COMMENT '线索id' ,
`owner` VARCHAR(32) NOT NULL COMMENT '责任人' ,
`collection_time` BIGINT NOT NULL COMMENT '领取时间' ,
`end_time` BIGINT NOT NULL COMMENT '结束时间' ,
`operator` VARCHAR(32) NOT NULL COMMENT '操作人' ,
PRIMARY KEY (id)
) COMMENT = '线索历史责任人'
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE INDEX idx_clue_id ON clue_owner(clue_id ASC);
-- set innodb lock wait timeout to default

View File

@@ -73,10 +73,7 @@ class ClueControllerTests extends BaseTest {
request.setCurrent(1);
request.setPageSize(10);
MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_PAGE, request);
Pager<List<ClueListResponse>> pageResult = getPageResult(mvcResult, ClueListResponse.class);
List<ClueListResponse> clueList = pageResult.getList();
Assertions.assertTrue(CollectionUtils.isEmpty(clueList));
this.requestPostWithOkAndReturn(DEFAULT_PAGE, request);
// 校验权限
requestPostPermissionTest(PermissionConstants.CLUE_MANAGEMENT_READ, DEFAULT_PAGE, request);

View File

@@ -0,0 +1,114 @@
package io.cordys.crm.clue.controller;
import io.cordys.common.constants.InternalUser;
import io.cordys.common.constants.PermissionConstants;
import io.cordys.crm.base.BaseTest;
import io.cordys.crm.clue.domain.Clue;
import io.cordys.crm.clue.domain.ClueOwner;
import io.cordys.crm.clue.dto.request.ClueAddRequest;
import io.cordys.crm.clue.dto.request.ClueBatchTransferRequest;
import io.cordys.crm.clue.dto.response.ClueOwnerListResponse;
import io.cordys.crm.clue.service.ClueOwnerHistoryService;
import io.cordys.crm.clue.service.ClueService;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MvcResult;
import java.util.List;
import java.util.UUID;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ClueOwnerHistoryControllerTests extends BaseTest {
private static final String BASE_PATH = "/clue/owner/history/" ;
protected static final String LIST = "list/{0}" ;
private static Clue addClue;
@Resource
private ClueOwnerHistoryService clueOwnerHistoryService;
@Resource
private ClueService clueService;
@Override
protected String getBasePath() {
return BASE_PATH;
}
@Test
@Order(0)
void testListEmpty() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(LIST, "111");
List<ClueOwnerListResponse> list = getResultDataArray(mvcResult, ClueOwnerListResponse.class);
Assertions.assertTrue(CollectionUtils.isEmpty(list));
// 校验权限
requestGetPermissionTest(PermissionConstants.CUSTOMER_MANAGEMENT_READ, LIST, "111");
}
@Test
@Order(1)
void testAdd() {
ClueAddRequest clueAddRequest = new ClueAddRequest();
clueAddRequest.setName(UUID.randomUUID().toString());
clueAddRequest.setOwner(InternalUser.ADMIN.getValue());
addClue = clueService.add(clueAddRequest, InternalUser.ADMIN.getValue(), DEFAULT_ORGANIZATION_ID);
ClueOwner clueOwner = clueOwnerHistoryService.add(addClue, InternalUser.ADMIN.getValue());
// 校验成功数据
Assertions.assertEquals(clueOwner.getClueId(), addClue.getId());
Assertions.assertEquals(clueOwner.getOperator(), InternalUser.ADMIN.getValue());
Assertions.assertEquals(clueOwner.getCollectionTime(), addClue.getCollectionTime());
Assertions.assertEquals(clueOwner.getOwner(), addClue.getOwner());
}
@Test
@Order(2)
void testBatchAdd() {
ClueBatchTransferRequest transferRequest = new ClueBatchTransferRequest();
transferRequest.setIds(List.of(addClue.getId()));
transferRequest.setOwner(PERMISSION_USER_NAME);
clueOwnerHistoryService.batchAdd(transferRequest, InternalUser.ADMIN.getValue());
List<ClueOwnerListResponse> list = clueOwnerHistoryService.list(addClue.getId(), DEFAULT_ORGANIZATION_ID);
for (ClueOwnerListResponse clueOwner : list) {
// 校验成功数据
Assertions.assertEquals(clueOwner.getClueId(), addClue.getId());
Assertions.assertEquals(clueOwner.getOperator(), InternalUser.ADMIN.getValue());
Assertions.assertEquals(clueOwner.getCollectionTime(), addClue.getCollectionTime());
Assertions.assertEquals(clueOwner.getOwner(), addClue.getOwner());
}
}
@Test
@Order(3)
void testList() throws Exception {
MvcResult mvcResult = this.requestGetWithOkAndReturn(LIST, addClue.getId());
List<ClueOwnerListResponse> list = getResultDataArray(mvcResult, ClueOwnerListResponse.class);
list.forEach(clueOwner -> {
// 校验成功数据
Assertions.assertEquals(clueOwner.getClueId(), addClue.getId());
Assertions.assertEquals(clueOwner.getOperator(), InternalUser.ADMIN.getValue());
Assertions.assertEquals(clueOwner.getCollectionTime(), addClue.getCollectionTime());
Assertions.assertEquals(clueOwner.getOwner(), addClue.getOwner());
Assertions.assertNotNull(clueOwner.getDepartmentName());
Assertions.assertNotNull(clueOwner.getOwnerName());
Assertions.assertNotNull(clueOwner.getOwnerName());
});
// 校验权限
requestGetPermissionTest(PermissionConstants.CUSTOMER_MANAGEMENT_READ, LIST, "111");
}
@Test
@Order(10)
void batchDelete() {
clueOwnerHistoryService.deleteByClueIds(List.of(addClue.getId()));
List<ClueOwnerListResponse> list = clueOwnerHistoryService.list(addClue.getId(), DEFAULT_ORGANIZATION_ID);
Assertions.assertTrue(list.isEmpty());
}
}

View File

@@ -79,10 +79,7 @@ class CustomerControllerTests extends BaseTest {
request.setCurrent(1);
request.setPageSize(10);
MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_PAGE, request);
Pager<List<CustomerListResponse>> pageResult = getPageResult(mvcResult, CustomerListResponse.class);
List<CustomerListResponse> customerList = pageResult.getList();
Assertions.assertTrue(CollectionUtils.isEmpty(customerList));
this.requestPostWithOkAndReturn(DEFAULT_PAGE, request);
// 校验权限
requestPostPermissionTest(PermissionConstants.CUSTOMER_MANAGEMENT_READ, DEFAULT_PAGE, request);