req_id: REQ-USR-001 date: 2026-05-13
spec_ref: docs/superpowers/specs/2026-05-13-REQ-USR-001.md
REQ-USR-001 增加用户 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 实现 POST /api/usr/user 增加用户接口;本 REQ 作为后端阶段第一个落地任务,同步建立 Spring Boot 工程骨架与通用基础设施(Result/异常/Security/JWT/MyBatis-Plus)供后续 REQ 复用。
Architecture: 分三段实现 — (A) 工程骨架 + Flyway V2 种子;(B) common 层基础设施(统一响应/异常/JWT/Security);(C) USR 模块 createUser 业务实现 + Controller 集成测试。每段任务遵循 RED→GREEN→Commit;Service 用 Mockito 单元测试 + Controller 用 @SpringBootTest + MockMvc 集成测试。
Tech Stack: Spring Boot 3.3.x(含 Web/Security/Validation)、MyBatis-Plus 3.5.x、Flyway 10.x、jjwt 0.12.x、MapStruct + Lombok、MySQL 8、JDK 21(.env.local JAVA_HOME=/opt/homebrew/opt/openjdk@21/...)、Maven 3.9。
Schema 改动
需要 migration:sql/migrations/V2__seed_admin_and_permissions.sql,作用:种子 4 条权限分类(usr:user:create / update / list / assign-role)+ 1 条种子超管 admin(哈希密码:admin)+ 4 条 admin × 权限授权。
完整 DDL(写死,schema 是事实源):
-- V2__seed_admin_and_permissions.sql
-- 种子权限分类(覆盖 USR 模块全部 REQ 所需)
INSERT INTO tPermission (sCategory, sCategoryName, sDescription, iIsDisabled)
VALUES
('usr:user:create', '新增用户', 'REQ-USR-001 增加用户', 0),
('usr:user:update', '修改用户', 'REQ-USR-002 修改用户', 0),
('usr:user:list', '查询用户', 'REQ-USR-003 查询用户', 0),
('usr:user:assign-role', '分配用户角色', '修改用户类型 NORMAL/ADMIN 用', 0);
-- 种子超级管理员(密码明文 'admin',下方哈希在 Task 2 执行前用 BCryptPasswordEncoder.encode('admin') 本地生成一次,
-- 长度 60、以 $2a$10$ 开头;占位 BCRYPT_ADMIN_HASH 在 Task 2 中实际替换为真实 BCrypt 字符串)
INSERT INTO tUser
(sUserCode, sUserName, iEmployeeId, sUserType, sLanguage, iCanEditDoc,
sPasswordHash, iIsDisabled, iLoginFailCount,
sBrandsId, sSubsidiaryId, sCreatedBy)
VALUES
('ADMIN001', 'admin', NULL, 'ADMIN', 'zh-CN', 1,
'{BCRYPT_ADMIN_HASH}', 0, 0,
'BR-DEFAULT', 'SUB-DEFAULT', 'system');
-- admin × 4 个权限授权
INSERT INTO tUserPermission (iUserId, iPermissionId, sGrantedBy, sBrandsId, sSubsidiaryId)
SELECT u.iIncrement, p.iIncrement, 'system', 'BR-DEFAULT', 'SUB-DEFAULT'
FROM tUser u
CROSS JOIN tPermission p
WHERE u.sUserName = 'admin'
AND p.sCategory IN ('usr:user:create','usr:user:update','usr:user:list','usr:user:assign-role');
合约级常量(跨任务复用,写死)
- 错误码(数字字符串,作为
Result.code: int,与 docs/05 一致):-
200成功 -
40001必填字段缺失或格式不合法 -
40002userName 已存在 -
40003userCode 已存在 -
40004employeeId 非法(不存在或已作废) -
40005permissionIds 含非法项 -
40101未认证 -
40301权限不足 -
50000系统内部错误
-
- JWT claim 名(HS256,secret 取
${JWT_SECRET}):-
sub= userName -
uid= userId(int) -
bid= brandsId -
sid= subsidiaryId -
auth= authorities(List<String>,如["usr:user:create"]) -
exp= now + 24h
-
- BCrypt 前缀:
$2a$或$2b$,长度 60 - DB 列名匈牙利前缀映射:
User.userName↔tUser.sUserName、User.iIncrement主键标@TableId(type = IdType.AUTO)
文件变更清单
全部为 create(项目根 backend/ 目录此前不存在)。
工程根
-
backend/pom.xml— Maven POM;Spring Boot parent + 业务/测试依赖(create) -
backend/.gitignore—target/*.iml.idea/(create)
main/java
-
backend/src/main/java/com/xly/test4/Application.java—@SpringBootApplication入口(create) -
backend/src/main/java/com/xly/test4/common/response/Result.java— 统一响应包装(create) -
backend/src/main/java/com/xly/test4/common/response/ResultCode.java— 错误码常量(create) -
backend/src/main/java/com/xly/test4/common/exception/BusinessException.java— 业务异常(create) -
backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java—@RestControllerAdvice全局异常(create) -
backend/src/main/java/com/xly/test4/common/security/CurrentUser.java— JWT 解析后的 principal 值对象(create) -
backend/src/main/java/com/xly/test4/common/security/CurrentUserContext.java— 静态工具读 SecurityContext(create) -
backend/src/main/java/com/xly/test4/common/security/JwtTokenProvider.java— 签发/解析 JWT(create) -
backend/src/main/java/com/xly/test4/common/security/JwtAuthenticationFilter.java—OncePerRequestFilter(create) -
backend/src/main/java/com/xly/test4/common/security/RestAuthenticationEntryPoint.java— 401 → Result(40101)(create) -
backend/src/main/java/com/xly/test4/common/security/RestAccessDeniedHandler.java— 403 → Result(40301)(create) -
backend/src/main/java/com/xly/test4/common/security/SecurityConfig.java—@EnableMethodSecurity+ SecurityFilterChain(create) -
backend/src/main/java/com/xly/test4/common/config/MybatisPlusConfig.java— 分页插件(create) -
backend/src/main/java/com/xly/test4/module/usr/entity/User.java—tUser映射(create) -
backend/src/main/java/com/xly/test4/module/usr/entity/UserPermission.java—tUserPermission映射(create) -
backend/src/main/java/com/xly/test4/module/usr/entity/Employee.java—tEmployee映射(create) -
backend/src/main/java/com/xly/test4/module/usr/entity/Permission.java—tPermission映射(create) -
backend/src/main/java/com/xly/test4/module/usr/mapper/UserMapper.java—extends BaseMapper<User>(create) -
backend/src/main/java/com/xly/test4/module/usr/mapper/UserPermissionMapper.java—extends BaseMapper<UserPermission>(create) -
backend/src/main/java/com/xly/test4/module/usr/mapper/EmployeeMapper.java—extends BaseMapper<Employee>(create) -
backend/src/main/java/com/xly/test4/module/usr/mapper/PermissionMapper.java—extends BaseMapper<Permission>(create) -
backend/src/main/java/com/xly/test4/module/usr/dto/UserCreateDTO.java— 入参(Bean Validation)(create) -
backend/src/main/java/com/xly/test4/module/usr/vo/UserCreateVO.java— 出参(create) -
backend/src/main/java/com/xly/test4/module/usr/converter/UserConverter.java— MapStruct(create) -
backend/src/main/java/com/xly/test4/module/usr/service/UserService.java— Service 接口(create) -
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java— 实现(create) -
backend/src/main/java/com/xly/test4/module/usr/controller/UserController.java— Controller(create)
main/resources
-
backend/src/main/resources/application.yml— 主配置(create) -
backend/src/main/resources/application-dev.yml— dev profile(create)
test/java
-
backend/src/test/java/com/xly/test4/ApplicationContextIT.java— 上下文加载 + Flyway 校验(create) -
backend/src/test/java/com/xly/test4/common/response/ResultTest.java— 单元(create) -
backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java— 单元(create) -
backend/src/test/java/com/xly/test4/common/security/JwtTokenProviderTest.java— 单元(create) -
backend/src/test/java/com/xly/test4/common/security/SecurityIntegrationIT.java— 集成(create) -
backend/src/test/java/com/xly/test4/module/usr/dto/UserCreateDTOValidationTest.java— Bean Validation 单元(create) -
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java— Service 单元(Mockito)(create) -
backend/src/test/java/com/xly/test4/module/usr/controller/UserControllerIT.java— Controller 集成(MockMvc)(create) -
backend/src/test/java/com/xly/test4/support/TestJwtFactory.java— 测试 JWT 签发工具(create)
sql
-
sql/migrations/V2__seed_admin_and_permissions.sql— 种子(create)
任务步骤
Task 1: 工程骨架 — pom.xml + Application + application.yml
Files:
- Create:
backend/pom.xml - Create:
backend/.gitignore - Create:
backend/src/main/java/com/xly/test4/Application.java - Create:
backend/src/main/resources/application.yml - Create:
backend/src/main/resources/application-dev.yml - Test:
backend/src/test/java/com/xly/test4/ApplicationContextIT.java
API shape / 关键配置:
-
pom.xml<parent>=spring-boot-starter-parent3.3.x;依赖:spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-validation、com.baomidou:mybatis-plus-spring-boot3-starter:3.5.7、org.flywaydb:flyway-core、org.flywaydb:flyway-mysql、com.mysql:mysql-connector-j、io.jsonwebtoken:jjwt-api:0.12.5+jjwt-impl+jjwt-jackson(runtime)、org.mapstruct:mapstruct:1.5.5.Final+org.projectlombok:lombok:1.18.32+org.projectlombok:lombok-mapstruct-binding、org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0。test 域:spring-boot-starter-test、spring-security-test -
Application.java:@SpringBootApplication+@MapperScan("com.xly.test4.module.*.mapper") -
application.yml关键键:spring.datasource.url=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai-
spring.datasource.username=${DB_USER}、spring.datasource.password=${DB_PASSWORD} -
spring.flyway.enabled=true、spring.flyway.locations=filesystem:../sql/migrations、spring.flyway.baseline-on-migrate=false -
mybatis-plus.mapper-locations=classpath:mapper/**/*.xml、mybatis-plus.global-config.db-config.id-type=AUTO -
app.security.default-password=666666、app.security.max-login-fail=5、app.security.lock-minutes=30 -
app.security.jwt.secret=${JWT_SECRET}、app.security.jwt.access-ttl-hours=24 -
server.port=8080、spring.profiles.active=dev
application-dev.yml:logging.level.com.xly.test4=DEBUG-
Step 1: 写失败测试
- 测试文件:
backend/src/test/java/com/xly/test4/ApplicationContextIT.java - 测试名:
ApplicationContextIT#contextLoads - 意图:
@SpringBootTest启动整个 Spring 上下文,断言ApplicationContext非空(即工程能跑起来 + Flyway 能 apply 已有的 V1) - 期望 FAIL 原因:
Application类、application.yml、pom.xml 都不存在 → 编译/启动失败
- 测试文件:
-
Step 2: 实现最小代码
- 创建
backend/pom.xml含上述全部依赖 - 创建
backend/.gitignore - 创建
backend/src/main/java/com/xly/test4/Application.java(仅@SpringBootApplication+@MapperScan+ main 方法) - 创建
backend/src/main/resources/application.yml+application-dev.yml - 启动前确保仓库根
sql/migrations/V1__initial_schema.sql已存在(事实如此),Flyway 会自动 apply V1
- 创建
-
Step 3: 子会话验证 PASS
- 子会话执行:
bash scripts/setup-test-db.sh && cd backend && mvn -DfailIfNoTests=false -Dtest=ApplicationContextIT test - 期望:BUILD SUCCESS,
ApplicationContextITPASS
- 子会话执行:
-
Step 4: Commit
git add backend/pom.xml backend/.gitignore backend/src/main/java/com/xly/test4/Application.java backend/src/main/resources/application.yml backend/src/main/resources/application-dev.yml backend/src/test/java/com/xly/test4/ApplicationContextIT.javagit commit -m "feat(usr): 后端工程骨架 (Spring Boot 3.3 + MyBatis-Plus + Flyway) REQ-USR-001"
Task 2: Flyway V2 种子 migration
Files:
- Create:
sql/migrations/V2__seed_admin_and_permissions.sql - Modify:
backend/src/test/java/com/xly/test4/ApplicationContextIT.java(增加种子断言)
API shape:
- migration 全文见 § Schema 改动;
{BCRYPT_ADMIN_HASH}占位实现时本地执行一次new BCryptPasswordEncoder().encode("admin")取真实哈希字符串(长度 60、以$2a$10$开头)落 SQL 断言:
tPermission至少含usr:user:create / usr:user:update / usr:user:list / usr:user:assign-role4 行;tUser含sUserName='admin' AND sUserType='ADMIN' AND iIsDisabled=0;tUserPermission该 admin 关联 4 个权限-
Step 1: 写失败测试
- 测试名:
ApplicationContextIT#flywayMigrationsApplied_seedDataPresent - 意图: 用
JdbcTemplate查tPermission行数 ≥ 4 + 4 个权限码全部存在;tUser中sUserName='admin'行存在且sUserType='ADMIN';tUserPermission中 admin × 4 个权限关联存在 - 期望 FAIL 原因:V2 文件不存在,DB 中只有 V1 创建的空表
- 测试名:
-
Step 2: 实现最小代码
- 本地 spawn Java 一次性脚本(或
mvn exec)执行new BCryptPasswordEncoder().encode("admin"),记录哈希字符串 - 创建
sql/migrations/V2__seed_admin_and_permissions.sql,将{BCRYPT_ADMIN_HASH}替换为真实哈希
- 本地 spawn Java 一次性脚本(或
-
Step 3: 子会话验证 PASS
- 子会话执行:
bash scripts/setup-test-db.sh && cd backend && mvn -Dtest=ApplicationContextIT test - 期望:两个测试方法均 PASS(contextLoads + flywayMigrationsApplied_seedDataPresent)
- 子会话执行:
-
Step 4: Commit
git add sql/migrations/V2__seed_admin_and_permissions.sql backend/src/test/java/com/xly/test4/ApplicationContextIT.javagit commit -m "feat(usr): V2 种子超管 + USR 权限分类 REQ-USR-001"
Task 3: MyBatis-Plus 分页配置
Files:
- Create:
backend/src/main/java/com/xly/test4/common/config/MybatisPlusConfig.java
API shape:
MybatisPlusConfig(@Configuration):暴露MybatisPlusInterceptorbean,内含PaginationInnerInterceptor(DbType.MYSQL)-
Step 1: 写失败测试
- 测试文件: 复用
ApplicationContextIT,新增方法paginationInterceptorRegistered - 意图:
@Autowired MybatisPlusInterceptor,断言其 inner interceptors 含PaginationInnerInterceptor - 期望 FAIL 原因:
MybatisPlusInterceptorbean 不存在 → 注入失败
- 测试文件: 复用
-
Step 2: 实现最小代码
- 创建
MybatisPlusConfig.java,注册分页拦截器
- 创建
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=ApplicationContextIT test
-
Step 4: Commit
git add backend/src/main/java/com/xly/test4/common/config/MybatisPlusConfig.java backend/src/test/java/com/xly/test4/ApplicationContextIT.javagit commit -m "feat(usr): MyBatis-Plus 分页拦截器配置 REQ-USR-001"
Task 4: Result + ResultCode 统一响应
Files:
- Create:
backend/src/main/java/com/xly/test4/common/response/Result.java - Create:
backend/src/main/java/com/xly/test4/common/response/ResultCode.java - Test:
backend/src/test/java/com/xly/test4/common/response/ResultTest.java
API shape:
-
Result<T>字段:int code、String message、T data、long timestamp - 静态方法:
-
public static <T> Result<T> success(T data)→code=200, message="操作成功" -
public static <T> Result<T> fail(int code, String message)→data=null - 构造时
timestamp = System.currentTimeMillis()
-
ResultCode(public final class,全大写常量):OK=200、PARAM_INVALID=40001、USER_NAME_DUPLICATE=40002、USER_CODE_DUPLICATE=40003、EMPLOYEE_INVALID=40004、PERMISSION_INVALID=40005、UNAUTHENTICATED=40101、FORBIDDEN=40301、INTERNAL_ERROR=50000-
Step 1: 写失败测试
- 测试名:
ResultTest#successContainsDataAndCode200、#failNullsDataAndSetsCustomCode - 期望 FAIL 原因:
Result/ResultCode不存在
- 测试名:
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=ResultTest test
-
Step 4: Commit
git commit -m "feat(usr): 统一响应 Result + 错误码常量 REQ-USR-001"
Task 5: BusinessException + GlobalExceptionHandler
Files:
- Create:
backend/src/main/java/com/xly/test4/common/exception/BusinessException.java - Create:
backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java - Test:
backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java
API shape:
-
BusinessException extends RuntimeException:构造BusinessException(int code, String message);gettergetCode()/ 继承getMessage() -
GlobalExceptionHandler(@RestControllerAdvice):-
@ExceptionHandler(BusinessException.class)→Result.fail(e.getCode(), e.getMessage()) -
@ExceptionHandler(MethodArgumentNotValidException.class)→Result.fail(40001, firstFieldError.getDefaultMessage()) -
@ExceptionHandler(AccessDeniedException.class)→Result.fail(40301, "权限不足") -
@ExceptionHandler(DuplicateKeyException.class)→ 解析e.getCause().getMessage()中包含uk_tUser_sUserName→ 40002;含uk_tUser_sUserCode→ 40003;否则 50000 -
@ExceptionHandler(Exception.class)兜底 →Result.fail(50000, "系统繁忙,请稍后重试"),堆栈进 SLF4Jlog.error
-
-
Step 1: 写失败测试
- 测试名: 在
GlobalExceptionHandlerTest中: -
#handleBusinessException_returnsFailResultWithCodeAndMessage— 构造BusinessException(40002,"用户名已存在")调 handler,断言返回Result.code==40002 && message=="用户名已存在" -
#handleDuplicateKey_userNameIndex_returns40002— 构造 cause 含uk_tUser_sUserName的DuplicateKeyException,断言 40002 #handleDuplicateKey_userCodeIndex_returns40003#handleAccessDenied_returns40301#handleGenericException_returns50000_noStackInResponse- 期望 FAIL 原因:类不存在
- 测试名: 在
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=GlobalExceptionHandlerTest test
-
Step 4: Commit
git commit -m "feat(usr): 全局异常处理器 + BusinessException REQ-USR-001"
Task 6: JwtTokenProvider + CurrentUser + CurrentUserContext
Files:
- Create:
backend/src/main/java/com/xly/test4/common/security/CurrentUser.java - Create:
backend/src/main/java/com/xly/test4/common/security/CurrentUserContext.java - Create:
backend/src/main/java/com/xly/test4/common/security/JwtTokenProvider.java - Test:
backend/src/test/java/com/xly/test4/common/security/JwtTokenProviderTest.java
API shape:
-
CurrentUser(Lombok@Data + @Builder):Integer userId; String userName; String brandsId; String subsidiaryId; List<String> authorities; -
CurrentUserContext:静态方法public static CurrentUser current()从SecurityContextHolder.getContext().getAuthentication().getPrincipal()强转为CurrentUser;未认证(principal 不是CurrentUser)抛BusinessException(40101,"未认证") -
JwtTokenProvider(@Component,构造注入@Value("${app.security.jwt.secret}") String secret+@Value("${app.security.jwt.access-ttl-hours}") long ttlHours):-
String issue(CurrentUser user)— HS256 签名,claims 含 sub/uid/bid/sid/auth/exp(见 § 合约级常量) -
CurrentUser parse(String token)— 解析失败/过期抛BusinessException(40101,"未认证")
-
-
Step 1: 写失败测试
- 测试名:
-
JwtTokenProviderTest#issueThenParse_roundTripsAllClaims— issue + parse 后 userName/userId/brandsId/subsidiaryId/authorities 与原值相等 JwtTokenProviderTest#parseExpiredToken_throws40101JwtTokenProviderTest#parseTamperedSignature_throws40101- 测试构造方式:直接
new JwtTokenProvider("test-secret-pure-ascii-at-least-32-bytes-long-xxxx", 24)(HS256 要求 secret ≥ 32 字节,纯 ASCII 字节数 == 字符数) - 期望 FAIL 原因:类不存在
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=JwtTokenProviderTest test
-
Step 4: Commit
git commit -m "feat(usr): JWT 签发/解析 + CurrentUser 上下文 REQ-USR-001"
Task 7: Security 链 — Filter + EntryPoint + AccessDeniedHandler + SecurityConfig
Files:
- Create:
backend/src/main/java/com/xly/test4/common/security/JwtAuthenticationFilter.java - Create:
backend/src/main/java/com/xly/test4/common/security/RestAuthenticationEntryPoint.java - Create:
backend/src/main/java/com/xly/test4/common/security/RestAccessDeniedHandler.java - Create:
backend/src/main/java/com/xly/test4/common/security/SecurityConfig.java - Create:
backend/src/test/java/com/xly/test4/support/TestJwtFactory.java - Test:
backend/src/test/java/com/xly/test4/common/security/SecurityIntegrationIT.java
API shape:
-
JwtAuthenticationFilter extends OncePerRequestFilter:- 读
Authorization头;缺失 → 直接放行(交给后续链/EntryPoint 决定) - 非
Bearer前缀 → 直接放行 - 调
jwtTokenProvider.parse(token)得CurrentUser;构造UsernamePasswordAuthenticationToken(currentUser, null, authoritiesAsSimpleGrantedAuthority)塞入SecurityContextHolder - 解析失败(捕获
BusinessException)→ 清 context,继续 chain
- 读
-
RestAuthenticationEntryPoint.commence(...)→ 写 HTTP 401 +Result.fail(40101,"未认证")JSON -
RestAccessDeniedHandler.handle(...)→ 写 HTTP 403 +Result.fail(40301,"权限不足")JSON -
SecurityConfig(@Configuration @EnableMethodSecurity):-
SecurityFilterChain:/api/auth/login、/v3/api-docs/**、/swagger-ui/**、/actuator/health白名单;其余/api/**.authenticated();csrf().disable();sessionCreationPolicy=STATELESS;exceptionHandling()注入两个 handler;JwtAuthenticationFilter在UsernamePasswordAuthenticationFilter之前 -
PasswordEncoderbean =BCryptPasswordEncoder
-
-
TestJwtFactory(test scope util):-
static String adminToken(JwtTokenProvider provider)— 签发种子 admin 的 token(userId=可从 DB 查,userName="admin", brandsId="BR-DEFAULT", subsidiaryId="SUB-DEFAULT", authorities=["usr:user:create","usr:user:update","usr:user:list","usr:user:assign-role"]) -
static String normalUserToken(JwtTokenProvider provider, Integer userId, String userName)— 普通用户 token,authorities=空
-
控制器桩(为本任务集成测试服务):在 SecurityIntegrationIT 内用 @RestController + @PreAuthorize("hasAuthority('usr:user:create')") 注解一个 /api/_probe/secured 端点(仅测试用,不入主源),返回 200。
-
Step 1: 写失败测试
- 测试名 (
SecurityIntegrationIT,@SpringBootTest + MockMvc): #unauthenticatedRequestToSecuredEndpoint_returns401AndCode40101#invalidJwt_returns401AndCode40101#validJwtMissingRequiredAuthority_returns403AndCode40301#validAdminJwt_returns200- 期望 FAIL 原因:filter / config 类不存在 → Spring Security 默认行为,body 不是 Result JSON
- 测试名 (
-
Step 2: 实现最小代码
- 实现 4 个类 +
TestJwtFactory
- 实现 4 个类 +
-
Step 3: 子会话验证 PASS
bash scripts/setup-test-db.sh && cd backend && mvn -Dtest=SecurityIntegrationIT test
-
Step 4: Commit
git commit -m "feat(usr): Spring Security + JWT filter 链 + 401/403 Result 转译 REQ-USR-001"
Task 8: USR 模块 4 张表 entity
Files:
- Create:
backend/src/main/java/com/xly/test4/module/usr/entity/User.java - Create:
backend/src/main/java/com/xly/test4/module/usr/entity/UserPermission.java - Create:
backend/src/main/java/com/xly/test4/module/usr/entity/Employee.java - Create:
backend/src/main/java/com/xly/test4/module/usr/entity/Permission.java - Create:
backend/src/main/java/com/xly/test4/module/usr/mapper/UserMapper.java - Create:
backend/src/main/java/com/xly/test4/module/usr/mapper/UserPermissionMapper.java - Create:
backend/src/main/java/com/xly/test4/module/usr/mapper/EmployeeMapper.java - Create:
backend/src/main/java/com/xly/test4/module/usr/mapper/PermissionMapper.java - Test:
backend/src/test/java/com/xly/test4/module/usr/mapper/UserMapperIT.java
API shape:
- entity 字段命名按 docs/03(匈牙利前缀);通过 MyBatis-Plus 注解显式映射:
-
User:@TableName("tUser")@TableId(value="iIncrement", type=IdType.AUTO)私有Integer iIncrement;其余字段:sId、sBrandsId、sSubsidiaryId、tCreateDate、sUserCode、sUserName、iEmployeeId、sUserType、sLanguage、iCanEditDoc、sPasswordHash、iIsDisabled、tLastLoginDate、iLoginFailCount、tLockedUntil、sCreatedBy、tUpdateDate(类型与 docs/03 对齐:int/String/LocalDateTime/Integer) -
UserPermission:@TableName("tUserPermission")字段iIncrement、sId、sBrandsId、sSubsidiaryId、tCreateDate、iUserId、iPermissionId、sGrantedBy -
Employee:@TableName("tEmployee")字段含iIncrement、sEmployeeName、iIsDisabled等(本 REQ 仅读这 3 个,但 entity 字段完整) -
Permission:@TableName("tPermission")字段含iIncrement、sCategory、sCategoryName、iIsDisabled等
-
- Lombok
@Data + @NoArgsConstructor + @AllArgsConstructor + @Builder mapper:仅
public interface XxxMapper extends BaseMapper<Xxx> { }-
Step 1: 写失败测试
- 测试名 (
UserMapperIT,@SpringBootTest真连测试库): -
#insertAndSelectById_columnsMappedCorrectly— 构造User含全部字段,userMapper.insert(user)后用selectById(iIncrement)取回,断言所有非 null 字段一致(验证驼峰→匈牙利前缀映射) -
#seedAdminLoadable_byUserName—new LambdaQueryWrapper<User>().eq(User::getSUserName,"admin")取回种子 admin,断言sUserType="ADMIN"、sBrandsId="BR-DEFAULT" - 期望 FAIL 原因:entity / mapper 不存在
- 测试名 (
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
bash scripts/setup-test-db.sh && cd backend && mvn -Dtest=UserMapperIT test
-
Step 4: Commit
git commit -m "feat(usr): USR 模块 entity + mapper (User/UserPermission/Employee/Permission) REQ-USR-001"
Task 9: UserCreateDTO + Bean Validation
Files:
- Create:
backend/src/main/java/com/xly/test4/module/usr/dto/UserCreateDTO.java - Test:
backend/src/test/java/com/xly/test4/module/usr/dto/UserCreateDTOValidationTest.java
API shape:
-
字段(Lombok
@Data):@NotBlank @Size(max=32) String userCode@NotBlank @Size(max=50) String userName-
Integer employeeId(可空) @NotBlank @Pattern(regexp="NORMAL|ADMIN") String userType@NotBlank @Pattern(regexp="zh-CN|en|zh-TW") String language@NotNull Boolean canEditDoc-
@Size(min=1, max=64) String password(可空,留给 service 用默认) -
List<Integer> permissionIds(可空)
-
Step 1: 写失败测试
- 测试名 (
UserCreateDTOValidationTest,用Validation.buildDefaultValidatorFactory().getValidator()): -
#allRequiredFieldsValid_zeroViolations— happy path #userNameBlank_violatesNotBlank-
#userTypeOutOfEnum_violatesPattern— 传 "FOO" -
#languageOutOfEnum_violatesPattern— 传 "fr" #userCodeOver32Chars_violatesSize#canEditDocNull_violatesNotNull#passwordOver64Chars_violatesSize- 期望 FAIL 原因:DTO 不存在
- 测试名 (
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserCreateDTOValidationTest test
-
Step 4: Commit
git commit -m "feat(usr): UserCreateDTO + Bean Validation REQ-USR-001"
Task 10: UserCreateVO + UserConverter (MapStruct)
Files:
- Create:
backend/src/main/java/com/xly/test4/module/usr/vo/UserCreateVO.java - Create:
backend/src/main/java/com/xly/test4/module/usr/converter/UserConverter.java - Test:
backend/src/test/java/com/xly/test4/module/usr/converter/UserConverterTest.java
API shape:
-
UserCreateVO:Integer userId; String userCode;(Lombok@Data + @Builder) -
UserConverter(@Mapper(componentModel="spring")):-
User toEntity(UserCreateDTO dto)— 字段名映射:dto.userCode → user.sUserCode、dto.userName → user.sUserName、dto.employeeId → user.iEmployeeId、dto.userType → user.sUserType、dto.language → user.sLanguage、dto.canEditDoc(true→1, false→0) → user.iCanEditDoc(用@Mapping(target="iCanEditDoc", expression="java(dto.getCanEditDoc() ? 1 : 0)"));password / permissionIds 不映射(service 单独处理) -
UserCreateVO toVO(User user)— user.iIncrement → vo.userId、user.sUserCode → vo.userCode
-
-
Step 1: 写失败测试
- 测试名:
UserConverterTest#toEntity_mapsAllFieldsExceptPasswordAndPermissionsUserConverterTest#toEntity_canEditDocTrueMapsTo1UserConverterTest#toEntity_canEditDocFalseMapsTo0UserConverterTest#toVO_mapsIncrementToUserIdAndUserCode- 期望 FAIL 原因:converter 不存在
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserConverterTest test
-
Step 4: Commit
git commit -m "feat(usr): UserConverter (MapStruct) + UserCreateVO REQ-USR-001"
Task 11: UserService 接口 + Impl 主插入逻辑(含隐式字段 + 密码哈希)
Files:
- Create:
backend/src/main/java/com/xly/test4/module/usr/service/UserService.java - Create:
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java - Test:
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java
API shape:
-
UserService:UserCreateVO createUser(UserCreateDTO dto) -
UserServiceImpl(@Service,构造注入UserMapper、UserPermissionMapper、EmployeeMapper、PermissionMapper、UserConverter、PasswordEncoder、@Value("${app.security.default-password}") String defaultPassword):-
@Transactional(rollbackFor = Exception.class)标注createUser - 本任务实现:
-
converter.toEntity(dto)得User - 取
CurrentUserContext.current()→ 写user.sCreatedBy = current.userName、user.sBrandsId = current.brandsId、user.sSubsidiaryId = current.subsidiaryId - 密码:
String raw = dto.getPassword() != null ? dto.getPassword() : defaultPassword;→user.sPasswordHash = passwordEncoder.encode(raw) user.iIsDisabled = 0; user.iLoginFailCount = 0;-
userMapper.insert(user)→ MyBatis-Plus 自动回填iIncrement - 暂不处理 permissionIds、唯一性、外键校验(留给 Task 12-13)
return converter.toVO(user)
-
-
Step 1: 写失败测试
- 测试名 (
UserServiceImplTest,Mockito mock 全部 mapper + converter;用@MockBean或纯MockitoExtension): -
#createUser_minimalDTO_writesSecurityContextFieldsAndHashesPassword- 准备:
MockedStatic<CurrentUserContext>mockcurrent()返回 admin CurrentUser;mockuserMapper.insert(any())同时塞 iIncrement=42;mock converter 返回 entity 与 VO - 断言:
userMapper.insert(captor)捕获参数User,断言sCreatedBy="admin"、sBrandsId="BR-DEFAULT"、sSubsidiaryId="SUB-DEFAULT"、sPasswordHash.startsWith("$2")、sPasswordHash.length()==60、iIsDisabled==0、iLoginFailCount==0 - 返回 VO
userId==42
- 准备:
-
#createUser_noPasswordInDTO_usesDefaultPassword666666- 断言
passwordEncoder.matches("666666", capturedUser.sPasswordHash)
- 断言
- 期望 FAIL 原因:service / impl 不存在
- 测试名 (
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserServiceImplTest test
-
Step 4: Commit
git commit -m "feat(usr): UserService.createUser 主插入 + 隐式字段 + BCrypt 哈希 REQ-USR-001"
Task 12: 唯一性预检 + DuplicateKey 兜底
Files:
- Modify:
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java - Modify:
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java
API shape:
-
在
createUser开头插入唯一性预检(DB 兜底由 GlobalExceptionHandler 在 Task 5 已处理):if (userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getSUserName, dto.getUserName())) > 0) throw new BusinessException(40002, "用户名已存在");if (userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getSUserCode, dto.getUserCode())) > 0) throw new BusinessException(40003, "用户号已存在");
-
Step 1: 写失败测试
- 新增方法:
-
#createUser_duplicateUserName_throws40002— mockuserMapper.selectCount对 userName 查询返回 1,断言抛BusinessException且code==40002 #createUser_duplicateUserCode_throws40003- 期望 FAIL 原因:service 无预检逻辑 → 不抛 / 抛错误异常
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserServiceImplTest test
-
Step 4: Commit
git commit -m "feat(usr): createUser 唯一性预检 (40002/40003) REQ-USR-001"
Task 13: employeeId + permissionIds 存在性校验
Files:
- Modify:
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java - Modify:
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java
API shape:
-
createUser唯一性预检之后、userMapper.insert(user)之前:- employeeId 非空 →
employeeMapper.selectOne(new LambdaQueryWrapper<Employee>().eq(Employee::getIIncrement, dto.getEmployeeId()).eq(Employee::getIIsDisabled, 0)),null →throw new BusinessException(40004, "员工不存在或已作废") - permissionIds 非空 →
List<Permission> found = permissionMapper.selectList(new LambdaQueryWrapper<Permission>().in(Permission::getIIncrement, dto.getPermissionIds()).eq(Permission::getIIsDisabled, 0));若found.size() != dto.getPermissionIds().size()→throw new BusinessException(40005, "权限分类含非法项")
- employeeId 非空 →
-
Step 1: 写失败测试
- 新增方法:
#createUser_invalidEmployeeId_throws40004#createUser_disabledEmployee_throws40004-
#createUser_invalidPermissionIds_throws40005— 传 [1,2,3],mock permissionMapper.selectList 仅返回 2 行 → 40005 #createUser_permissionIdsNullOrEmpty_skipsCheck- 期望 FAIL 原因:service 无该校验
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserServiceImplTest test
-
Step 4: Commit
git commit -m "feat(usr): createUser 外键校验 (40004/40005) REQ-USR-001"
Task 14: tUserPermission 批量写入 + 事务回滚
Files:
- Modify:
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java - Modify:
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java
API shape:
-
userMapper.insert(user)之后(main 主键已回填)、return toVO之前:- permissionIds 非空 → 循环
dto.getPermissionIds():构造UserPermission(iUserId = user.getIIncrement()、iPermissionId = pid、sGrantedBy = current.userName、sBrandsId = current.brandsId、sSubsidiaryId = current.subsidiaryId),调userPermissionMapper.insert(up) - 任一 insert 抛异常 →
@Transactional已自动回滚(断言userMapper.insert也回滚 = tUser 行最终不存在;service 层只需信任 Spring)
- permissionIds 非空 → 循环
-
Step 1: 写失败测试
- 新增方法:
-
#createUser_withPermissionIds_insertsOneUserPermissionPerId— 传 permissionIds=[10,20,30],断言userPermissionMapper.insert调用 3 次;用ArgumentCaptor捕获,断言每行iUserId == user.iIncrement、iPermissionId依次 10/20/30、sGrantedBy=="admin"、sBrandsId/sSubsidiaryId与 current 一致 #createUser_emptyPermissionIds_doesNotCallUserPermissionInsert
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
cd backend && mvn -Dtest=UserServiceImplTest test
-
Step 4: Commit
git commit -m "feat(usr): tUserPermission 批量写入 + 事务边界 REQ-USR-001"
Task 15: UserController + 集成测试 happy path
Files:
- Create:
backend/src/main/java/com/xly/test4/module/usr/controller/UserController.java - Test:
backend/src/test/java/com/xly/test4/module/usr/controller/UserControllerIT.java
API shape:
-
UserController(@RestController @RequestMapping("/api/usr/user")):@PostMapping @PreAuthorize("hasAuthority('usr:user:create')") public Result<UserCreateVO> createUser(@Valid @RequestBody UserCreateDTO dto)- body:
return Result.success(userService.createUser(dto));
-
UserControllerIT(@SpringBootTest @AutoConfigureMockMvc,真连 DB):-
@BeforeEach:注入TestJwtFactory+JwtTokenProvider,签发 admin token;可选bash scripts/setup-test-db.sh之后再启动(test.sh 会做)
-
-
Step 1: 写失败测试
- 测试名:
UserControllerIT#createUser_validRequestWithAdminToken_returns200WithUserIdAndUserCode - body 示例:
json { "userCode": "U-IT-001", "userName": "it-user-001", "employeeId": null, "userType": "NORMAL", "language": "zh-CN", "canEditDoc": false, "password": "Pass1234", "permissionIds": [] } - 断言:HTTP 200;
$.code==200;$.data.userId为正整数;$.data.userCode=="U-IT-001";JdbcTemplate 查tUser WHERE sUserName='it-user-001'存在且sCreatedBy='admin'、sBrandsId='BR-DEFAULT'、sPasswordHash长度 60 起$2 - 期望 FAIL 原因:Controller 不存在 → 404
- 测试名:
Step 2: 实现最小代码
-
Step 3: 子会话验证 PASS
bash scripts/setup-test-db.sh && cd backend && mvn -Dtest=UserControllerIT test
-
Step 4: Commit
git commit -m "feat(usr): UserController.createUser + happy path 集成测试 REQ-USR-001"
Task 16: Controller 错误路径集成测试
Files:
- Modify:
backend/src/test/java/com/xly/test4/module/usr/controller/UserControllerIT.java
API shape:
在
UserControllerIT内追加 7 个错误路径测试方法(共享 admin token / normal token)-
Step 1: 写失败测试
- 新增方法(每个
@Test一对断言): -
#createUser_duplicateUserName_returns40002— 先 POST 写一个,再 POST 同 userName,第二次$.code==40002 -
#createUser_duplicateUserCode_returns40003— 同上,复用 userCode -
#createUser_invalidEmployeeId_returns40004— 传employeeId=99999(不存在) -
#createUser_invalidPermissionIds_returns40005— 传permissionIds=[99999] -
#createUser_missingUserName_returns40001— body 不带 userName -
#createUser_normalUserToken_returns40301— 用TestJwtFactory.normalUserToken(..., authorities=[])签发 token,HTTP 403 +$.code==40301 -
#createUser_noAuthHeader_returns40101— 不带 Authorization,HTTP 401 +$.code==40101 - 期望 FAIL 原因:上一任务只覆盖 happy path
- 说明:此处"上一任务"指 Task 15;不要复用 Task 15 测试方法
- 新增方法(每个
-
Step 2: 实现最小代码
- 实际上业务逻辑在 Task 11-14 已完成;此 task 只验证它们透过 Controller 链路联通
- 如果有任何断言挂红 → 回溯到 Task 11-14 修复对应 Service/Handler
-
Step 3: 子会话验证 PASS
bash scripts/setup-test-db.sh && cd backend && mvn -Dtest=UserControllerIT test- 全 8 个测试方法(含 Task 15 的 happy path)PASS
-
Step 4: Commit
git commit -m "test(usr): createUser 错误路径集成测试 (40001-40005/40101/40301) REQ-USR-001"
Task 17: 全量测试闸门
Files: (无)
-
Step 1: 子会话执行全量测试
-
bash scripts/test.sh(项目根脚本,含 setup-test-db + mvn test) - 期望:BUILD SUCCESS,全部测试通过;若失败 → 回溯具体 task
-
Step 2: (无需 commit) — 测试通过即代表本 REQ TDD 阶段完成;下游 feature-verify / feature-review 负责剩余流程
提交计划
| Task | Commit message |
|---|---|
| 1 | feat(usr): 后端工程骨架 (Spring Boot 3.3 + MyBatis-Plus + Flyway) REQ-USR-001 |
| 2 | feat(usr): V2 种子超管 + USR 权限分类 REQ-USR-001 |
| 3 | feat(usr): MyBatis-Plus 分页拦截器配置 REQ-USR-001 |
| 4 | feat(usr): 统一响应 Result + 错误码常量 REQ-USR-001 |
| 5 | feat(usr): 全局异常处理器 + BusinessException REQ-USR-001 |
| 6 | feat(usr): JWT 签发/解析 + CurrentUser 上下文 REQ-USR-001 |
| 7 | feat(usr): Spring Security + JWT filter 链 + 401/403 Result 转译 REQ-USR-001 |
| 8 | feat(usr): USR 模块 entity + mapper (User/UserPermission/Employee/Permission) REQ-USR-001 |
| 9 | feat(usr): UserCreateDTO + Bean Validation REQ-USR-001 |
| 10 | feat(usr): UserConverter (MapStruct) + UserCreateVO REQ-USR-001 |
| 11 | feat(usr): UserService.createUser 主插入 + 隐式字段 + BCrypt 哈希 REQ-USR-001 |
| 12 | feat(usr): createUser 唯一性预检 (40002/40003) REQ-USR-001 |
| 13 | feat(usr): createUser 外键校验 (40004/40005) REQ-USR-001 |
| 14 | feat(usr): tUserPermission 批量写入 + 事务边界 REQ-USR-001 |
| 15 | feat(usr): UserController.createUser + happy path 集成测试 REQ-USR-001 |
| 16 | test(usr): createUser 错误路径集成测试 (40001-40005/40101/40301) REQ-USR-001 |
| 17 | (无 commit,整体测试闸门) |