REQ-USR-001 增加用户 — 任务级 TDD 计划(后端)
阶段:后端(backend)。作用域:
backend/**(controller / service / repository(mapper) / DTO / VO / entity / 校验 / 配置 / REST 契约实现)。禁止写frontend/**。 上游 SSoT:specdocs/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,启动时自动 applysql/migrations/)。 - Spring Security + JWT(jjwt);
BCryptPasswordEncoder哈希密码;MapStruct(可选,本 REQ 转换简单可手写);Hutool。 - 根包
com.xly.erp;端口5172(config-vars.yaml backend.http_port);DB schemaxlyweberp_vibe_erp_test;JWT 密钥取config-vars.yaml secrets.jwt_secret。 - 命令(docs/04 § 零):build
mvn -q -B -DskipTests package;lintmvn -q -B checkstyle:check;unitmvn -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<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,激活testprofile)。意图:证明 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+sUserTypeclaim 的 token,再解析能取回相同值且校验通过;SecurityConfigTest(轻量:断言BCryptPasswordEncoderBean 存在且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();注册BCryptPasswordEncoderBean;将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<T>)、modules/usr/dto/CreateUserDTO.java(按「DTO 形状」加校验注解)。 - 验证:子会话跑
CreateUserDTOValidationTestPASS。 - 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,mockUsrUserService与安全上下文):-
::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、不写业务逻辑。 - 验证:子会话跑
UsrUserControllerTestPASS。 - 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 暴露缺口时做最小修补(如审计字段填充、权限去重边界),不引入新公共契约。
- 验证:子会话跑
UsrUserCreateITPASS(连库);随后跑全量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<Map>(data.id)跨 T5/T7 一致。 - 错误码字面量
0/40001/40301/40901与 docs/05、spec § 6 一致。 - REST 路径
/api/usr/users、放行路径/api/usr/login与 docs/05 一致。