diff --git a/docs/superpowers/plans/2026-06-01-REQ-USR-001.md b/docs/superpowers/plans/2026-06-01-REQ-USR-001.md new file mode 100644 index 0000000..63a4d53 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-REQ-USR-001.md @@ -0,0 +1,197 @@ +# REQ-USR-001 增加用户 — 任务级 TDD 计划(后端) + +> 阶段:后端(backend)。作用域:`backend/**`(controller / service / repository(mapper) / DTO / VO / entity / 校验 / 配置 / REST 契约实现)。**禁止**写 `frontend/**`。 +> 上游 SSoT:spec `docs/superpowers/specs/2026-06-01-REQ-USR-001.md`;需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-001.md`;DB 设计 `docs/03-数据库设计文档.md`;API 契约 `docs/05-API接口契约.md`;技术规范 `docs/04-技术规范.md`;配置 `config-vars.yaml`。 +> 本计划告诉 TDD 执行者**做什么 / 文件边界 / 测试意图 / API 形状 / 完成判据**;具体代码由红-绿-提交循环产出,不在此 dump 整文件。 + +--- + +## Goal(目标) + +实现后台管理员新建用户账号的唯一端点 `POST /api/usr/users`:接收 `CreateUserDTO`,校验参数与权限,对用户名查重,BCrypt 哈希初始密码,写入 `usr_user` 并按需为新用户批量授权 `usr_user_permission`,返回 `Result<{ id }>`。同时因这是项目首个后端 REQ,需先一次性搭好 `backend/` Maven 骨架与 `common/**` 公共基础设施(统一响应 / 异常 / 安全 / BaseEntity / 配置),供本 REQ 及后续 REQ 复用。 + +## Architecture(架构 / 分层) + +遵循 `docs/04 § 1.2`,根包 `com.xly.erp`(来自 `config-vars.yaml backend.base_package`): + +``` +backend/ +├── pom.xml # spring-boot 3 / mybatis-plus / mysql / flyway-core+flyway-mysql / spring-security / jjwt / mapstruct / hutool / bcrypt(随 spring-security) / spring-boot-starter-test +├── src/main/java/com/xly/erp/ +│ ├── ErpApplication.java # 启动类 @SpringBootApplication + @MapperScan("com.xly.erp.**.mapper") +│ ├── common/ +│ │ ├── response/Result.java # 统一响应体 Result +│ │ ├── response/ResultCode.java # 错误码枚举(0/40001/40301/40901/40101/40302/40401/42201) +│ │ ├── exception/BusinessException.java # 业务异常 +│ │ ├── exception/GlobalExceptionHandler.java # @RestControllerAdvice:BusinessException / MethodArgumentNotValidException / DuplicateKeyException / 兜底 +│ │ ├── base/BaseEntity.java # 标准列公共字段(iIncrement/sId/sBrandsId/sSubsidiaryId/tCreateDate)+ MP 自动填充元信息 +│ │ ├── config/MybatisPlusConfig.java # MP 配置(自动填充 MetaObjectHandler、可选分页插件) +│ │ ├── config/SecurityConfig.java # SecurityFilterChain:放行 /api/usr/login + swagger,其余需认证;注册 BCryptPasswordEncoder Bean;挂 JwtAuthenticationFilter +│ │ └── security/JwtAuthenticationFilter.java + JwtUtil.java + SecurityUtil.java # JWT 解析过滤器、JWT 工具、取当前登录用户名工具 +│ └── modules/usr/ +│ ├── controller/UsrUserController.java # 仅 @Valid + 委派 +│ ├── service/UsrUserService.java # 接口 +│ ├── service/impl/UsrUserServiceImpl.java # 业务实现 @Transactional +│ ├── mapper/UsrUserMapper.java # BaseMapper +│ ├── mapper/UsrUserPermissionMapper.java # BaseMapper +│ ├── mapper/UsrEmployeeMapper.java # BaseMapper(读校验) +│ ├── mapper/UsrPermissionMapper.java # BaseMapper(读校验) +│ ├── entity/UsrUser.java / UsrUserPermission.java / UsrEmployee.java / UsrPermission.java +│ └── dto/CreateUserDTO.java +├── src/main/resources/ +│ ├── application.yml # 端口/数据源/MP/Flyway locations/JWT(值引用 config-vars,不硬编码密钥) +│ └── application-test.yml # 测试 profile(连同一测试库;Flyway 自动 apply V1) +└── src/test/java/com/xly/erp/ # 测试,包结构镜像主代码 +``` + +- **跨模块**:本 REQ 仅触及 `modules/usr/**` 与 `common/**`。`common/**` 为首次创建的公共基础设施(非业务模块改动),属项目骨架初始化。 +- **数据访问**:只走 Mapper(MyBatis-Plus),查重 / 存在性校验用 `LambdaQueryWrapper` 或 MP 内置;Controller 禁止直接调 Mapper。 + +## Tech Stack(技术栈,源自 docs/04 § 零 + config-vars) + +- Spring Boot 3.x / Java 17 / Maven 3.9.x;MyBatis-Plus;MySQL 8.x;Flyway 10.x(`flyway-core` + `flyway-mysql`,启动时自动 apply `sql/migrations/`)。 +- Spring Security + JWT(jjwt);`BCryptPasswordEncoder` 哈希密码;MapStruct(可选,本 REQ 转换简单可手写);Hutool。 +- 根包 `com.xly.erp`;端口 `5172`(`config-vars.yaml backend.http_port`);DB schema `xlyweberp_vibe_erp_test`;JWT 密钥取 `config-vars.yaml secrets.jwt_secret`。 +- 命令(docs/04 § 零):build `mvn -q -B -DskipTests package`;lint `mvn -q -B checkstyle:check`;unit `mvn -q -B test`;e2e 无。 +- migration:**不新增**,复用 `V1__initial_schema.sql`(spec § 4 / D4)。 + +## 合同级常量(跨 task 必须一致) + +- REST:`POST /api/usr/users`。 +- 错误码(`ResultCode` 枚举,spec § 6 / docs/05):`SUCCESS=0`、`PARAM_INVALID=40001`、`FORBIDDEN=40301`、`USERNAME_EXISTS=40901`。(本枚举同时预留后续 REQ 用:`UNAUTHORIZED=40101`、`ACCOUNT_DISABLED=40302`、`NOT_FOUND=40401`、`PAGE_PARAM_INVALID=42201`,由本 REQ 一次性建好枚举,避免后续重复改公共文件。) +- 默认值:`initialPassword` 缺省 `666666`;`sUserType` 缺省 `普通用户`;`iCanModifyBill` 缺省 `0`;`sBrandsId`/`sSubsidiaryId` 由 DB 列默认 `1111111111` 兜底(实体不强制赋值)。 +- 枚举取值:`sUserType ∈ {普通用户, 超级管理员}`;`sLanguage ∈ {中文, 英文, 繁体}`(必填,spec D1:后端不强制业务默认)。 +- 系统字段:`tCreateDate`=当前时间;`sCreator`=当前登录用户名(SecurityContext 取 `sUserName`);`sPassword`=BCrypt(initialPassword);`iIsVoid=0`;`tLastLoginDate=null`。 + +## 关键签名(首次出现处给出,跨 task 保持一致) + +- `Result`:静态工厂 `Result.success(T data)`、`Result.success()`、`Result.fail(ResultCode code, String message)`;字段 `int code` / `String message` / `T data`。 +- `ResultCode`:枚举常量 `(int code, String message)`,含 `getCode()` / `getMessage()`。 +- `BusinessException extends RuntimeException`:构造 `BusinessException(ResultCode code)` 与 `BusinessException(ResultCode code, String message)`;暴露 `getResultCode()`。 +- `BaseEntity`:受保护公共字段 `Integer iIncrement`(`@TableId(type = IdType.AUTO)`)/ `String sId` / `String sBrandsId` / `String sSubsidiaryId` / `LocalDateTime tCreateDate`(`@TableField(fill = INSERT)`)。 +- `CreateUserDTO`:字段与校验注解见下「DTO 形状」。 +- `UsrUserService#createUser(CreateUserDTO dto)` 返回 `Integer`(新建用户主键 `iIncrement`)。 +- `UsrUserController#createUser(@Valid @RequestBody CreateUserDTO dto)` 返回 `Result>`(`data.id` = 新主键,键名 `id`)。 +- `SecurityUtil.currentUserName()` 返回 `String`(当前 JWT 主体的 `sUserName`)。 +- `SecurityUtil.currentUserType()` 返回 `String`(当前主体 `sUserType`,用于管理员判定)。 + +### DTO 形状(`CreateUserDTO`) + +| 字段 | 类型 | 校验注解 | +|---|---|---| +| `sUserName` | String | `@NotBlank` + `@Pattern(regexp="^[A-Za-z0-9_]{3,20}$")` | +| `sUserNo` | String | `@Size(max=50)` | +| `iEmployeeId` | Integer | —(存在性在 Service 校验) | +| `sUserType` | String | `@Pattern(regexp="^(普通用户|超级管理员)$")`(为空时 Service 兜底 `普通用户`,故不加 `@NotBlank`) | +| `sLanguage` | String | `@NotBlank` + `@Pattern(regexp="^(中文|英文|繁体)$")` | +| `iCanModifyBill` | Integer | `@Min(0)` + `@Max(1)`(为空时 Service 兜底 0) | +| `permissionIds` | `List` | —(元素存在性在 Service 校验) | +| `initialPassword` | String | `@Size(max=100)`(为空时 Service 兜底 `666666`) | + +> 注:`@Valid` 失败由 `GlobalExceptionHandler` 统一转 `40001`。枚举/格式越界既可由注解触发(如已传非法值),也由 Service 兜底前的显式校验保证;Service 对「关联 id 不存在」「枚举越界(兜底后再判)」抛 `BusinessException(PARAM_INVALID)`。 + +--- + +## 任务清单(每个 task = red → green → 子会话验证 PASS → commit;粒度 2-5 分钟) + +> 说明:T1-T3 为项目骨架初始化(首个 REQ 必需,否则无可编译/可测的工程)。骨架就绪后 T4 起进入本 REQ 业务红绿循环。每个 task 完成后单独 commit(业务类 commit subject 带 `REQ-USR-001`)。 + +### T1 — Maven 骨架 + 启动 + 健康自检(绿) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/ErpApplicationTests.java::contextLoads` —— Spring 上下文能加载(`@SpringBootTest`,激活 `test` profile)。意图:证明 pom 依赖 / 启动类 / `application.yml` 自洽、Flyway 能对测试库 apply V1、MyBatis-Plus 装配成功。 +- [ ] **实现**:`backend/pom.xml`(按 Tech Stack 声明依赖与构建命令对应插件,含 `checkstyle` 插件以支撑 lint 命令)、`backend/src/main/java/com/xly/erp/ErpApplication.java`、`backend/src/main/resources/application.yml`(端口 5172、数据源指向 `config-vars` 的 DB、`spring.flyway.locations=filesystem:../sql/migrations`、JWT 配置占位读 env/配置)、`application-test.yml`(test profile 连同一库)。 +- [ ] **验证**:子会话跑 `mvn -q -B test -Dtest=ErpApplicationTests`(或全量 `mvn -q -B test`)PASS。 +- [ ] **commit**:`chore(usr): 初始化 backend Maven 骨架与启动类` + +### T2 — 公共响应与异常基础设施 +- [ ] **测试**:`backend/src/test/java/com/xly/erp/common/response/ResultTest.java::successCarriesCodeZeroAndData` + `failCarriesBusinessCodeAndMessage`;`backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java::businessExceptionMapsToResult` —— 校验 `Result.success(data).code==0`,`Result.fail(USERNAME_EXISTS,msg)` 带 `40901` 与 message;`GlobalExceptionHandler` 把 `BusinessException` 转为对应 `Result`(可用单元方式直接调 handler 方法断言返回 `Result`)。 +- [ ] **实现**:`common/response/Result.java`、`common/response/ResultCode.java`(含「合同级常量」全部错误码)、`common/exception/BusinessException.java`、`common/exception/GlobalExceptionHandler.java`(处理 `BusinessException`→对应码、`MethodArgumentNotValidException`→`40001`、`DuplicateKeyException`→`40901`、其余→通用系统错误且记 ERROR 日志不泄栈)。 +- [ ] **验证**:子会话跑相关测试类 PASS。 +- [ ] **commit**:`feat(usr): 统一响应体/错误码枚举/全局异常处理 REQ-USR-001` + +### T3 — BaseEntity + MP 自动填充 + Security/JWT 基础设施 +- [ ] **测试**:`backend/src/test/java/com/xly/erp/common/security/JwtUtilTest.java::generateThenParseRoundTrip` —— 用 `config-vars` 的 `jwt_secret` 签发含 `sUserName` + `sUserType` claim 的 token,再解析能取回相同值且校验通过;`SecurityConfigTest`(轻量:断言 `BCryptPasswordEncoder` Bean 存在且 `encode/matches("666666")` 成立,可作 `@SpringBootTest` 注入或单元 new)。 +- [ ] **实现**:`common/base/BaseEntity.java`(标准列字段 + MP 注解)、`common/config/MybatisPlusConfig.java`(`MetaObjectHandler` 填充 `tCreateDate`;可注册分页插件供后续 REQ)、`common/config/SecurityConfig.java`(`SecurityFilterChain`:放行 `/api/usr/login`、swagger、actuator/health;其余 `authenticated()`;注册 `BCryptPasswordEncoder` Bean;将 `JwtAuthenticationFilter` 加在 `UsernamePasswordAuthenticationFilter` 之前;无状态 session)、`common/security/JwtUtil.java`、`common/security/JwtAuthenticationFilter.java`(解析 `Authorization: Bearer`,把 `sUserName`/`sUserType` 放入 `Authentication` 的 principal/authorities)、`common/security/SecurityUtil.java`(`currentUserName()` / `currentUserType()` 从 `SecurityContextHolder` 取)。 +- [ ] **验证**:子会话跑 `JwtUtilTest` + Security 相关测试 PASS。 +- [ ] **commit**:`feat(usr): BaseEntity 与 MP 自动填充、Security/JWT 基础设施 REQ-USR-001` + +### T4 — USR 实体 + Mapper + CreateUserDTO(含 Bean Validation) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/dto/CreateUserDTOValidationTest.java`: + - `::rejectsBadUserName` —— `sUserName="ab"`(<3 位)或含非法字符触发约束违反。 + - `::rejectsIllegalLanguage` —— `sLanguage="日文"` 违反 `@Pattern`。 + - `::acceptsMinimalValidBody` —— `{sUserName:"good_user", sLanguage:"中文"}` 无违反(`sUserType`/`iCanModifyBill`/`initialPassword` 允许空,Service 兜底)。 + (用 `jakarta.validation.Validator` 直接 validate DTO 断言 violations 数量。) +- [ ] **实现**:`modules/usr/entity/{UsrUser,UsrUserPermission,UsrEmployee,UsrPermission}.java`(`@TableName` 映射对应表,字段名与匈牙利前缀列名一致;`UsrUser`/关联表可继承 `BaseEntity` 复用标准列,业务列按 docs/03 列出,`sPassword` 标 `@TableField` 不参与序列化输出可用 VO 层控制——本 REQ 不返回实体)、`modules/usr/mapper/{UsrUserMapper,UsrUserPermissionMapper,UsrEmployeeMapper,UsrPermissionMapper}.java`(继承 `BaseMapper`)、`modules/usr/dto/CreateUserDTO.java`(按「DTO 形状」加校验注解)。 +- [ ] **验证**:子会话跑 `CreateUserDTOValidationTest` PASS。 +- [ ] **commit**:`feat(usr): 用户相关实体/Mapper/CreateUserDTO 校验 REQ-USR-001` + +### T5 — Service:用户名查重 + 密码哈希 + 落库(核心写) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/service/UsrUserServiceImplTest.java`(Mockito 单元,mock 4 个 Mapper + `BCryptPasswordEncoder` + `SecurityUtil`): + - `::createUserHashesPasswordAndSetsAuditFields` —— 给最小合法 DTO(不传 `initialPassword`),断言传给 `UsrUserMapper.insert` 的 `UsrUser`:`sPassword` 非明文 `666666` 且 `encoder.encode` 被以 `666666` 调用、`iIsVoid==0`、`sUserType=="普通用户"`、`sCreator==当前用户名`、`tLastLoginDate==null`,返回新主键。 + - `::duplicateUserNameThrows40901` —— 查重命中(mock 查到已存在)→ 抛 `BusinessException(USERNAME_EXISTS)`。 + - `::duplicateKeyExceptionTranslatesTo40901` —— `insert` 抛 `DuplicateKeyException` → Service 转 `BusinessException(USERNAME_EXISTS)`(并发兜底)。 +- [ ] **实现**:`modules/usr/service/UsrUserService.java`(接口 `Integer createUser(CreateUserDTO)`)+ `modules/usr/service/impl/UsrUserServiceImpl.java`:先按 `sUserName` 查重命中抛 `40901`;兜底默认值(`sUserType`/`iCanModifyBill`/`initialPassword`/`sLanguage` 非法再校验抛 `40001`);`BCryptPasswordEncoder.encode` 哈希;填审计字段;`insert` 捕获 `DuplicateKeyException` 转 `40901`;方法标 `@Transactional(rollbackFor = Exception.class)`。 +- [ ] **验证**:子会话跑 `UsrUserServiceImplTest`(上述三个用例)PASS。 +- [ ] **commit**:`feat(usr): 新增用户 Service 查重/哈希/落库 REQ-USR-001` + +### T6 — Service:关联职员/权限存在性校验 + 权限批量授权 +- [ ] **测试**:续 `UsrUserServiceImplTest`: + - `::nonExistentEmployeeThrows40001` —— 传 `iEmployeeId` 但 `UsrEmployeeMapper` 查不到 → `BusinessException(PARAM_INVALID)`,不 insert。 + - `::nonExistentPermissionThrows40001` —— `permissionIds` 含库中不存在 id → `40001`,回滚(不写 user_permission)。 + - `::grantsDedupedPermissions` —— `permissionIds=[a,a,b]`(均存在)→ 去重后对 `UsrUserPermissionMapper` 批量插入 2 行 `(newUserId,a)`/`(newUserId,b)`(断言插入次数/内容)。 +- [ ] **实现**:在 `UsrUserServiceImpl.createUser` 内:`iEmployeeId` 非空时校验 `usr_employee` 存在(不存在抛 `40001`);`permissionIds` 非空时去重并逐个/批量校验 `usr_permission` 存在(缺失抛 `40001`),用户落库后用 `iIncrement` 批量写 `usr_user_permission`;全程同一 `@Transactional`。 +- [ ] **验证**:子会话跑上述三用例 PASS。 +- [ ] **commit**:`feat(usr): 新增用户关联职员/权限校验与授权写入 REQ-USR-001` + +### T7 — Controller + 权限前置(管理员判定) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/controller/UsrUserControllerTest.java`(`@WebMvcTest(UsrUserController.class)` + MockMvc,mock `UsrUserService` 与安全上下文): + - `::adminCreateReturnsCodeZeroWithId` —— 模拟管理员(`sUserType=超级管理员`)POST 合法 body → HTTP 200,响应 `code==0`,`data.id` 为返回主键;响应体不含 `sPassword`/明文密码。 + - `::nonAdminReturns40301` —— 模拟普通用户(`sUserType=普通用户`)→ `code==40301`,Service.createUser 不被调用。 + - `::invalidBodyReturns40001` —— `sUserName` 非法 → `code==40001`(经 `@Valid` + `GlobalExceptionHandler`)。 +- [ ] **实现**:`modules/usr/controller/UsrUserController.java`:`@PostMapping("/api/usr/users")`,`@Valid @RequestBody CreateUserDTO`;入口判定 `SecurityUtil.currentUserType()` 非 `超级管理员` 抛 `BusinessException(FORBIDDEN)`(先于业务校验,spec § 3.9);委派 `service.createUser`;返回 `Result.success(Map.of("id", newId))`。Controller 不直接调 Mapper、不写业务逻辑。 +- [ ] **验证**:子会话跑 `UsrUserControllerTest` PASS。 +- [ ] **commit**:`feat(usr): 新增用户 Controller 与管理员权限前置 REQ-USR-001` + +### T8 — 端到端验收回归(按 spec § 7 验收标准收口) +- [ ] **测试**:`backend/src/test/java/com/xly/erp/modules/usr/UsrUserCreateIT.java`(`@SpringBootTest` + MockMvc,test profile 连测试库,Flyway 已 apply V1;测试内预置/清理 fixture 数据)覆盖 spec § 7: + - `::ac1_normalCreatePersistsHashedPassword` —— 管理员合法 body → `code=0`,库中新增一行,`sPassword` 可被 `encoder.matches("666666", hash)` 通过,`iIsVoid=0`、`sCreator`=调用者、`tCreateDate` 已填。 + - `::ac2_duplicateUserNameRollsBack` —— 重复 `sUserName` → `40901`,无新增行。 + - `::ac4_permissionGrantWritesRows` —— `permissionIds=[a,b]`(先插 fixture 权限)→ `usr_user_permission` 新增 2 行。 + - `::ac5_nonAdminForbidden` —— 普通用户/无 token → `40301` / 401,不创建记录。 + - `::ac7_responseHasNoPassword` —— 成功响应体仅含 `data.id`,无密码字段。 + (IT 通过真实 BCrypt + 真实库写入做端到端确认;如环境无法连库,子会话需明确报告而非静默跳过。) +- [ ] **实现**:仅在前序 task 暴露缺口时做最小修补(如审计字段填充、权限去重边界),不引入新公共契约。 +- [ ] **验证**:子会话跑 `UsrUserCreateIT` PASS(连库);随后跑全量 `mvn -q -B test` 全绿,`mvn -q -B checkstyle:check` 通过。 +- [ ] **commit**:`test(usr): 新增用户端到端验收回归 REQ-USR-001` + +--- + +## 自审 + +### 占位符扫描 +- 全文无 `【人工填写】` / `TBD` / `TODO` / 待定占位(DB 文档 D1/D5 的「需用户审阅」遗留标记不在本后端 REQ 作用域,按 spec D1 取「`sLanguage` 必填、不强制业务默认」继续)。 + +### Spec coverage(spec 每节 → task 映射) +- § 1 Goal → T7(端点)+ 全部 task。 +- § 2.1 输入 / DTO 字段与校验 → T4(DTO 校验)+ T5/T6(Service 兜底与存在性)。 +- § 2.2 输出 `Result<{id}>` → T2(Result)+ T7(Controller 组装)。 +- § 3.1 用户名唯一(含并发 DuplicateKey 转 40901)→ T5。 +- § 3.2 密码默认 + BCrypt + 不落明文/不进响应 → T3(encoder)+ T5(哈希)+ T7/T8(响应不含密码)。 +- § 3.3 用户类型默认与约束 → T4(注解)+ T5(兜底/越界 40001)。 +- § 3.4 语言取值约束 → T4(`@Pattern`)。 +- § 3.5 关联职员存在性 → T6。 +- § 3.6 权限组多对多授权 + 去重 + 唯一索引 → T6。 +- § 3.7 新建即生效 `iIsVoid=0` → T5(审计/状态字段)+ T8(AC1)。 +- § 3.8 制单人/创建时间审计 → T3(MetaObjectHandler)+ T5(显式赋值 sCreator)。 +- § 3.9 权限前置(非管理员 40301,先于业务)→ T7。 +- § 4 约束(分层/包路径/命名/统一响应/异常/事务/认证/数据访问/配置/schema)→ T1-T7 分层落位,schema 复用 V1(无新 migration)。 +- § 5 Schema 引用 → T4(实体/Mapper 映射 4 表)。 +- § 6 错误码 → T2(ResultCode 枚举)。 +- § 7 验收标准 1-7 → T8(IT 覆盖 AC1/2/4/5/7;AC3 参数非法在 T4+T7,AC6 默认密码在 T5+T8 AC1)。 +- § 8 decisions(D1-D5)→ 已体现于「合同级常量」「DTO 形状」与不新增 migration(D4)。 + +### 类型一致性 +- `Result` / `ResultCode` / `BusinessException` 签名在 T2 定义,T5/T6/T7 一致引用。 +- `CreateUserDTO` 字段与校验在 T4 锁定,T5/T6/T7/T8 一致使用。 +- `UsrUserService#createUser(CreateUserDTO):Integer` 与 `UsrUserController#createUser` 返回 `Result`(`data.id`)跨 T5/T7 一致。 +- 错误码字面量 `0/40001/40301/40901` 与 docs/05、spec § 6 一致。 +- REST 路径 `/api/usr/users`、放行路径 `/api/usr/login` 与 docs/05 一致。