From 09db89581b36c5b05c6438b28b80bd1ddefa23e9 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 13:56:46 +0800 Subject: [PATCH] docs(plan:REQ-USR-002): 任务级 TDD 计划 --- docs/superpowers/plans/2026-06-01-REQ-USR-002.md | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+), 0 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-01-REQ-USR-002.md diff --git a/docs/superpowers/plans/2026-06-01-REQ-USR-002.md b/docs/superpowers/plans/2026-06-01-REQ-USR-002.md new file mode 100644 index 0000000..cbe735d --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-REQ-USR-002.md @@ -0,0 +1,172 @@ +# REQ-USR-002 修改用户 — 任务级 TDD 计划(后端) + +> 阶段:后端(backend)。作用域:`backend/**`(controller / service / service.impl / mapper / DTO / 校验 / REST 契约实现)。**禁止**写 `frontend/**`。 +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-REQ-USR-002.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-002.md`;DB 设计 `docs/03-数据库设计文档.md`;API 契约 `docs/05-API接口契约.md`;技术规范 `docs/04-技术规范.md`;配置 `config-vars.yaml`。 +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / API 形状 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整文件。 +> 本 REQ 复用 REQ-USR-001 已建的 backend 骨架与 `modules/usr/**`(controller / service / mapper / entity)、`common/**`(Result / ResultCode / BusinessException / GlobalExceptionHandler / SecurityUtil / JWT);**不**新建公共基础设施,**不**新增 migration。 + +--- + +## Goal(目标) + +实现后台管理员修改已有用户基本信息的唯一端点 `PUT /api/usr/users/{id}`:接收路径 `id` + `UpdateUserDTO`,前置管理员权限校验,校验目标用户存在、枚举/取值合法、关联职员与权限组 id 存在,按"部分更新(null 不改)"语义更新 `usr_user`(不动 `sUserName` / `sPassword` / 审计列 / 租户列),并按"全量覆盖"语义重写该用户在 `usr_user_permission` 的授权,整体单事务,返回 `Result<{ id }>`。变更立即生效(角色 / 禁用状态实时反映到查询与登录流程)。 + +## Architecture(架构 / 分层) + +遵循 `docs/04 § 1.2`,根包 `com.xly.erp`;本 REQ 仅触及 `modules/usr/**`,不跨模块、不动 `common/**`: + +``` +backend/src/main/java/com/xly/erp/modules/usr/ +├── controller/UsrUserController.java # 既有类,新增 @PutMapping("/users/{id}") updateUser;仅 @Valid + 管理员前置 + 委派 +├── service/UsrUserService.java # 既有接口,新增 updateUser(Integer id, UpdateUserDTO dto) +├── service/impl/UsrUserServiceImpl.java # 既有实现类,新增 updateUser 实现 @Transactional +├── mapper/UsrUserMapper.java # 既有,复用 BaseMapper(selectById/updateById/update) +├── mapper/UsrUserPermissionMapper.java # 既有,复用 BaseMapper(delete by iUserId / insert) +├── mapper/UsrEmployeeMapper.java # 既有,复用 selectById 做职员存在性校验 +├── mapper/UsrPermissionMapper.java # 既有,复用 selectById 做权限存在性校验 +├── entity/{UsrUser,UsrUserPermission,UsrEmployee,UsrPermission}.java # 既有,无需改 +└── dto/UpdateUserDTO.java # 【本 REQ 新增】修改用户入参 +``` + +- **跨模块**:无。本 REQ 全部落在 `modules/usr/**`,不动 `common/**`,不新增公共契约(错误码 `40401`/`40301`/`40001` 已在 REQ-USR-001 一次性建好的 `ResultCode` 枚举中预留,直接复用,**不改 `ResultCode`**)。 +- **数据访问**:只走 Mapper(MyBatis-Plus);存在性校验 / 删授权用 `LambdaQueryWrapper` 或 MP 内置(`selectById` / `selectCount` / `delete`);Controller 禁止直接调 Mapper。 +- **部分更新落库策略**:为实现"null 列不更新",更新主记录用 MP 的 `updateById`(MP 默认 null 字段不参与 SET,恰好匹配"传 null 不改该列"语义),目标实体只 set 非 null 的可更新列 + 主键 `iIncrement`;不 set `sUserName` / `sPassword` / `sCreator` / `tCreateDate` / 租户列,杜绝覆盖原值。 + +## Tech Stack(技术栈,源自 docs/04 § 零 + config-vars) + +- Spring Boot 3.x / Java 17 / Maven 3.9.x;MyBatis-Plus;MySQL 8.x;Flyway 10.x(启动时自动 apply `sql/migrations/`,**本 REQ 不新增 migration**,复用 `V1__initial_schema.sql`,spec § 4 / D5)。 +- Spring Security + JWT(既有 `JwtAuthenticationFilter` / `JwtUtil` / `SecurityUtil`);本接口受保护(非登录端点,安全链已要求认证)。 +- 根包 `com.xly.erp`;端口与 DB 凭据 / JWT 密钥只读 `config-vars.yaml` / `application.yml`,不硬编码。 +- 命令(docs/04 § 零):build `mvn -q -B -DskipTests package`;lint `mvn -q -B checkstyle:check`;unit `mvn -q -B test`;e2e 无。 + +## 合同级常量(跨 task 必须一致) + +- REST:`PUT /api/usr/users/{id}`(`id` 为 `@PathVariable Integer`)。 +- 错误码(复用既有 `ResultCode` 枚举,spec § 6 / docs/05,**不新增不修改枚举**): + - `SUCCESS=0` — 成功,`data.id` = 被修改用户主键。 + - `PARAM_INVALID=40001` — 参数校验失败(字段格式 / 必填 / 枚举越界 / `iEmployeeId` 或 `permissionIds` 元素不存在 / `id` 非正整数)。 + - `FORBIDDEN=40301` — 无权限(非管理员调用)。 + - `NOT_FOUND=40401` — 用户不存在(路径 `id` 无对应 `usr_user` 记录)。 +- 管理员判定口径(与 REQ-USR-001 一致,spec § 8 D6):`SecurityUtil.currentUserType()` 等于常量 `超级管理员` 视为有权;否则抛 `BusinessException(FORBIDDEN)`。控制器内沿用既有 `ADMIN_USER_TYPE = "超级管理员"` 常量,不重复定义新口径。 +- 枚举取值:`sUserType ∈ {普通用户, 超级管理员}`;`sLanguage ∈ {中文, 英文, 繁体}`;`iCanModifyBill ∈ {0,1}`;`iIsVoid ∈ {0,1}`(0 正常 / 1 禁用)。 +- 部分更新语义(spec § 8 D3):可选字段(`sUserNo` / `iEmployeeId` / `iCanModifyBill` / `iIsVoid`)传 `null` = 本次不改该列(保持原值);非 null 即覆盖。 +- 权限覆盖语义(spec § 8 D4):`permissionIds` 非 null = 以该集合(去重)全量覆盖该用户授权(先删该用户全部旧授权再批量插入去重后的目标集合,结果 = 目标集合);`[]` = 清空全部授权;`null` = 不改动现有授权。 +- 只读 / 不接收字段(spec § 2.1 / § 3):`sUserName` / `sPassword` / `tCreateDate` / `sCreator` / `tLastLoginDate` / `iIncrement` / `sId` / `sBrandsId` / `sSubsidiaryId` 一律不在本接口更新;`UpdateUserDTO` 不含这些字段(即便请求体携带也因 DTO 无该字段而被忽略)。 + +## 关键签名(首次出现处给出,跨 task 保持一致) + +- `UsrUserService#updateUser(Integer id, UpdateUserDTO dto)` 返回 `Integer`(被修改用户主键 `iIncrement`,等于入参 `id`)。 +- `UsrUserController#updateUser(@PathVariable Integer id, @Valid @RequestBody UpdateUserDTO dto)` 返回 `Result>`(`data.id` = 被修改主键,键名 `id`,与 REQ-USR-001 `createUser` 返回形状一致)。 +- 复用既有:`SecurityUtil.currentUserType()` → `String`;`Result.success(T)`;`BusinessException(ResultCode)` / `BusinessException(ResultCode, String)` 暴露 `getResultCode()`;Mapper 继承 `BaseMapper`(`selectById` / `updateById` / `selectCount` / `delete` / `insert`);`UsrUserPermission(Integer iUserId, Integer iPermissionId)` 构造器。 +- 实体 getter/setter 沿用匈牙利前缀风格(`getIIncrement` / `setSUserType` / `setIIsVoid` 等,已存在于 `UsrUser` / `UsrUserPermission`)。 + +### DTO 形状(`UpdateUserDTO`,置于 `modules/usr/dto`) + +> 字段为匈牙利前缀命名(与列名一致),getter 形如 `getSUserType` 会被 Jackson 推断为属性名 `SUserType`,与契约 JSON 键不符;故对带前缀字段显式 `@JsonProperty` 锁定 JSON 键名(与 `CreateUserDTO` 同样做法)。 + +| 字段 | 类型 | 校验注解 | 语义 | +|---|---|---|---| +| `sUserNo` | String | `@Size(max=50)` | 可选;非 null 覆盖,null 不改 | +| `iEmployeeId` | Integer | —(存在性在 Service 校验) | 可选;非 null 须存在于 `usr_employee` 否则 `40001`;null 不改 | +| `sUserType` | String | `@NotBlank` + `@Pattern(regexp="^(普通用户|超级管理员)$")` | 必填;越界 `40001` | +| `sLanguage` | String | `@NotBlank` + `@Pattern(regexp="^(中文|英文|繁体)$")` | 必填;越界 `40001` | +| `iCanModifyBill` | Integer | `@Min(0)` + `@Max(1)` | 可选;非 0/1 越界 `40001`;null 不改 | +| `iIsVoid` | Integer | `@Min(0)` + `@Max(1)` | 可选;0 正常 / 1 禁用;非 0/1 越界 `40001`;null 不改 | +| `permissionIds` | `List` | —(元素存在性在 Service 校验) | 可选;非 null 全量覆盖授权;`[]` 清空;null 不改 | + +> 注:`@Valid` 失败由既有 `GlobalExceptionHandler` 统一转 `40001`。`sUserType` / `sLanguage` 既由注解兜底,也在 Service 端不做额外越界放行;Service 对"目标用户不存在"抛 `40401`,对"关联 id 不存在"抛 `40001`。本 DTO 不含 `sUserName` / `sPassword` / 审计 / 租户字段。 + +--- + +## 任务清单(每个 task = red → green → 子会话验证 PASS → commit;粒度 2-5 分钟) + +> 业务类 commit subject 必须带 `REQ-USR-002` 后缀(CLAUDE.md § Git 提交规范)。每个 task 完成后单独 commit。 + +### T1 — `UpdateUserDTO` + Bean Validation +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/dto/UpdateUserDTOValidationTest.java`(用 `jakarta.validation.Validator` 直接 validate DTO 断言 violations 数量): + - `::acceptsMinimalValidBody` —— 仅 `{sUserType:"普通用户", sLanguage:"中文"}`(其余可选字段全 null)无违反。 + - `::rejectsBlankUserType` —— `sUserType` 为空白触发 `@NotBlank`。 + - `::rejectsIllegalLanguage` —— `sLanguage="日文"` 违反 `@Pattern`。 + - `::rejectsOutOfRangeIsVoid` —— `iIsVoid=2` 违反 `@Max(1)`;`::rejectsOutOfRangeCanModifyBill` —— `iCanModifyBill=2` 违反 `@Max(1)`。 +- [ ] **实现**:`modules/usr/dto/UpdateUserDTO.java`,按上「DTO 形状」加字段 + `@JsonProperty` + 校验注解 + getter/setter(不含 `sUserName`/`sPassword`/审计/租户字段)。 +- [ ] **验证**:子会话跑 `UpdateUserDTOValidationTest` PASS。 +- [ ] **commit**:`feat(usr): 修改用户入参 UpdateUserDTO 与校验 REQ-USR-002` + +### T2 — Service:目标用户存在性 + 主记录部分更新(核心写) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/service/UsrUserServiceImplTest.java`(续既有类,Mockito mock 4 Mapper + PasswordEncoder + SecurityUtil 静态): + - `::updateNonExistentUserThrows40401` —— `usrUserMapper.selectById(id)` 返回 null → 抛 `BusinessException(NOT_FOUND)`,且不调用 `updateById` / 不动权限表。 + - `::updateAppliesNonNullColumnsAndKeepsIdentityImmutable` —— 目标用户存在,DTO 设 `sUserType="超级管理员"` / `sLanguage="英文"` / `iCanModifyBill=1` / `iIsVoid=1` / `sUserNo="N9"`,断言传给 `usrUserMapper.updateById` 的 `UsrUser`:`iIncrement==id`、上述列为新值、且 `sUserName`/`sPassword`/`sCreator`/`tCreateDate` 未被赋值(保持 null,证明不参与 SET,依赖 MP null 不更新语义)。 + - `::nullOptionalColumnsAreNotOverwritten` —— DTO 仅含必填 `sUserType`/`sLanguage`,`sUserNo`/`iEmployeeId`/`iCanModifyBill`/`iIsVoid` 均 null → 传给 `updateById` 的实体这些字段为 null(MP 不 SET,保持原值)。 +- [ ] **实现**:`UsrUserService.java` 新增 `Integer updateUser(Integer id, UpdateUserDTO dto)`;`UsrUserServiceImpl.java` 新增实现并标 `@Transactional(rollbackFor = Exception.class)`:先 `selectById(id)` 校验存在(null → `40401`);组装目标 `UsrUser` 仅 `setIIncrement(id)` + set 非 null 可更新列(`sUserType`/`sLanguage` 必填总 set,`sUserNo`/`iEmployeeId`/`iCanModifyBill`/`iIsVoid` 仅在 DTO 非 null 时 set);`updateById`。 +- [ ] **验证**:子会话跑上述 3 个用例 PASS。 +- [ ] **commit**:`feat(usr): 修改用户主记录部分更新与存在性校验 REQ-USR-002` + +### T3 — Service:关联职员存在性 + 权限组全量覆盖 +- [ ] **测试**:续 `UsrUserServiceImplTest`: + - `::nonExistentEmployeeOnUpdateThrows40001` —— 目标用户存在但 DTO `iEmployeeId` 指向 `usrEmployeeMapper.selectById` 返回 null → `BusinessException(PARAM_INVALID)`,不 `updateById`、不动权限表(事务回滚语义)。 + - `::nonExistentPermissionOnUpdateThrows40001` —— `permissionIds` 含 `usrPermissionMapper.selectById` 返回 null 的 id → `40001`,不 `updateById`、不写权限表。 + - `::permissionIdsOverwriteDeletesThenInserts` —— `permissionIds=[a,a,b]`(均存在)→ 先对 `usrUserPermissionMapper.delete`(按 `iUserId=id`)调用 1 次清旧授权,再去重批量 `insert` 2 行 `(id,a)`/`(id,b)`(断言 delete 与 insert 次数/内容;用 `ArgumentCaptor` 验证 `iUserId==id`、`iPermissionId` 含 a/b 各一次)。 + - `::emptyPermissionIdsClearsAll` —— `permissionIds=[]` → `delete`(按 `iUserId=id`)被调用,`insert` 不被调用(清空)。 + - `::nullPermissionIdsLeavesGrantsUntouched` —— `permissionIds=null` → `delete` 与 `insert` 均不被调用(不改动授权)。 +- [ ] **实现**:在 `updateUser` 内(主记录更新前先做存在性校验,保证非法即整体回滚不留副作用):`iEmployeeId` 非 null 时校验 `usr_employee` 存在(缺失 `40001`);`permissionIds` 非 null 时去重并逐个校验 `usr_permission` 存在(缺失 `40001`);主记录 `updateById` 成功后,若 `permissionIds` 非 null 则按 `LambdaQueryWrapper.eq(UsrUserPermission::getIUserId, id)` 全量删除旧授权,再对去重集合逐行 `insert(new UsrUserPermission(id, permId))`(空集合则只删不插);全程同一 `@Transactional`。 +- [ ] **验证**:子会话跑上述 5 个用例 PASS。 +- [ ] **commit**:`feat(usr): 修改用户关联职员校验与权限组全量覆盖 REQ-USR-002` + +### T4 — Controller + 管理员权限前置 +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/controller/UsrUserControllerTest.java`(续既有类,MockMvc standaloneSetup + 真实 `GlobalExceptionHandler` + mock `UsrUserService` + mock `SecurityUtil` 静态): + - `::adminUpdateReturnsCodeZeroWithId` —— 管理员(`currentUserType="超级管理员"`)`PUT /api/usr/users/55` 带合法 body(`sUserType`/`sLanguage` 合法)→ HTTP 200,`code==0`,`data.id==55`;响应体不含 `sPassword`/`password`。 + - `::nonAdminUpdateReturns40301` —— 普通用户 → `code==40301`,`usrUserService.updateUser` 不被调用。 + - `::invalidBodyUpdateReturns40001` —— body 缺 `sUserType`(`@NotBlank` 失败)→ `code==40001`,Service 不被调用。 + - `::userNotFoundReturns40401` —— 管理员 + 合法 body,但 `usrUserService.updateUser` 抛 `BusinessException(NOT_FOUND)` → `code==40401`(验证 Controller 不吞业务异常、由全局处理器转码)。 +- [ ] **实现**:`UsrUserController.java` 新增 `@PutMapping("/users/{id}")` 方法 `updateUser(@PathVariable Integer id, @Valid @RequestBody UpdateUserDTO dto)`:入口判定 `!ADMIN_USER_TYPE.equals(SecurityUtil.currentUserType())` 抛 `BusinessException(FORBIDDEN)`(先于业务校验,spec § 3.9);委派 `usrUserService.updateUser(id, dto)`;返回 `Result.success(Map.of("id", id))`。Controller 不直接调 Mapper、不写业务逻辑。 +- [ ] **验证**:子会话跑 `UsrUserControllerTest`(既有 + 新增用例)PASS。 +- [ ] **commit**:`feat(usr): 修改用户 Controller 与管理员权限前置 REQ-USR-002` + +### T5 — 端到端验收回归(按 spec § 7 验收标准收口) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java`(`@SpringBootTest` + `@AutoConfigureMockMvc` + `@ActiveProfiles("test")`,连测试库 Flyway 已 apply V1;用真实 `JwtUtil` 签发 token 走安全链;`@AfterEach` 自清理本测试 fixture,命名前缀如 `it2_user_` / `IT2_PERM_` 便于 likeRight 清理;管理员 token 与普通用户 token 仿 `UsrUserCreateIT` 生成)覆盖 spec § 7: + - `::ac1UpdateBasicInfoPersists` —— 先经 `POST /api/usr/users` 建一个用户取 `id`(或直接 Mapper 插 fixture),再 `PUT /api/usr/users/{id}` 改 `sUserType`/`sLanguage`/`iCanModifyBill`/`sUserNo` → `code=0`、`data.id==id`;库中该行上述列为新值,且 `sUserName`/`sPassword`/`sCreator`/`tCreateDate` 字节级不变(保存改前后 `selectById` 比对)。 + - `::ac2UpdateNonExistentReturns40401` —— `PUT /api/usr/users/{很大且不存在的 id}` → `code=40401`,无任何写入。 + - `::ac3InvalidParamRollsBack` —— `iEmployeeId` 指向不存在职员(或 `permissionIds` 含不存在 id)→ `code=40001`,目标用户行各列与调用前一致(无副作用)。 + - `::ac6PermissionOverwrite` —— 预置该用户授权为 `{a,c}`(fixture 插 `usr_permission` a/b/c + `usr_user_permission`),`PUT` 传 `permissionIds=[a,b,a]` → 覆盖后该用户授权恰为 `{a,b}`(c 被删、b 新增、a 去重一次);另一用例或同用例追加:传 `permissionIds=[]` → 该用户授权清空;不传 `permissionIds` → 授权不变。 + - `::ac7NonAdminAndNoTokenBlocked` —— 普通用户 token → `code=40301`;无 token → HTTP 401;两种情况下目标用户行均未被修改。 + - `::ac8PasswordUnchangedAndAbsentFromResponse` —— 修改成功后 `sPassword` 列与改前字节级一致(`selectById` 比对哈希值相等);成功响应体仅含 `data.id`,不含 `sPassword` / 明文 / `password` 字段。 + - (AC4「禁用实时生效」、AC5「角色变更实时生效」依赖 REQ-USR-004 登录 / REQ-USR-003 查询接口,尚未实现;本 IT 以"`PUT iIsVoid=1` / 改 `sUserType` 后 `selectById` 读回库内 `iIsVoid==1` / `sUserType` 为新值"做后端落库层等价验证,登录/查询联动留待对应 REQ 的 IT 覆盖,在本测试注释中标注此边界。) +- [ ] **实现**:仅在前序 task 暴露缺口时做最小修补(如 MP `updateById` 对 null 字段的实际行为与预期不符时调整组装策略、权限删除 wrapper 边界),不引入新公共契约、不新增 migration。 +- [ ] **验证**:子会话跑 `UsrUserUpdateIT` PASS(连库);随后全量 `mvn -q -B test` 全绿、`mvn -q -B checkstyle:check` 通过。 +- [ ] **commit**:`test(usr): 修改用户端到端验收回归 REQ-USR-002` + +--- + +## 自审 + +### 占位符扫描 +- 全文无 `【人工填写】` / `TBD` / `TODO` / 待定占位。spec § 8 注记的 DB 文档「需用户审阅」遗留标记不在本后端 REQ 作用域,按 spec 锁定(语言 ∈ {中文,英文,繁体})继续,不阻塞。 + +### Spec coverage(spec 每节 → task 映射) +- § 1 Goal(唯一端点 `PUT /api/usr/users/{id}`、`sUserName` 不可改、密码不改)→ T4(端点)+ T2(不动 `sUserName`/`sPassword`)+ 全部 task。 +- § 2.1 输入 / DTO 字段与校验 / 路径参数 `id` / Auth → T1(DTO 校验)+ T2/T3(Service 存在性与覆盖)+ T4(路径参数 + 管理员前置)。 +- § 2.1 忽略只读/不接收字段(`sUserName`/`sPassword`/审计/租户)→ T1(DTO 不含这些字段)+ T2(组装实体不 set 这些列)+ T5 AC8(密码不变)。 +- § 2.2 输出 `Result<{id}>`、不含敏感字段 → T4(Controller 组装)+ T5 AC8。 +- § 3.1 目标用户必须存在(`40401`,不写入)→ T2 + T5 AC2。 +- § 3.2 `sUserName` 不可改 → T1(DTO 无该字段)+ T2 + T5 AC1。 +- § 3.3 密码不改/不返回 → T2(不 set `sPassword`)+ T5 AC8。 +- § 3.4 用户类型必填 + 枚举约束(越界 `40001`)→ T1(`@NotBlank`+`@Pattern`)+ T4。 +- § 3.5 语言必填 + 枚举约束 → T1(`@NotBlank`+`@Pattern`)。 +- § 3.6 关联职员可选且需存在(`40001`)/ 部分更新语义 → T3 + T2(null 不改)。 +- § 3.7 作废/禁用实时生效(`iIsVoid ∈ {0,1}`,越界 `40001`)→ T1(`@Min/@Max`)+ T2(落库)+ T5(落库层等价验证,登录联动留 REQ-USR-004)。 +- § 3.8 权限组全量覆盖(`permissionIds` 非 null 覆盖 / `[]` 清空 / null 不改 / 去重 / 唯一索引)→ T3 + T5 AC6。 +- § 3.9 权限校验前置(非管理员 `40301`,先于业务)→ T4。 +- § 3.10 审计字段只读、不新增列/migration → T2(不 set 审计列)+ Tech Stack(不新增 migration)。 +- § 4 约束(分层/包路径/命名 `updateUser`/统一响应/异常/事务/认证/数据访问/配置/schema)→ T1-T4 分层落位,事务在 T2/T3,schema 复用 V1。 +- § 5 Schema 引用(写 `usr_user`/`usr_user_permission`、读 `usr_employee`/`usr_permission`)→ T2/T3(复用既有实体/Mapper)。 +- § 6 错误码(`0`/`40001`/`40401`/`40301`)→ 复用既有 `ResultCode`,T2(`40401`)/T3(`40001`)/T4(`40301`)。 +- § 7 验收标准 1-8 → T5(AC1/2/3/6/7/8 直接覆盖;AC4/AC5 以落库层等价验证 + 注释边界,联动留对应 REQ)。 +- § 8 decisions(D1-D6)→ D1(不改密码)T2、D2(`iIsVoid` 承载状态)T1/T2、D3(null 不改)T2、D4(全量覆盖)T3、D5(不新增 migration)Tech Stack、D6(管理员口径)T4 已体现。 + +### 类型一致性 +- `UsrUserService#updateUser(Integer id, UpdateUserDTO):Integer` 在 T2 定义,T3(实现续写)/T4(Controller 调用)/T5(IT)一致引用。 +- `UsrUserController#updateUser(@PathVariable Integer id, @Valid @RequestBody UpdateUserDTO)` 返回 `Result>`(`data.id`),与 REQ-USR-001 `createUser` 返回形状一致。 +- `UpdateUserDTO` 字段与校验注解在 T1 锁定,T2/T3/T4/T5 一致使用;字段命名与 `UsrUser` 列名匈牙利前缀一致,`@JsonProperty` 锁 JSON 键(与 `CreateUserDTO` 同风格)。 +- 错误码字面量 `0/40001/40301/40401` 复用既有 `ResultCode`,与 docs/05、spec § 6 一致,不新增枚举常量。 +- REST 路径 `PUT /api/usr/users/{id}`、管理员口径 `超级管理员` 与 docs/05、spec、既有 `UsrUserController.ADMIN_USER_TYPE` 一致。 +- Mapper 复用既有 `BaseMapper`(`selectById`/`updateById`/`delete`/`insert`/`selectCount`)+ `UsrUserPermission(Integer,Integer)` 构造器,无新增 Mapper 方法签名。 -- libgit2 0.22.2