Commit 91676882a89c831273775c5064ea785ed1fb4b22

Authored by zichun
1 parent 74a4622f

feat(usr): UserUpdateService 完整实现(校验 + 字段写入 + 权限差集) REQ-USR-003

backend/src/main/java/com/xly/erp/module/usr/service/UserUpdateService.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.xly.erp.module.usr.dto.UpdateUserReq;
  4 +import com.xly.erp.module.usr.vo.UserDetailVo;
  5 +
  6 +public interface UserUpdateService {
  7 + /**
  8 + * REQ-USR-003 PUT /api/v1/users/{userId}:部分字段更新 + 权限分类增量差集。
  9 + *
  10 + * @throws com.xly.erp.common.exception.BizException
  11 + * 40004 employee/permissionCategory 不存在 / 40302 自我停用 / 40401 用户不存在 / 40902 用户号冲突
  12 + */
  13 + UserDetailVo update(Integer userId, UpdateUserReq req,
  14 + Integer operatorUserId, String operatorUsername);
  15 +}
... ...
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserUpdateServiceImpl.java 0 → 100644
  1 +package com.xly.erp.module.usr.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
  4 +import com.xly.erp.common.exception.BizException;
  5 +import com.xly.erp.common.response.ErrorCode;
  6 +import com.xly.erp.module.usr.dto.UpdateUserReq;
  7 +import com.xly.erp.module.usr.entity.SysEmployee;
  8 +import com.xly.erp.module.usr.entity.SysUser;
  9 +import com.xly.erp.module.usr.entity.SysUserPermissionCategory;
  10 +import com.xly.erp.module.usr.mapper.SysEmployeeMapper;
  11 +import com.xly.erp.module.usr.mapper.SysPermissionCategoryMapper;
  12 +import com.xly.erp.module.usr.mapper.SysUserMapper;
  13 +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper;
  14 +import com.xly.erp.module.usr.service.UserDetailService;
  15 +import com.xly.erp.module.usr.service.UserUpdateService;
  16 +import com.xly.erp.module.usr.vo.UserDetailVo;
  17 +import lombok.RequiredArgsConstructor;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.stereotype.Service;
  20 +import org.springframework.transaction.annotation.Transactional;
  21 +
  22 +import java.time.LocalDateTime;
  23 +import java.util.HashSet;
  24 +import java.util.LinkedHashSet;
  25 +import java.util.List;
  26 +import java.util.Set;
  27 +
  28 +@Service
  29 +@RequiredArgsConstructor
  30 +@Slf4j
  31 +public class UserUpdateServiceImpl implements UserUpdateService {
  32 +
  33 + private final SysUserMapper userMapper;
  34 + private final SysEmployeeMapper employeeMapper;
  35 + private final SysPermissionCategoryMapper permissionCategoryMapper;
  36 + private final SysUserPermissionCategoryMapper upcMapper;
  37 + private final UserDetailService userDetailService;
  38 +
  39 + @Override
  40 + @Transactional
  41 + public UserDetailVo update(Integer userId, UpdateUserReq req,
  42 + Integer operatorUserId, String operatorUsername) {
  43 + // 1. 存在性
  44 + SysUser existing = userMapper.selectById(userId);
  45 + if (existing == null) {
  46 + throw new BizException(ErrorCode.USER_NOT_FOUND, "用户不存在");
  47 + }
  48 +
  49 + // 2. 自我停用守卫
  50 + if (Boolean.TRUE.equals(req.getIsDeleted()) && userId.equals(operatorUserId)) {
  51 + throw new BizException(ErrorCode.USER_FORBIDDEN_SELF_DEACTIVATE,
  52 + "不允许停用当前登录用户自己");
  53 + }
  54 +
  55 + // 3. userCode 唯一(排除自身)
  56 + if (req.getUserCode() != null
  57 + && !req.getUserCode().equals(existing.getSUserCode())
  58 + && userMapper.existsByUserCodeExcludingId(req.getUserCode(), userId)) {
  59 + throw new BizException(ErrorCode.CONFLICT_USERCODE, "用户号已被占用");
  60 + }
  61 +
  62 + // 4. employeeId 外键(仅正整数才查)
  63 + if (req.getEmployeeId() != null && req.getEmployeeId() > 0) {
  64 + SysEmployee emp = employeeMapper.selectById(req.getEmployeeId());
  65 + if (emp == null || Integer.valueOf(1).equals(emp.getIIsDeleted())) {
  66 + throw new BizException(ErrorCode.COMPANY_NOT_FOUND, "指定的员工不存在或已删除");
  67 + }
  68 + }
  69 +
  70 + // 5. permissionCategoryIds 外键(dedup 后校验)
  71 + Set<Integer> targetPcSet = null;
  72 + if (req.getPermissionCategoryIds() != null) {
  73 + targetPcSet = new LinkedHashSet<>(req.getPermissionCategoryIds());
  74 + if (!targetPcSet.isEmpty()) {
  75 + int active = permissionCategoryMapper.countActiveByIds(
  76 + new java.util.ArrayList<>(targetPcSet));
  77 + if (active != targetPcSet.size()) {
  78 + throw new BizException(ErrorCode.COMPANY_NOT_FOUND,
  79 + "指定的权限分类含不存在或已删除项");
  80 + }
  81 + }
  82 + }
  83 +
  84 + // 6. 写 sys_user 字段
  85 + UpdateWrapper<SysUser> uw = new UpdateWrapper<>();
  86 + uw.eq("iIncrement", userId);
  87 +
  88 + if (req.getUserCode() != null) uw.set("sUserCode", req.getUserCode());
  89 + if (req.getUserType() != null) uw.set("sUserType", req.getUserType());
  90 + if (req.getLanguage() != null) uw.set("sLanguage", req.getLanguage());
  91 + if (req.getCanEditDocument() != null)
  92 + uw.set("iCanEditDocument", req.getCanEditDocument() ? 1 : 0);
  93 + if (req.getIsDeleted() != null)
  94 + uw.set("iIsDeleted", req.getIsDeleted() ? 1 : 0);
  95 + if (req.getEmployeeId() != null) {
  96 + if (req.getEmployeeId() == 0) {
  97 + uw.set("iEmployeeId", null); // 解除关联
  98 + } else {
  99 + uw.set("iEmployeeId", req.getEmployeeId());
  100 + }
  101 + }
  102 + uw.set("sUpdatedBy", operatorUsername);
  103 + uw.set("tUpdatedDate", LocalDateTime.now());
  104 +
  105 + userMapper.update(null, uw);
  106 +
  107 + // 7. 权限分类增量差集
  108 + if (targetPcSet != null) {
  109 + List<Integer> currentList = upcMapper.selectPermissionCategoryIdsByUserId(userId);
  110 + Set<Integer> currentSet = new HashSet<>(currentList);
  111 +
  112 + Set<Integer> toRemove = new HashSet<>(currentSet);
  113 + toRemove.removeAll(targetPcSet);
  114 +
  115 + Set<Integer> toAdd = new LinkedHashSet<>(targetPcSet);
  116 + toAdd.removeAll(currentSet);
  117 +
  118 + if (!toRemove.isEmpty()) {
  119 + upcMapper.deleteByUserAndCategoryIds(userId, new java.util.ArrayList<>(toRemove));
  120 + }
  121 + for (Integer pcId : toAdd) {
  122 + SysUserPermissionCategory link = new SysUserPermissionCategory();
  123 + link.setIUserId(userId);
  124 + link.setIPermissionCategoryId(pcId);
  125 + link.setSGrantedBy(operatorUsername);
  126 + upcMapper.insert(link);
  127 + }
  128 + log.info("[user-update] userId={} pc.toRemove={} pc.toAdd={}",
  129 + userId, toRemove.size(), toAdd.size());
  130 + }
  131 +
  132 + log.info("[user-update] userId={} byOperator={} 完成", userId, operatorUsername);
  133 + return userDetailService.getById(userId);
  134 + }
  135 +}
... ...
backend/src/test/java/com/xly/erp/module/usr/service/UserUpdateServiceImplTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.xly.erp.common.exception.BizException;
  4 +import com.xly.erp.common.response.ErrorCode;
  5 +import com.xly.erp.module.usr.dto.UpdateUserReq;
  6 +import com.xly.erp.module.usr.entity.SysUser;
  7 +import com.xly.erp.module.usr.entity.SysUserPermissionCategory;
  8 +import com.xly.erp.module.usr.mapper.SysUserMapper;
  9 +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper;
  10 +import com.xly.erp.module.usr.support.LoginTestSeeder;
  11 +import com.xly.erp.module.usr.vo.UserDetailVo;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.springframework.beans.factory.annotation.Autowired;
  15 +import org.springframework.boot.test.context.SpringBootTest;
  16 +import org.springframework.test.context.ActiveProfiles;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.HashSet;
  20 +import java.util.List;
  21 +import java.util.Set;
  22 +
  23 +import static org.junit.jupiter.api.Assertions.*;
  24 +
  25 +@SpringBootTest
  26 +@ActiveProfiles("test")
  27 +class UserUpdateServiceImplTest {
  28 +
  29 + @Autowired private UserUpdateService service;
  30 + @Autowired private SysUserMapper userMapper;
  31 + @Autowired private SysUserPermissionCategoryMapper upcMapper;
  32 + @Autowired private LoginTestSeeder seeder;
  33 +
  34 + private LoginTestSeeder.Fixture fx;
  35 +
  36 + @BeforeEach
  37 + void setUp() {
  38 + fx = seeder.reset();
  39 + }
  40 +
  41 + private UpdateUserReq req() {
  42 + return new UpdateUserReq();
  43 + }
  44 +
  45 + // ===== 校验路径(Task 6) =====
  46 +
  47 + @Test
  48 + void update_unknownUserId_throws40401() {
  49 + BizException e = assertThrows(BizException.class,
  50 + () -> service.update(99999, req(), fx.adminId(), LoginTestSeeder.USER_ADMIN));
  51 + assertEquals(ErrorCode.USER_NOT_FOUND, e.getCode());
  52 + }
  53 +
  54 + @Test
  55 + void update_selfDeactivate_throws40302() {
  56 + UpdateUserReq r = req();
  57 + r.setIsDeleted(true);
  58 + BizException e = assertThrows(BizException.class,
  59 + () -> service.update(fx.adminId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN));
  60 + assertEquals(ErrorCode.USER_FORBIDDEN_SELF_DEACTIVATE, e.getCode());
  61 + }
  62 +
  63 + @Test
  64 + void update_userCodeConflict_throws40902() {
  65 + UpdateUserReq r = req();
  66 + r.setUserCode("U001"); // alice's code
  67 + BizException e = assertThrows(BizException.class,
  68 + () -> service.update(fx.adminId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN));
  69 + assertEquals(ErrorCode.CONFLICT_USERCODE, e.getCode());
  70 + }
  71 +
  72 + @Test
  73 + void update_employeeIdNotFound_throws40004() {
  74 + UpdateUserReq r = req();
  75 + r.setEmployeeId(99999);
  76 + BizException e = assertThrows(BizException.class,
  77 + () -> service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN));
  78 + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode());
  79 + }
  80 +
  81 + @Test
  82 + void update_permissionCategoryNotFound_throws40004_rollsBack() {
  83 + UpdateUserReq r = req();
  84 + r.setUserCode("U_NEW");
  85 + r.setPermissionCategoryIds(List.of(99999));
  86 + BizException e = assertThrows(BizException.class,
  87 + () -> service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN));
  88 + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode());
  89 + // 回滚:sys_user.sUserCode 不应被改
  90 + SysUser after = userMapper.selectById(fx.aliceId());
  91 + assertEquals("U001", after.getSUserCode());
  92 + }
  93 +
  94 + // ===== 字段写入(Task 7) =====
  95 +
  96 + @Test
  97 + void update_userCode_only_persisted_otherFieldsUnchanged() {
  98 + UpdateUserReq r = req();
  99 + r.setUserCode("U_NEW");
  100 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  101 +
  102 + SysUser db = userMapper.selectById(fx.aliceId());
  103 + assertEquals("U_NEW", db.getSUserCode());
  104 + assertEquals(LoginTestSeeder.USER_OK, db.getSUsername(), "用户名不变");
  105 + assertEquals(LoginTestSeeder.USER_ADMIN, db.getSUpdatedBy());
  106 + assertNotNull(db.getTUpdatedDate());
  107 + }
  108 +
  109 + @Test
  110 + void update_userType_language_canEditDocument() {
  111 + UpdateUserReq r = req();
  112 + r.setUserType("SUPER_ADMIN");
  113 + r.setLanguage("en-US");
  114 + r.setCanEditDocument(true);
  115 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  116 +
  117 + SysUser db = userMapper.selectById(fx.aliceId());
  118 + assertEquals("SUPER_ADMIN", db.getSUserType());
  119 + assertEquals("en-US", db.getSLanguage());
  120 + assertEquals(1, db.getICanEditDocument());
  121 + }
  122 +
  123 + @Test
  124 + void update_employeeId_positiveInteger_setsToValue() {
  125 + UpdateUserReq r = req();
  126 + r.setEmployeeId(fx.employeeId());
  127 + service.update(fx.adminId(), r, fx.adminId() + 1000, LoginTestSeeder.USER_ADMIN);
  128 + // operatorUserId 不等于 adminId 才能避开自我停用守卫(本测试未触发,但保持参数有效)
  129 + SysUser db = userMapper.selectById(fx.adminId());
  130 + assertEquals(fx.employeeId(), db.getIEmployeeId());
  131 + }
  132 +
  133 + @Test
  134 + void update_employeeId_zero_setsToNull() {
  135 + UpdateUserReq r = req();
  136 + r.setEmployeeId(0);
  137 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  138 + SysUser db = userMapper.selectById(fx.aliceId());
  139 + assertNull(db.getIEmployeeId());
  140 + }
  141 +
  142 + @Test
  143 + void update_employeeId_unchanged_preservesOriginalValue() {
  144 + UpdateUserReq r = req();
  145 + // 不设 employeeId
  146 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  147 + SysUser db = userMapper.selectById(fx.aliceId());
  148 + assertEquals(fx.employeeId(), db.getIEmployeeId());
  149 + }
  150 +
  151 + @Test
  152 + void update_isDeleted_true_marksUserDeleted() {
  153 + UpdateUserReq r = req();
  154 + r.setIsDeleted(true);
  155 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  156 + SysUser db = userMapper.selectById(fx.aliceId());
  157 + assertEquals(1, db.getIIsDeleted());
  158 + }
  159 +
  160 + @Test
  161 + void update_isDeleted_false_revivesUser() {
  162 + UpdateUserReq r = req();
  163 + r.setIsDeleted(false);
  164 + service.update(fx.bobDeletedId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  165 + SysUser db = userMapper.selectById(fx.bobDeletedId());
  166 + assertEquals(0, db.getIIsDeleted());
  167 + }
  168 +
  169 + @Test
  170 + void update_emptyRequest_onlyUpdatesAuditFields() {
  171 + service.update(fx.aliceId(), req(), fx.adminId(), LoginTestSeeder.USER_ADMIN);
  172 + SysUser db = userMapper.selectById(fx.aliceId());
  173 + assertEquals(LoginTestSeeder.USER_ADMIN, db.getSUpdatedBy());
  174 + assertNotNull(db.getTUpdatedDate());
  175 + // 其他字段不变
  176 + assertEquals("U001", db.getSUserCode());
  177 + assertEquals("NORMAL", db.getSUserType());
  178 + }
  179 +
  180 + @Test
  181 + void update_userCodeUnchangedSameAsSelf_returns200() {
  182 + UpdateUserReq r = req();
  183 + r.setUserCode("U001"); // alice 自己当前的 userCode
  184 + UserDetailVo vo = service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  185 + assertEquals("U001", vo.getUserCode());
  186 + }
  187 +
  188 + // ===== 权限分类增量差集(Task 8) =====
  189 +
  190 + @Test
  191 + void update_permissionCategories_emptyList_clearsAll() {
  192 + // 先给 alice 加 2 个
  193 + for (Integer pcId : fx.activePermissionCategoryIds()) {
  194 + SysUserPermissionCategory l = new SysUserPermissionCategory();
  195 + l.setIUserId(fx.aliceId());
  196 + l.setIPermissionCategoryId(pcId);
  197 + l.setSGrantedBy("system");
  198 + upcMapper.insert(l);
  199 + }
  200 + UpdateUserReq r = req();
  201 + r.setPermissionCategoryIds(List.of());
  202 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  203 +
  204 + assertTrue(upcMapper.selectPermissionCategoryIdsByUserId(fx.aliceId()).isEmpty());
  205 + }
  206 +
  207 + @org.springframework.beans.factory.annotation.Autowired
  208 + private com.xly.erp.module.usr.mapper.SysPermissionCategoryMapper permissionCategoryMapper;
  209 +
  210 + @Test
  211 + void update_permissionCategories_subsetDelta_addsAndRemoves() {
  212 + Integer pur = fx.activePermissionCategoryIds().get(0);
  213 + Integer sal = fx.activePermissionCategoryIds().get(1);
  214 + // 初始:alice = {PUR, SAL}
  215 + for (Integer pcId : List.of(pur, sal)) {
  216 + SysUserPermissionCategory l = new SysUserPermissionCategory();
  217 + l.setIUserId(fx.aliceId());
  218 + l.setIPermissionCategoryId(pcId);
  219 + l.setSGrantedBy("system");
  220 + upcMapper.insert(l);
  221 + }
  222 + Integer salRowId = upcMapper.selectList(
  223 + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysUserPermissionCategory>()
  224 + .eq("iUserId", fx.aliceId())
  225 + .eq("iPermissionCategoryId", sal))
  226 + .get(0).getIIncrement();
  227 +
  228 + // 新增分类 X(活跃)
  229 + com.xly.erp.module.usr.entity.SysPermissionCategory x =
  230 + new com.xly.erp.module.usr.entity.SysPermissionCategory();
  231 + x.setSCategoryName("X");
  232 + x.setSCategoryCode("X");
  233 + x.setISortOrder(99);
  234 + x.setIIsDeleted(0);
  235 + permissionCategoryMapper.insert(x);
  236 +
  237 + UpdateUserReq r = req();
  238 + r.setPermissionCategoryIds(List.of(sal, x.getIIncrement()));
  239 + UserDetailVo vo = service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  240 +
  241 + // 最终 alice 应有 {SAL, X}
  242 + Set<Integer> finalSet = new HashSet<>(
  243 + upcMapper.selectPermissionCategoryIdsByUserId(fx.aliceId()));
  244 + assertEquals(Set.of(sal, x.getIIncrement()), finalSet);
  245 +
  246 + // SAL 行 iIncrement 不变(差集而非全量替换)
  247 + Integer salRowIdAfter = upcMapper.selectList(
  248 + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysUserPermissionCategory>()
  249 + .eq("iUserId", fx.aliceId())
  250 + .eq("iPermissionCategoryId", sal))
  251 + .get(0).getIIncrement();
  252 + assertEquals(salRowId, salRowIdAfter, "保留项的 iIncrement 应不变");
  253 + }
  254 +
  255 + @Test
  256 + void update_permissionCategories_omitted_preservesExisting() {
  257 + Integer pur = fx.activePermissionCategoryIds().get(0);
  258 + SysUserPermissionCategory l = new SysUserPermissionCategory();
  259 + l.setIUserId(fx.aliceId());
  260 + l.setIPermissionCategoryId(pur);
  261 + l.setSGrantedBy("system");
  262 + upcMapper.insert(l);
  263 +
  264 + // 请求不含 permissionCategoryIds
  265 + UpdateUserReq r = req();
  266 + r.setUserCode("U_NEW");
  267 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  268 +
  269 + assertEquals(1, upcMapper.selectPermissionCategoryIdsByUserId(fx.aliceId()).size());
  270 + }
  271 +
  272 + @Test
  273 + void update_permissionCategories_duplicateInRequest_deduped() {
  274 + Integer pur = fx.activePermissionCategoryIds().get(0);
  275 + Integer sal = fx.activePermissionCategoryIds().get(1);
  276 + UpdateUserReq r = req();
  277 + r.setPermissionCategoryIds(new ArrayList<>(List.of(pur, pur, sal)));
  278 + service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  279 +
  280 + Set<Integer> finalSet = new HashSet<>(
  281 + upcMapper.selectPermissionCategoryIdsByUserId(fx.aliceId()));
  282 + assertEquals(Set.of(pur, sal), finalSet);
  283 + }
  284 +
  285 + @Test
  286 + void update_returnsUserDetailVoReflectingFinalState() {
  287 + UpdateUserReq r = req();
  288 + r.setUserType("SUPER_ADMIN");
  289 + r.setLanguage("en-US");
  290 + UserDetailVo vo = service.update(fx.aliceId(), r, fx.adminId(), LoginTestSeeder.USER_ADMIN);
  291 + assertEquals("SUPER_ADMIN", vo.getUserType());
  292 + assertEquals("en-US", vo.getLanguage());
  293 + assertEquals(LoginTestSeeder.USER_ADMIN, vo.getUpdatedBy());
  294 + assertNotNull(vo.getUpdatedDate());
  295 + }
  296 +}
... ...