From 47225d652c79bd0bb6d482d69fb7fefb03c375fd Mon Sep 17 00:00:00 2001 From: AgAngle <1323481023@qq.com> Date: Wed, 12 Mar 2025 16:55:26 +0800 Subject: [PATCH] feat: clue owner history --- .../ClueOwnerHistoryController.java | 35 ++++++ .../io/cordys/crm/clue/domain/ClueOwner.java | 37 ++++++ .../dto/response/ClueOwnerListResponse.java | 30 +++++ .../cordys/crm/clue/mapper/ExtClueMapper.xml | 2 +- .../crm/clue/mapper/ExtClueOwnerMapper.java | 14 +++ .../crm/clue/mapper/ExtClueOwnerMapper.xml | 15 +++ .../clue/service/ClueOwnerHistoryService.java | 96 +++++++++++++++ .../cordys/crm/clue/service/ClueService.java | 15 +++ .../migration/1.0.0/ddl/V1.0.0_5__clue.sql | 18 ++- .../clue/controller/ClueControllerTests.java | 5 +- .../ClueOwnerHistoryControllerTests.java | 114 ++++++++++++++++++ .../controller/CustomerControllerTests.java | 5 +- 12 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/controller/ClueOwnerHistoryController.java create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/domain/ClueOwner.java create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/dto/response/ClueOwnerListResponse.java create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.java create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.xml create mode 100644 backend/crm/src/main/java/io/cordys/crm/clue/service/ClueOwnerHistoryService.java create mode 100644 backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueOwnerHistoryControllerTests.java diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/controller/ClueOwnerHistoryController.java b/backend/crm/src/main/java/io/cordys/crm/clue/controller/ClueOwnerHistoryController.java new file mode 100644 index 000000000..12457bc79 --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/controller/ClueOwnerHistoryController.java @@ -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 list(@PathVariable String clueId) { + return clueOwnerHistoryService.list(clueId, OrganizationContext.getOrganizationId()); + } +} diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/domain/ClueOwner.java b/backend/crm/src/main/java/io/cordys/crm/clue/domain/ClueOwner.java new file mode 100644 index 000000000..96b83d223 --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/domain/ClueOwner.java @@ -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; +} diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/dto/response/ClueOwnerListResponse.java b/backend/crm/src/main/java/io/cordys/crm/clue/dto/response/ClueOwnerListResponse.java new file mode 100644 index 000000000..0f58efe7d --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/dto/response/ClueOwnerListResponse.java @@ -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; + +} diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueMapper.xml b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueMapper.xml index cea419bbb..dfc37fb57 100644 --- a/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueMapper.xml +++ b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueMapper.xml @@ -5,7 +5,7 @@ update `clue` set owner = #{request.owner} - where id in + where owner != #{request.owner} and id in #{id} diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.java b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.java new file mode 100644 index 000000000..7d2d3cd54 --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.java @@ -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); +} diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.xml b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.xml new file mode 100644 index 000000000..615d8c751 --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/mapper/ExtClueOwnerMapper.xml @@ -0,0 +1,15 @@ + + + + + + + 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 + + #{id} + + + \ No newline at end of file diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueOwnerHistoryService.java b/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueOwnerHistoryService.java new file mode 100644 index 000000000..cd3c552a4 --- /dev/null +++ b/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueOwnerHistoryService.java @@ -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 clueOwnerMapper; + @Resource + private BaseService baseService; + @Resource + private ExtClueOwnerMapper extClueOwnerMapper; + + public List list(String clueId, String orgId) { + ClueOwner example = new ClueOwner(); + example.setClueId(clueId); + List owners = clueOwnerMapper.select(example); + return buildListData(orgId, owners); + } + + private List buildListData(String orgId, List owners) { + if (CollectionUtils.isEmpty(owners)) { + return List.of(); + } + Set userIds = new HashSet<>(); + Set ownerIds = new HashSet<>(); + + for (ClueOwner owner : owners) { + userIds.add(owner.getOwner()); + userIds.add(owner.getOperator()); + ownerIds.add(owner.getOwner()); + } + + Map userDeptMap = baseService.getUserDeptMapByUserIds(new ArrayList<>(ownerIds), orgId); + + Map 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 clueIds) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.in(ClueOwner::getClueId, clueIds); + clueOwnerMapper.deleteByLambda(wrapper); + } +} \ No newline at end of file diff --git a/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueService.java b/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueService.java index f131b8e7e..620d34453 100644 --- a/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueService.java +++ b/backend/crm/src/main/java/io/cordys/crm/clue/service/ClueService.java @@ -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 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); } } \ No newline at end of file diff --git a/backend/crm/src/main/resources/migration/1.0.0/ddl/V1.0.0_5__clue.sql b/backend/crm/src/main/resources/migration/1.0.0/ddl/V1.0.0_5__clue.sql index 124464f4a..c2bd16c45 100644 --- a/backend/crm/src/main/resources/migration/1.0.0/ddl/V1.0.0_5__clue.sql +++ b/backend/crm/src/main/resources/migration/1.0.0/ddl/V1.0.0_5__clue.sql @@ -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 diff --git a/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueControllerTests.java b/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueControllerTests.java index 220318a31..4d55e8bde 100644 --- a/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueControllerTests.java +++ b/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueControllerTests.java @@ -73,10 +73,7 @@ class ClueControllerTests extends BaseTest { request.setCurrent(1); request.setPageSize(10); - MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_PAGE, request); - Pager> pageResult = getPageResult(mvcResult, ClueListResponse.class); - List clueList = pageResult.getList(); - Assertions.assertTrue(CollectionUtils.isEmpty(clueList)); + this.requestPostWithOkAndReturn(DEFAULT_PAGE, request); // 校验权限 requestPostPermissionTest(PermissionConstants.CLUE_MANAGEMENT_READ, DEFAULT_PAGE, request); diff --git a/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueOwnerHistoryControllerTests.java b/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueOwnerHistoryControllerTests.java new file mode 100644 index 000000000..59d0d5b4b --- /dev/null +++ b/backend/crm/src/test/java/io/cordys/crm/clue/controller/ClueOwnerHistoryControllerTests.java @@ -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 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 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 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 list = clueOwnerHistoryService.list(addClue.getId(), DEFAULT_ORGANIZATION_ID); + Assertions.assertTrue(list.isEmpty()); + } +} \ No newline at end of file diff --git a/backend/crm/src/test/java/io/cordys/crm/customer/controller/CustomerControllerTests.java b/backend/crm/src/test/java/io/cordys/crm/customer/controller/CustomerControllerTests.java index 663c56662..826f58856 100644 --- a/backend/crm/src/test/java/io/cordys/crm/customer/controller/CustomerControllerTests.java +++ b/backend/crm/src/test/java/io/cordys/crm/customer/controller/CustomerControllerTests.java @@ -79,10 +79,7 @@ class CustomerControllerTests extends BaseTest { request.setCurrent(1); request.setPageSize(10); - MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_PAGE, request); - Pager> pageResult = getPageResult(mvcResult, CustomerListResponse.class); - List customerList = pageResult.getList(); - Assertions.assertTrue(CollectionUtils.isEmpty(customerList)); + this.requestPostWithOkAndReturn(DEFAULT_PAGE, request); // 校验权限 requestPostPermissionTest(PermissionConstants.CUSTOMER_MANAGEMENT_READ, DEFAULT_PAGE, request);