Commit 0d149e9a96c3c345fa311faa6ef22bfc995b8ebf

Authored by YunaiV
1 parent 54b3b586

fix(pms): 修复待规划个人排序重复保存的唯一键冲突

物理清理个人排序及历史逻辑删除记录,保留租户隔离,避免工作项反复拖回待规划时报错。
补齐排序唯一索引测试约束与清理脚本,增加重复保存、历史记录及租户隔离的回归测试。

验证:56 项相关测试、独立 MySQL 6 项回归测试通过;四路独立评审通过。
反馈:https://t.zsxq.com/V48Hl
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemUserSortMapper.java
... ... @@ -3,7 +3,9 @@ package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem;
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 5 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO;
  6 +import org.apache.ibatis.annotations.Delete;
6 7 import org.apache.ibatis.annotations.Mapper;
  8 +import org.apache.ibatis.annotations.Param;
7 9  
8 10 import java.util.List;
9 11  
... ... @@ -18,20 +20,14 @@ public interface PmsWorkItemUserSortMapper extends BaseMapperX<PmsWorkItemUserSo
18 20 .orderByAsc(PmsWorkItemUserSortDO::getId));
19 21 }
20 22  
21   - default void deleteByProjectIdAndUserId(Long projectId, Long userId) {
22   - delete(new LambdaQueryWrapperX<PmsWorkItemUserSortDO>()
23   - .eq(PmsWorkItemUserSortDO::getProjectId, projectId)
24   - .eq(PmsWorkItemUserSortDO::getUserId, userId));
25   - }
  23 + // 个人排序无需保留删除历史,物理删除避免重复排序时逻辑删除记录发生唯一键冲突
  24 + @Delete("DELETE FROM pms_work_item_user_sort WHERE project_id = #{projectId} AND user_id = #{userId}")
  25 + void deleteByProjectIdAndUserId(@Param("projectId") Long projectId, @Param("userId") Long userId);
26 26  
27   - default void deleteByWorkItemId(Long workItemId) {
28   - delete(new LambdaQueryWrapperX<PmsWorkItemUserSortDO>()
29   - .eq(PmsWorkItemUserSortDO::getWorkItemId, workItemId));
30   - }
  27 + @Delete("DELETE FROM pms_work_item_user_sort WHERE work_item_id = #{workItemId}")
  28 + void deleteByWorkItemId(@Param("workItemId") Long workItemId);
31 29  
32   - default void deleteByProjectId(Long projectId) {
33   - delete(new LambdaQueryWrapperX<PmsWorkItemUserSortDO>()
34   - .eq(PmsWorkItemUserSortDO::getProjectId, projectId));
35   - }
  30 + @Delete("DELETE FROM pms_work_item_user_sort WHERE project_id = #{projectId}")
  31 + void deleteByProjectId(@Param("projectId") Long projectId);
36 32  
37 33 }
... ...
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pms.service.pm.workitem;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
  4 +import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  6 +import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
  7 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  8 +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO;
  9 +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper;
  10 +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  11 +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
  12 +import jakarta.annotation.Resource;
  13 +import org.junit.jupiter.api.AfterEach;
  14 +import org.junit.jupiter.api.BeforeEach;
  15 +import org.junit.jupiter.api.Test;
  16 +import org.springframework.boot.test.context.TestConfiguration;
  17 +import org.springframework.context.annotation.Bean;
  18 +import org.springframework.context.annotation.Import;
  19 +import org.springframework.jdbc.core.JdbcTemplate;
  20 +
  21 +import javax.sql.DataSource;
  22 +import java.util.Arrays;
  23 +import java.util.Collections;
  24 +import java.util.List;
  25 +
  26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  27 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
  28 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  29 +import static org.junit.jupiter.api.Assertions.assertEquals;
  30 +
  31 +/**
  32 + * {@link PmsWorkItemUserSortServiceImpl} 的单元测试类
  33 + *
  34 + * @author 芋道源码
  35 + */
  36 +@Import({PmsWorkItemUserSortServiceImpl.class, PmsWorkItemUserSortServiceImplTest.TenantTestConfiguration.class})
  37 +public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
  38 +
  39 + @Resource
  40 + private PmsWorkItemUserSortServiceImpl workItemUserSortService;
  41 + @Resource
  42 + private PmsWorkItemUserSortMapper workItemUserSortMapper;
  43 + @Resource
  44 + private JdbcTemplate jdbcTemplate;
  45 + @Resource
  46 + private TenantLineInnerInterceptor tenantLineInnerInterceptor;
  47 +
  48 + @BeforeEach
  49 + public void before() {
  50 + TenantContextHolder.setTenantId(0L);
  51 + }
  52 +
  53 + @AfterEach
  54 + public void after() {
  55 + TenantContextHolder.clear();
  56 + }
  57 +
  58 + @Test
  59 + public void testUpdateWorkItemUserSort_repeated() {
  60 + // 准备参数
  61 + Long projectId = randomLongId();
  62 + Long userId = randomLongId();
  63 + Long firstId = randomLongId();
  64 + Long secondId = randomLongId();
  65 +
  66 + // 调用:连续保存三次,覆盖同一排序记录重复删除的情况
  67 + workItemUserSortService.updateWorkItemUserSort(projectId, Arrays.asList(firstId, secondId), userId);
  68 + workItemUserSortService.updateWorkItemUserSort(projectId, Arrays.asList(secondId, firstId), userId);
  69 + workItemUserSortService.updateWorkItemUserSort(projectId, Arrays.asList(firstId, secondId), userId);
  70 +
  71 + // 断言
  72 + List<PmsWorkItemUserSortDO> userSorts = workItemUserSortMapper.selectListByProjectIdAndUserId(projectId, userId);
  73 + assertEquals(Arrays.asList(firstId, secondId), convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId));
  74 + assertEquals(Arrays.asList(1, 2), convertList(userSorts, PmsWorkItemUserSortDO::getSort));
  75 + assertEquals(2, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  76 + }
  77 +
  78 + @Test
  79 + public void testUpdateWorkItemUserSort_deletedRecords() {
  80 + // mock 数据
  81 + Long projectId = randomLongId();
  82 + Long userId = randomLongId();
  83 + Long workItemId = randomLongId();
  84 + insertUserSort(projectId, workItemId, userId, false);
  85 + insertUserSort(projectId, workItemId, userId, true);
  86 + PmsWorkItemUserSortDO otherUser = insertUserSort(projectId, workItemId, randomLongId(), false);
  87 + PmsWorkItemUserSortDO otherProject = insertUserSort(randomLongId(), workItemId, userId, false);
  88 +
  89 + // 调用
  90 + workItemUserSortService.updateWorkItemUserSort(projectId, Collections.singletonList(workItemId), userId);
  91 +
  92 + // 断言:历史记录被清理,其他用户和项目的排序不受影响
  93 + assertEquals(1, workItemUserSortMapper.selectListByProjectIdAndUserId(projectId, userId).size());
  94 + assertPojoEquals(otherUser, workItemUserSortMapper.selectById(otherUser.getId()), "createTime", "updateTime");
  95 + assertPojoEquals(otherProject, workItemUserSortMapper.selectById(otherProject.getId()), "createTime", "updateTime");
  96 + assertEquals(3, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  97 + }
  98 +
  99 + @Test
  100 + public void testUpdateWorkItemUserSort_empty() {
  101 + // mock 数据
  102 + Long projectId = randomLongId();
  103 + Long userId = randomLongId();
  104 + Long workItemId = randomLongId();
  105 + insertUserSort(projectId, workItemId, userId, false);
  106 + insertUserSort(projectId, workItemId, userId, true);
  107 +
  108 + // 调用
  109 + workItemUserSortService.updateWorkItemUserSort(projectId, Collections.emptyList(), userId);
  110 +
  111 + // 断言
  112 + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  113 + }
  114 +
  115 + @Test
  116 + public void testDeleteWorkItemUserSortByWorkItemId_success() {
  117 + // mock 数据
  118 + Long projectId = randomLongId();
  119 + Long userId = randomLongId();
  120 + Long workItemId = randomLongId();
  121 + insertUserSort(projectId, workItemId, userId, false);
  122 + insertUserSort(projectId, workItemId, userId, true);
  123 + PmsWorkItemUserSortDO other = insertUserSort(projectId, randomLongId(), userId, false);
  124 +
  125 + // 调用
  126 + workItemUserSortService.deleteWorkItemUserSortByWorkItemId(workItemId);
  127 +
  128 + // 断言
  129 + assertPojoEquals(other, workItemUserSortMapper.selectById(other.getId()), "createTime", "updateTime");
  130 + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  131 + }
  132 +
  133 + @Test
  134 + public void testDeleteWorkItemUserSortByProjectId_success() {
  135 + // mock 数据
  136 + Long projectId = randomLongId();
  137 + Long userId = randomLongId();
  138 + Long workItemId = randomLongId();
  139 + insertUserSort(projectId, workItemId, userId, false);
  140 + insertUserSort(projectId, workItemId, userId, true);
  141 + PmsWorkItemUserSortDO other = insertUserSort(randomLongId(), workItemId, userId, false);
  142 +
  143 + // 调用
  144 + workItemUserSortService.deleteWorkItemUserSortByProjectId(projectId);
  145 +
  146 + // 断言
  147 + assertPojoEquals(other, workItemUserSortMapper.selectById(other.getId()), "createTime", "updateTime");
  148 + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  149 + }
  150 +
  151 + @Test
  152 + public void testDeleteWorkItemUserSort_tenantIsolation() {
  153 + // mock 数据:两个租户使用相同的业务编号
  154 + Long projectId = randomLongId();
  155 + Long userId = randomLongId();
  156 + Long workItemId = randomLongId();
  157 + insertUserSort(projectId, workItemId, userId, false);
  158 + TenantContextHolder.setTenantId(1L);
  159 + insertUserSort(projectId, workItemId, userId, false);
  160 + TenantContextHolder.setTenantId(0L);
  161 +
  162 + // 调用,并断言:三个清理入口均保留其他租户的记录
  163 + workItemUserSortService.updateWorkItemUserSort(projectId, Collections.emptyList(), userId);
  164 + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  165 + insertUserSort(projectId, workItemId, userId, false);
  166 + workItemUserSortService.deleteWorkItemUserSortByWorkItemId(workItemId);
  167 + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  168 + insertUserSort(projectId, workItemId, userId, false);
  169 + workItemUserSortService.deleteWorkItemUserSortByProjectId(projectId);
  170 + assertEquals(1, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pms_work_item_user_sort", Integer.class));
  171 + assertEquals(1L, jdbcTemplate.queryForObject("SELECT tenant_id FROM pms_work_item_user_sort", Long.class));
  172 + }
  173 +
  174 + private PmsWorkItemUserSortDO insertUserSort(Long projectId, Long workItemId, Long userId, boolean deleted) {
  175 + PmsWorkItemUserSortDO userSort = new PmsWorkItemUserSortDO().setProjectId(projectId)
  176 + .setWorkItemId(workItemId).setUserId(userId).setSort(1);
  177 + userSort.setDeleted(deleted);
  178 + workItemUserSortMapper.insert(userSort);
  179 + return userSort;
  180 + }
  181 +
  182 + @TestConfiguration(proxyBeanMethods = false)
  183 + static class TenantTestConfiguration {
  184 +
  185 + @Bean
  186 + public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  187 + return new JdbcTemplate(dataSource);
  188 + }
  189 +
  190 + @Bean
  191 + public TenantLineInnerInterceptor tenantLineInnerInterceptor(MybatisPlusInterceptor interceptor) {
  192 + TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(
  193 + new TenantDatabaseInterceptor(new TenantProperties()));
  194 + MyBatisUtils.addInterceptor(interceptor, inner, 0);
  195 + return inner;
  196 + }
  197 +
  198 + }
  199 +
  200 +}
... ...
yudao-module-pms/src/test/resources/sql/clean.sql
... ... @@ -5,6 +5,7 @@ DELETE FROM &quot;pms_work_item_comment&quot;;
5 5 DELETE FROM "pms_work_item_label";
6 6 DELETE FROM "pms_work_item_work_log";
7 7 DELETE FROM "pms_work_item_member";
  8 +DELETE FROM "pms_work_item_user_sort";
8 9 DELETE FROM "pms_work_item";
9 10 DELETE FROM "pms_work_item_status";
10 11 DELETE FROM "pms_iteration";
... ...
yudao-module-pms/src/test/resources/sql/create_tables.sql
... ... @@ -245,7 +245,8 @@ CREATE TABLE IF NOT EXISTS &quot;pms_work_item_user_sort&quot; (
245 245 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
246 246 "deleted" bit NOT NULL DEFAULT FALSE,
247 247 "tenant_id" bigint NOT NULL DEFAULT 0,
248   - PRIMARY KEY ("id")
  248 + PRIMARY KEY ("id"),
  249 + CONSTRAINT "uk_tenant_project_work_item_user_deleted" UNIQUE ("tenant_id", "project_id", "work_item_id", "user_id", "deleted")
249 250 );
250 251  
251 252 CREATE TABLE IF NOT EXISTS "pms_work_item_work_log" (
... ...