Commit 81a263b909a3f3c174838b87208cc6bba01b70be

Authored by zichun
1 parent 704d75a4

fix(usr): 修复 review round 1 must-fix REQ-USR-002

M2: UserUpdateReqDTO 添加 @NotBlank/@Pattern/@NotNull 字段校验
M3: UserFormDrawer InitialData 补 permGroupIds 字段并初始化
M4: 列表查询加 bCanEditDoc 返回字段,编辑抽屉传真实值而非 false
M1: 补充 updateUser_selfAdminKeepType_doesNotThrow 测试
backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java
1 package com.example.erp.module.usr.dto; 1 package com.example.erp.module.usr.dto;
2 2
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Pattern;
3 import lombok.Getter; 6 import lombok.Getter;
4 import lombok.Setter; 7 import lombok.Setter;
5 8
@@ -9,10 +12,16 @@ import java.util.List; @@ -9,10 +12,16 @@ import java.util.List;
9 @Setter 12 @Setter
10 public class UserUpdateReqDTO { 13 public class UserUpdateReqDTO {
11 14
  15 + @NotBlank(message = "用户类型不能为空")
  16 + @Pattern(regexp = "普通用户|超级管理员", message = "用户类型无效")
12 private String userType; 17 private String userType;
  18 +
  19 + @NotBlank(message = "语言不能为空")
  20 + @Pattern(regexp = "中文|英文|繁体", message = "语言无效")
13 private String language; 21 private String language;
14 private boolean canEditDoc; 22 private boolean canEditDoc;
15 private boolean isDisabled; 23 private boolean isDisabled;
16 private String employeeId; 24 private String employeeId;
  25 + @NotNull(message = "权限组列表不能为空")
17 private List<String> permGroupIds; 26 private List<String> permGroupIds;
18 } 27 }
backend/src/main/java/com/example/erp/module/usr/vo/UserListItemVO.java
@@ -25,6 +25,9 @@ public class UserListItemVO { @@ -25,6 +25,9 @@ public class UserListItemVO {
25 @JsonProperty("sLanguage") 25 @JsonProperty("sLanguage")
26 private String sLanguage; 26 private String sLanguage;
27 27
  28 + @JsonProperty("bCanEditDoc")
  29 + private Integer bCanEditDoc;
  30 +
28 @JsonProperty("bIsDisabled") 31 @JsonProperty("bIsDisabled")
29 private Integer bIsDisabled; 32 private Integer bIsDisabled;
30 33
backend/src/main/resources/mapper/UsrUserMapper.xml
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 <!-- REQ-USR-003: 查询用户 --> 6 <!-- REQ-USR-003: 查询用户 -->
7 <select id="selectUserList" resultType="com.example.erp.module.usr.vo.UserListItemVO"> 7 <select id="selectUserList" resultType="com.example.erp.module.usr.vo.UserListItemVO">
8 SELECT u.sId, u.sUsername, u.sUserCode, u.sUserType, u.sLanguage, 8 SELECT u.sId, u.sUsername, u.sUserCode, u.sUserType, u.sLanguage,
9 - u.bIsDisabled, u.tLastLoginDate, u.sCreatorUsername, u.tCreateDate, 9 + u.bCanEditDoc, u.bIsDisabled, u.tLastLoginDate, u.sCreatorUsername, u.tCreateDate,
10 s.sStaffName, s.sDepartment 10 s.sStaffName, s.sDepartment
11 FROM usr_user u 11 FROM usr_user u
12 LEFT JOIN tStaff s ON u.sEmployeeId = s.sId 12 LEFT JOIN tStaff s ON u.sEmployeeId = s.sId
backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java
@@ -233,6 +233,22 @@ class UserServiceTest { @@ -233,6 +233,22 @@ class UserServiceTest {
233 } 233 }
234 234
235 @Test 235 @Test
  236 + void updateUser_selfAdminKeepType_doesNotThrow() {
  237 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  238 + dto.setUserType("超级管理员");
  239 + dto.setLanguage("中文");
  240 + dto.setPermGroupIds(List.of());
  241 + UsrUserEntity existing = new UsrUserEntity();
  242 + existing.setSId("u1");
  243 + existing.setSUsername("admin");
  244 + when(userMapper.selectOne(any())).thenReturn(existing);
  245 + when(userMapper.update(any(), any())).thenReturn(1);
  246 +
  247 + // 自己保持 超级管理员 角色 → 不应抛 40301
  248 + assertDoesNotThrow(() -> userService.updateUser("u1", dto, superAdmin));
  249 + }
  250 +
  251 + @Test
236 void updateUser_invalidEmployee_throws40001() { 252 void updateUser_invalidEmployee_throws40001() {
237 UserUpdateReqDTO dto = new UserUpdateReqDTO(); 253 UserUpdateReqDTO dto = new UserUpdateReqDTO();
238 dto.setUserType("普通用户"); 254 dto.setUserType("普通用户");
docs/superpowers/reviews/2026-05-08-REQ-USR-002.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-002
  3 +date: 2026-05-08
  4 +round: 1
  5 +reviewer: superpower-code-reviewer
  6 +---
  7 +
  8 +# Review: REQ-USR-002 — round 1
  9 +
  10 +## 结论
  11 +request-changes
  12 +
  13 +## Must-fix
  14 +- [HIGH] backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java:12-13 — userType / language 缺少 @NotBlank + @Pattern 校验,非法值直接写库(建议:参照 UserCreateReqDTO 添加 @NotBlank + @Pattern 注解)
  15 +- [HIGH] frontend/src/pages/usr/UserFormDrawer.tsx:8-14 — InitialData 接口缺少 permGroupIds 字段,导致有权限组的用户打开编辑抽屉时权限组无法被预填(建议:添加 permGroupIds?: string[],useEffect 改为 setSelectedPermIds(initialData.permGroupIds ?? []))
  16 +- [HIGH] frontend/src/pages/usr/UserListPage.tsx:124 — canEditDoc 写死为 false,每次保存会将已有用户的 canEditDoc 覆盖为 false(建议:在列表查询中加入 bCanEditDoc,传入 initialData 时使用真实值)
  17 +- [MEDIUM] backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java — 缺少测试 userId==自己 但 userType 仍为 "超级管理员" 时不抛出 40301(建议:补充 updateUser_selfAdminKeepType_doesNotThrow 测试,确认允许路径)
  18 +
  19 +## Nice-to-have
  20 +- backend/.../vo/UserUpdateRespVO.java — updatedAt 建议加 @JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss") 确保序列化格式显式
  21 +- backend/.../dto/UserUpdateReqDTO.java — permGroupIds 建议加 @NotNull,防止调用方传 null 时静默跳过权限组删除
  22 +- frontend/src/test/UserListPage.test.tsx:106 — editMode_submit_callsUpdateUser 只断言调用次数,建议补充 toHaveBeenCalledWith('u1', ...) 验证 userId 传值正确
  23 +
  24 +## 反例 / 测试覆盖缺口
  25 +- 缺 "自己修改自己 userType 但保持 超级管理员" 的无异常路径测试(G1)
  26 +- controller 层无 BizException → 错误码映射测试(G4,40300/40400/40301 未被 controller 测试覆盖)
  27 +- canEditDoc/isDisabled 写入 DB 的 int 映射值(0/1)未在 service happy-path 测试中验证
frontend/src/api/usr.ts
@@ -54,6 +54,7 @@ export interface UserListItemVO { @@ -54,6 +54,7 @@ export interface UserListItemVO {
54 sUserCode: string 54 sUserCode: string
55 sUserType: string 55 sUserType: string
56 sLanguage: string 56 sLanguage: string
  57 + bCanEditDoc: number
57 bIsDisabled: number 58 bIsDisabled: number
58 tLastLoginDate: string | null 59 tLastLoginDate: string | null
59 sCreatorUsername: string | null 60 sCreatorUsername: string | null
frontend/src/pages/usr/UserFormDrawer.tsx
@@ -11,6 +11,7 @@ interface InitialData { @@ -11,6 +11,7 @@ interface InitialData {
11 canEditDoc: boolean 11 canEditDoc: boolean
12 isDisabled: boolean 12 isDisabled: boolean
13 employeeId?: string | null 13 employeeId?: string | null
  14 + permGroupIds?: string[]
14 } 15 }
15 16
16 interface Props { 17 interface Props {
@@ -42,7 +43,7 @@ export default function UserFormDrawer({ open, onClose, onSuccess, userId, initi @@ -42,7 +43,7 @@ export default function UserFormDrawer({ open, onClose, onSuccess, userId, initi
42 isDisabled: initialData.isDisabled, 43 isDisabled: initialData.isDisabled,
43 employeeId: initialData.employeeId ?? null, 44 employeeId: initialData.employeeId ?? null,
44 }) 45 })
45 - setSelectedPermIds([]) 46 + setSelectedPermIds(initialData.permGroupIds ?? [])
46 } else { 47 } else {
47 form.resetFields() 48 form.resetFields()
48 setSelectedPermIds([]) 49 setSelectedPermIds([])
frontend/src/pages/usr/UserListPage.tsx
@@ -121,7 +121,7 @@ export default function UserListPage() { @@ -121,7 +121,7 @@ export default function UserListPage() {
121 initialData={editingUser ? { 121 initialData={editingUser ? {
122 userType: editingUser.sUserType, 122 userType: editingUser.sUserType,
123 language: editingUser.sLanguage, 123 language: editingUser.sLanguage,
124 - canEditDoc: false, 124 + canEditDoc: editingUser.bCanEditDoc === 1,
125 isDisabled: editingUser.bIsDisabled === 1, 125 isDisabled: editingUser.bIsDisabled === 1,
126 employeeId: null, 126 employeeId: null,
127 } : undefined} 127 } : undefined}
frontend/src/test/UserListPage.test.tsx
@@ -70,7 +70,7 @@ describe(&#39;UserListPage&#39;, () =&gt; { @@ -70,7 +70,7 @@ describe(&#39;UserListPage&#39;, () =&gt; {
70 total: 1, page: 1, pageSize: 20, 70 total: 1, page: 1, pageSize: 20,
71 list: [{ 71 list: [{
72 sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', 72 sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
73 - sLanguage: '中文', bIsDisabled: 0, tLastLoginDate: null, 73 + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,
74 sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', 74 sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
75 sStaffName: '张三', sDepartment: '研发部' 75 sStaffName: '张三', sDepartment: '研发部'
76 }] 76 }]
@@ -93,7 +93,7 @@ describe(&#39;UserListPage&#39;, () =&gt; { @@ -93,7 +93,7 @@ describe(&#39;UserListPage&#39;, () =&gt; {
93 total: 1, page: 1, pageSize: 20, 93 total: 1, page: 1, pageSize: 20,
94 list: [{ 94 list: [{
95 sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', 95 sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
96 - sLanguage: '中文', bIsDisabled: 0, tLastLoginDate: null, 96 + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,
97 sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', 97 sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
98 sStaffName: null, sDepartment: null 98 sStaffName: null, sDepartment: null
99 }] 99 }]