2026-06-01-REQ-USR-001.md 21.2 KB

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<T>
│   │   ├── 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<UsrUser>
│       ├── mapper/UsrUserPermissionMapper.java  # BaseMapper<UsrUserPermission>
│       ├── mapper/UsrEmployeeMapper.java         # BaseMapper<UsrEmployee>(读校验)
│       ├── mapper/UsrPermissionMapper.java       # BaseMapper<UsrPermission>(读校验)
│       ├── 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;端口 5172config-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=0PARAM_INVALID=40001FORBIDDEN=40301USERNAME_EXISTS=40901。(本枚举同时预留后续 REQ 用:UNAUTHORIZED=40101ACCOUNT_DISABLED=40302NOT_FOUND=40401PAGE_PARAM_INVALID=42201,由本 REQ 一次性建好枚举,避免后续重复改公共文件。)
  • 默认值:initialPassword 缺省 666666sUserType 缺省 普通用户iCanModifyBill 缺省 0sBrandsId/sSubsidiaryId 由 DB 列默认 1111111111 兜底(实体不强制赋值)。
  • 枚举取值:sUserType ∈ {普通用户, 超级管理员}sLanguage ∈ {中文, 英文, 繁体}(必填,spec D1:后端不强制业务默认)。
  • 系统字段:tCreateDate=当前时间;sCreator=当前登录用户名(SecurityContext 取 sUserName);sPassword=BCrypt(initialPassword);iIsVoid=0tLastLoginDate=null

关键签名(首次出现处给出,跨 task 保持一致)

  • Result<T>:静态工厂 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<Map<String,Object>>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="^(普通用户
sLanguage String @NotBlank + `@Pattern(regexp="^(中文
iCanModifyBill Integer @Min(0) + @Max(1)(为空时 Service 兜底 0)
permissionIds List<Integer> —(元素存在性在 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.javabackend/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。
  • commitchore(usr): 初始化 backend Maven 骨架与启动类

T2 — 公共响应与异常基础设施

  • 测试backend/src/test/java/com/xly/erp/common/response/ResultTest.java::successCarriesCodeZeroAndData + failCarriesBusinessCodeAndMessagebackend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java::businessExceptionMapsToResult —— 校验 Result.success(data).code==0Result.fail(USERNAME_EXISTS,msg)40901 与 message;GlobalExceptionHandlerBusinessException 转为对应 Result(可用单元方式直接调 handler 方法断言返回 Result)。
  • 实现common/response/Result.javacommon/response/ResultCode.java(含「合同级常量」全部错误码)、common/exception/BusinessException.javacommon/exception/GlobalExceptionHandler.java(处理 BusinessException→对应码、MethodArgumentNotValidException40001DuplicateKeyException40901、其余→通用系统错误且记 ERROR 日志不泄栈)。
  • 验证:子会话跑相关测试类 PASS。
  • commitfeat(usr): 统一响应体/错误码枚举/全局异常处理 REQ-USR-001

T3 — BaseEntity + MP 自动填充 + Security/JWT 基础设施

  • 测试backend/src/test/java/com/xly/erp/common/security/JwtUtilTest.java::generateThenParseRoundTrip —— 用 config-varsjwt_secret 签发含 sUserName + sUserType claim 的 token,再解析能取回相同值且校验通过;SecurityConfigTest(轻量:断言 BCryptPasswordEncoder Bean 存在且 encode/matches("666666") 成立,可作 @SpringBootTest 注入或单元 new)。
  • 实现common/base/BaseEntity.java(标准列字段 + MP 注解)、common/config/MybatisPlusConfig.javaMetaObjectHandler 填充 tCreateDate;可注册分页插件供后续 REQ)、common/config/SecurityConfig.javaSecurityFilterChain:放行 /api/usr/login、swagger、actuator/health;其余 authenticated();注册 BCryptPasswordEncoder Bean;将 JwtAuthenticationFilter 加在 UsernamePasswordAuthenticationFilter 之前;无状态 session)、common/security/JwtUtil.javacommon/security/JwtAuthenticationFilter.java(解析 Authorization: Bearer,把 sUserName/sUserType 放入 Authentication 的 principal/authorities)、common/security/SecurityUtil.javacurrentUserName() / currentUserType()SecurityContextHolder 取)。
  • 验证:子会话跑 JwtUtilTest + Security 相关测试 PASS。
  • commitfeat(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<T>)、modules/usr/dto/CreateUserDTO.java(按「DTO 形状」加校验注解)。
  • 验证:子会话跑 CreateUserDTOValidationTest PASS。
  • commitfeat(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.insertUsrUsersPassword 非明文 666666encoder.encode 被以 666666 调用、iIsVoid==0sUserType=="普通用户"sCreator==当前用户名tLastLoginDate==null,返回新主键。
    • ::duplicateUserNameThrows40901 —— 查重命中(mock 查到已存在)→ 抛 BusinessException(USERNAME_EXISTS)
    • ::duplicateKeyExceptionTranslatesTo40901 —— insertDuplicateKeyException → 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 捕获 DuplicateKeyException40901;方法标 @Transactional(rollbackFor = Exception.class)
  • 验证:子会话跑 UsrUserServiceImplTest(上述三个用例)PASS。
  • commitfeat(usr): 新增用户 Service 查重/哈希/落库 REQ-USR-001

T6 — Service:关联职员/权限存在性校验 + 权限批量授权

  • 测试:续 UsrUserServiceImplTest
    • ::nonExistentEmployeeThrows40001 —— 传 iEmployeeIdUsrEmployeeMapper 查不到 → 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。
  • commitfeat(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==0data.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。
  • commitfeat(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=0sCreator=调用者、tCreateDate 已填。
    • ::ac2_duplicateUserNameRollsBack —— 重复 sUserName40901,无新增行。
    • ::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 通过。
  • committest(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):IntegerUsrUserController#createUser 返回 Result<Map>data.id)跨 T5/T7 一致。
  • 错误码字面量 0/40001/40301/40901 与 docs/05、spec § 6 一致。
  • REST 路径 /api/usr/users、放行路径 /api/usr/login 与 docs/05 一致。