req_id: REQ-USR-001 date: 2026-05-15
spec_ref: docs/superpowers/specs/2026-05-15-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/v1/auth/login,按 spec 完成 4 类校验(公司 / 作废 / 锁定 / 密码)+ 失败计数 + 锁定写入 + JWT 签发,并落地后端项目骨架以支撑后续 REQ。
Architecture:
- Spring Boot 3 + MyBatis-Plus + Flyway + BCrypt + JJWT (HS256);分层 controller → service(impl) → mapper(docs/04 § 1.1)。
- 业务逻辑全部在
LoginServiceImpl,事务边界为成功路径的"清零计数 + 更新登录时间 + 签发 JWT"原子提交。 - 失败逻辑(计数累加 + 锁定写入)在独立事务里执行;锁定时间通过
tLockUntil字段比对NOW()判定(无需 Redis)。 - 错误码集中在
ErrorCode常量类;GlobalExceptionHandler把BizException转Result.fail。
Tech Stack: Spring Boot 3.x(Java 17)/ MyBatis-Plus 最新稳定 / Flyway 10.x(core + mysql)/ Spring Security crypto(BCryptPasswordEncoder,不启用完整 Security filter chain)/ JJWT 0.12.x / Lombok / Jakarta Validation。
Schema 改动
无。V1 已建好 sys_user / sys_company / sys_employee,本 REQ 不动 schema。
文件变更清单
Bootstrap(首次 REQ 一次性投入,后续 REQ 复用):
-
backend/pom.xml— Create(Maven POM,声明依赖与插件) -
backend/src/main/java/com/xly/erp/Application.java— Create(Spring Boot 启动类) -
backend/src/main/resources/application.yml— Create(主配置,从.env.local/ 环境变量读敏感项) -
backend/src/main/resources/application-test.yml— Create(测试 profile,复用相同 schema 但禁日志彩色) -
backend/src/main/resources/logback-spring.xml— Create(最小 logback 配置)
通用基础层(首次 REQ 一次性投入):
-
backend/src/main/java/com/xly/erp/common/response/Result.java— Create -
backend/src/main/java/com/xly/erp/common/response/ErrorCode.java— Create -
backend/src/main/java/com/xly/erp/common/exception/BizException.java— Create -
backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java— Create -
backend/src/main/java/com/xly/erp/common/security/JwtUtil.java— Create -
backend/src/main/java/com/xly/erp/common/config/PasswordEncoderConfig.java— Create(BCryptPasswordEncoderBean)
业务层(REQ-USR-001 专属):
-
backend/src/main/java/com/xly/erp/module/usr/entity/SysUser.java— Create -
backend/src/main/java/com/xly/erp/module/usr/entity/SysCompany.java— Create -
backend/src/main/java/com/xly/erp/module/usr/entity/SysEmployee.java— Create(只读 join 用,最小字段) -
backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserMapper.java— Create -
backend/src/main/java/com/xly/erp/module/usr/mapper/SysCompanyMapper.java— Create -
backend/src/main/java/com/xly/erp/module/usr/mapper/SysEmployeeMapper.java— Create -
backend/src/main/java/com/xly/erp/module/usr/dto/LoginReq.java— Create -
backend/src/main/java/com/xly/erp/module/usr/vo/LoginVo.java— Create -
backend/src/main/java/com/xly/erp/module/usr/vo/UserInfoVo.java— Create -
backend/src/main/java/com/xly/erp/module/usr/service/LoginService.java— Create(接口) -
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java— Create -
backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java— Create
测试:
-
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java— Create(unit/integration with Spring Test + 真实 MySQL,按 spec 验收 1-10 项) -
backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerTest.java— Create(MockMvc端到端覆盖错误码 40001 / 40004 / 40101 / 40103 / 42301) -
backend/src/test/resources/sql/req-usr-001-seed.sql— Create(测试 fixture:1 个启用用户、1 个作废用户、2 个公司、1 个员工)
约束常量(跨任务,写死不允许漂移)
错误码(ErrorCode 常量类):
| 常量名 | 值 | HTTP |
|---|---|---|
OK |
200 |
200 |
BAD_REQUEST |
40001 |
400 |
COMPANY_NOT_FOUND |
40004 |
400 |
BAD_CREDENTIALS |
40101 |
401 |
ACCOUNT_DELETED |
40103 |
401 |
ACCOUNT_LOCKED |
42301 |
423 |
INTERNAL_ERROR |
50000 |
500 |
JWT 常量:算法 HS256;TTL 7200 秒;claims 名 sub / username / userType / companyCode / language / iat / exp / jti;签名密钥从 application.yml 注入 ${JWT_SECRET}(已在 .env.local 配置)。
锁定策略:阈值 5 次(含第 5 次触发锁定);锁定时长 30 分钟。Magic number 集中在 LoginServiceImpl 私有常量 MAX_FAILED_LOGIN_COUNT = 5 与 LOCK_DURATION_MINUTES = 30L。
API 形状:
POST /api/v1/auth/login (公开接口,无需 Bearer)
Request: LoginReq { username:String, password:String, companyCode:String }
Response: Result<LoginVo>
LoginVo { accessToken:String, tokenType:"Bearer", expiresInSec:7200, userInfo:UserInfoVo }
UserInfoVo { userId:int, username:String, userType:String, language:String,
employeeName:String?, companyCode:String }
任务步骤
Task 1: Bootstrap Spring Boot 项目骨架
Files:
- Create:
backend/pom.xml - Create:
backend/src/main/java/com/xly/erp/Application.java - Create:
backend/src/main/resources/application.yml - Create:
backend/src/main/resources/application-test.yml - Create:
backend/src/main/resources/logback-spring.xml - Create:
backend/src/test/java/com/xly/erp/ApplicationContextTest.java
配置要点(POM 内容由 TDD 实现,签名约束如下):
- Parent:
spring-boot-starter-parent:3.3.x,Java 17 - Dependencies:
spring-boot-starter-web,spring-boot-starter-validation,spring-boot-starter-test,mybatis-plus-spring-boot3-starter:3.5.x,mysql-connector-j(runtime),flyway-core+flyway-mysql,spring-security-crypto(不引入spring-boot-starter-security,避免开启 filter chain),io.jsonwebtoken:jjwt-api/jjwt-impl/jjwt-jackson:0.12.5,lombok - Flyway 自动启用:默认指向 classpath:db/migration,本项目改为
classpath:db/migration同步指向仓库根sql/migrations/—— 由 Spring Boot 配置加载实现:在application.yml写spring.flyway.locations: filesystem:../sql/migrations(相对 backend/ 目录) -
application.yml必须用${VAR_NAME}注入 DB / JWT 凭据(来源.env.local,启动时由 maven-dotenv-plugin 或EnvironmentPostProcessor加载——TDD 选择最简单方案spring-boot-dotenv第三方 starter 或 spring-cloud-context 都不引入,改用application.yml占位 + 启动命令显式--spring.config.location或 OS 环境变量;推荐:要求开发者把.env.local中的变量 export 到 shell 后./mvnw spring-boot:run;测试场景由 surefire<environmentVariables>注入或测试自行@TestPropertySource覆盖)
注:以上"如何加载 .env.local"是 Spring Boot 项目的通用工程难题,本任务最小可行实现:测试用
@DynamicPropertySource从System.getenv()注入;启动用 OS 环境变量。生产部署后续 REQ 再优化。
API shape:
-
Application.main(String[])— 标准SpringApplication.run ApplicationContextTest#contextLoads()— Spring Test 验证 ApplicationContext 启动-
Step 1: 写失败测试
- 测试名:
ApplicationContextTest#contextLoads - 意图: 验证 Spring Boot context 能启动;Flyway 能连接到 .env.local 指定的 MySQL 并发现 V1 已 apply(含
flyway_schema_history表) - 子会话确认 FAIL(pom.xml 不存在 / Application 类不存在)
- 测试名:
-
Step 2: 实现最小代码
- 写 pom.xml(依赖如上)、Application.java、application.yml、application-test.yml、logback-spring.xml
- 在
application-test.yml中通过@DynamicPropertySource或spring.datasource.url=jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_SCHEMA}占位符让测试读 env
-
Step 3: 子会话验证 PASS
- 子会话跑
cd backend && ./mvnw -B test -Dtest=ApplicationContextTest应绿
- 子会话跑
-
Step 4: Commit
git add backend/pom.xml backend/src/main/java/com/xly/erp/Application.java backend/src/main/resources/ backend/src/test/java/com/xly/erp/ApplicationContextTest.javagit commit -m "feat(usr): bootstrap spring boot 后端骨架 REQ-USR-001"
Task 2: 通用响应包装 + 异常处理
Files:
- Create:
backend/src/main/java/com/xly/erp/common/response/Result.java - Create:
backend/src/main/java/com/xly/erp/common/response/ErrorCode.java - Create:
backend/src/main/java/com/xly/erp/common/exception/BizException.java - Create:
backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java - Create:
backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java
API shape:
Result<T> { int code; String message; T data; long timestamp; static <T> Result<T> ok(T data); static Result<?> fail(int code, String message); }-
ErrorCode— 常量类,含上方"约束常量"表中所有 code(int 字段) BizException extends RuntimeException { int code; String message; BizException(int code, String message); }-
GlobalExceptionHandler—@RestControllerAdvice,handles:-
BizException→ 按其code映射到ResponseEntity<Result>,HTTP 状态按 ErrorCode 表 -
MethodArgumentNotValidException/ConstraintViolationException→Result.fail(40001, ...),HTTP 400 -
Exception(兜底) →Result.fail(50000, "服务器内部错误"),HTTP 500,记 ERROR 日志,不回显堆栈到 message
-
-
Step 1: 写失败测试
- 测试名:
GlobalExceptionHandlerTest#bizException_returnsCodeAndHttpStatus等 3 个测试 - 意图: 用
MockMvc配合一个/_test/throw-biz测试 controller 触发BizException(42301, "..."),断言 HTTP 423 + bodycode=42301;类似覆盖BizException(40101, ...)→ 401,以及兜底RuntimeException→ 500 且 message 不含 "java." 前缀 - 子会话确认 FAIL
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): 通用响应包装 + 全局异常处理 REQ-USR-001"
Task 3: JWT 工具 + 密码编码器 Bean
Files:
- Create:
backend/src/main/java/com/xly/erp/common/security/JwtUtil.java - Create:
backend/src/main/java/com/xly/erp/common/config/PasswordEncoderConfig.java - Create:
backend/src/test/java/com/xly/erp/common/security/JwtUtilTest.java
API shape:
-
JwtUtil#issue(Map<String,Object> claims, long ttlSec) : String— 用${JWT_SECRET}(注入@Value)签发 HS256 JWT,含iat/exp/jti(=UUID) -
JwtUtil#parse(String token) : Map<String,Object>— 验签 + 解析,签名错或过期抛BizException(40101, ...) PasswordEncoderConfig#passwordEncoder() : BCryptPasswordEncoder— Spring Bean,strength=10-
Step 1: 写失败测试
- 测试名:
JwtUtilTest#issuedToken_canBeParsedBackToClaims/JwtUtilTest#tamperedToken_throwsBizException/JwtUtilTest#expiredToken_throwsBizException(用 ttl=0 模拟) - 意图: 验证签发→解析往返一致;篡改任意一字节抛 40101;过期抛 40101
- 测试名:
-
Step 2: 实现最小代码
- JWT_SECRET 来自
application-test.yml的jwt.secret: ${JWT_SECRET:test-secret-256bit-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
- JWT_SECRET 来自
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): JWT 工具 + BCrypt 编码器 REQ-USR-001"
Task 4: Entity + Mapper(sys_user / sys_company / sys_employee)
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/entity/SysUser.java - Create:
backend/src/main/java/com/xly/erp/module/usr/entity/SysCompany.java - Create:
backend/src/main/java/com/xly/erp/module/usr/entity/SysEmployee.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/SysUserMapper.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/SysCompanyMapper.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/SysEmployeeMapper.java - Create:
backend/src/test/java/com/xly/erp/module/usr/mapper/SysUserMapperTest.java - Create:
backend/src/test/resources/sql/req-usr-001-seed.sql
API shape:
-
SysUser— 含iIncrement / sUsername / sUserCode / sPasswordHash / iEmployeeId / sUserType / sLanguage / iCanEditDocument / iIsDeleted / iFailedLoginCount / tLockUntil / tLastLoginDate / sCreatedBy / sUpdatedBy / tUpdatedDate字段(命名按 docs/03,TableField 映射);@TableName("sys_user"),@TableId(value="iIncrement", type=IdType.AUTO) -
SysCompany— 含iIncrement / sCompanyCode / sCompanyName / iIsDeleted(最小字段) -
SysEmployee— 含iIncrement / sEmployeeName / iDepartmentId(最小字段) - Mapper 三个均
extends BaseMapper<T>(MyBatis-Plus);SysUserMapper额外定义一个方法:selectByUsername(String username) : SysUser(@Select 注解),用于登录查找
Seed SQL 内容(写死):
-- req-usr-001-seed.sql
DELETE FROM sys_user_permission_category;
DELETE FROM sys_user;
DELETE FROM sys_employee;
DELETE FROM sys_company;
INSERT INTO sys_company (sCompanyName, sCompanyCode, iIsDeleted) VALUES
('总部', 'HQ', 0),
('已删公司', 'DEL_CO', 1);
INSERT INTO sys_employee (sEmployeeName, sEmployeeCode, iDepartmentId)
SELECT '张三', 'E001', 1 FROM (SELECT 1) t
WHERE EXISTS (SELECT 1 FROM sys_department LIMIT 1);
-- 若 sys_department 为空,先插入一行
INSERT INTO sys_department (sDepartmentName, sDepartmentCode) VALUES ('技术部', 'TECH');
INSERT INTO sys_employee (sEmployeeName, sEmployeeCode, iDepartmentId)
SELECT '张三', 'E001', iIncrement FROM sys_department WHERE sDepartmentCode='TECH' LIMIT 1;
-- password = 'Password1!' 的 BCrypt(strength=10) 哈希(TDD 阶段实际生成填入)
INSERT INTO sys_user (sUsername, sUserCode, sPasswordHash, iEmployeeId, sUserType, sLanguage, iIsDeleted, iFailedLoginCount, sCreatedBy)
SELECT 'alice', 'U001', '<BCRYPT_HASH_OF_Password1!>', iIncrement, 'NORMAL', 'zh-CN', 0, 0, 'system'
FROM sys_employee WHERE sEmployeeCode='E001';
INSERT INTO sys_user (sUsername, sUserCode, sPasswordHash, sUserType, sLanguage, iIsDeleted, sCreatedBy)
VALUES ('bob_deleted', 'U002', '<BCRYPT_HASH_OF_Password1!>', 'NORMAL', 'zh-CN', 1, 'system');
<BCRYPT_HASH_OF_Password1!>在 Task 4 实现时通过一次性 java main 或BCryptPasswordEncoder调用生成后填入;不允许保留占位符进 commit。
-
Step 1: 写失败测试
- 测试名:
SysUserMapperTest#selectByUsername_returnsUserWithAllFields/SysUserMapperTest#selectByUsername_returnsNullWhenNotFound - 意图: seed 后查
alice→ 字段完整;查nobody→ null
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): sys_user/sys_company/sys_employee entity + mapper REQ-USR-001"
Task 5: LoginReq DTO + LoginVo + UserInfoVo
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/dto/LoginReq.java - Create:
backend/src/main/java/com/xly/erp/module/usr/vo/LoginVo.java - Create:
backend/src/main/java/com/xly/erp/module/usr/vo/UserInfoVo.java - Create:
backend/src/test/java/com/xly/erp/module/usr/dto/LoginReqValidationTest.java
API shape:
LoginReq { @NotBlank @Size(max=50) String username; @NotBlank @Size(max=128) String password; @NotBlank @Size(max=50) String companyCode; }LoginVo { String accessToken; String tokenType; long expiresInSec; UserInfoVo userInfo; }UserInfoVo { Integer userId; String username; String userType; String language; String employeeName; String companyCode; }-
Step 1: 写失败测试
- 测试名:
LoginReqValidationTest#blankUsername_fails/LoginReqValidationTest#tooLongUsername_fails/LoginReqValidationTest#allFieldsPresent_passes - 意图: 用
Validator校验 jakarta 约束注解工作正常
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): LoginReq + LoginVo + UserInfoVo REQ-USR-001"
Task 6: LoginService 接口骨架
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/service/LoginService.java - Create:
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java - Create:
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java
API shape:
-
LoginService#login(String username, String password, String companyCode) : LoginVo— 业务方法签名 -
LoginServiceImpl implements LoginService—@Service,注入SysUserMapper、SysCompanyMapper、SysEmployeeMapper、BCryptPasswordEncoder、JwtUtil -
LoginServiceImpl私有常量:MAX_FAILED_LOGIN_COUNT = 5、LOCK_DURATION_MINUTES = 30L、TOKEN_TTL_SEC = 7200L
本 task 仅产出接口 + 空实现 + 一个 baseline 测试(直接抛 UnsupportedOperationException),用于建立后续 task 的脚手架。
-
Step 1: 写失败测试
- 测试名:
LoginServiceImplTest#contextLoads(验证 LoginService Bean 注入成功;与@SpringBootTest+ seed.sql 一起工作) - 意图: Bean 装配可用
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): LoginService 接口骨架 REQ-USR-001"
Task 7: Login — 公司不存在或已删 → 40004
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java
API behavior:当 companyCode 在 sys_company 查不到(不存在 OR iIsDeleted=1)→ 抛 BizException(ErrorCode.COMPANY_NOT_FOUND, "公司不存在或已删除");不触碰 sys_user。
-
Step 1: 写失败测试
- 测试名:
LoginServiceImplTest#login_unknownCompany_throws40004/login_softDeletedCompany_throws40004 - 意图: 用 seed 中的
HQ(正常)vsNOPE(不存在)vsDEL_CO(软删)触发不同分支;断言抛BizException且code == 40004;同时断言alice的iFailedLoginCount仍为 0(未被错误计入失败)
- 测试名:
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): 登录校验公司存在性 REQ-USR-001"
Task 8: Login — 用户不存在 / 密码错 → 40101(同文案,含失败计数)
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java
API behavior:
- 用户名不存在 →
BizException(40101, "用户名或密码错误");不写 DB(无 user 行可写) 用户存在 + 密码 hash 不匹配 →
sys_user.iFailedLoginCount += 1,返BizException(40101, "用户名或密码错误")-
Step 1: 写失败测试
- 测试名:
login_unknownUser_throws40101_noDbWrite-
login_badPassword_throws40101_andIncrementsFailCount(断言一次错误后iFailedLoginCount == 1) - 意图: 防用户名枚举 + 失败计数累加
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): 登录用户名/密码错误统一返回 40101 + 累加失败计数 REQ-USR-001"
Task 9: Login — 失败 5 次锁定 → 第 6 次返 42301
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java
API behavior:
- 累计第 5 次错误密码时,在同一事务里写
tLockUntil = NOW() + 30 分钟;本次响应仍返40101(不在第 5 次直接返锁定,避免泄露阈值) - 后续请求遇到
tLockUntil IS NOT NULL AND tLockUntil > NOW()→ 直接抛BizException(ACCOUNT_LOCKED, "账号已锁定,请稍后再试"),HTTP 423;不计入失败次数 tLockUntil <= NOW()(锁定到期)→ 视为已解锁,正常进入密码校验流程(即仍允许累加失败、仍允许成功)-
Step 1: 写失败测试
- 测试名:
-
login_5thBadPassword_setsLockUntil_andStillReturns40101(断言iFailedLoginCount == 5且tLockUntil不为空 ≥ NOW()+29min) login_duringLockWindow_throws42301_noCountIncrement-
login_afterLockExpired_allowsNewAttempt(用 SQL 把 tLockUntil 改成过去时刻,再次登录正确密码 → 应成功并清零计数;属于先做小验证,可在 Task 10 完整覆盖成功路径) - 意图: 锁定语义闭环
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): 登录失败 5 次锁定 30 分钟 REQ-USR-001"
Task 10: Login — 作废账号 → 40103;成功 → 签 JWT + 清零 + 更新登录时间
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/usr/service/impl/LoginServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/usr/service/LoginServiceImplTest.java
API behavior:
-
sys_user.iIsDeleted == 1→BizException(ACCOUNT_DELETED, "账号已被作废,禁止登录"),HTTP 401;不进入密码校验,不累加失败 - 成功路径(
@Transactional):-
iFailedLoginCount = 0,tLockUntil = NULL,tLastLoginDate = NOW()(一次 UPDATE) - 加载
sys_employee.sEmployeeName(若iEmployeeId非空) - 构造 JWT claims(
sub=userId,username,userType,companyCode,language,jti=UUID),通过JwtUtil.issue(claims, TOKEN_TTL_SEC) - 返回
LoginVo
-
判定顺序(先后明确):1) 公司校验 → 2) 用户查找 → 3) 作废校验 → 4) 锁定校验 → 5) 密码校验 → 6) 成功路径。
-
Step 1: 写失败测试
- 测试名:
login_deletedUser_throws40103_noCountIncrementlogin_success_returnsTokenAndClearsFailCount_andUpdatesLastLoginlogin_success_jwtParsesBack_with_sub_username_companyCode- 意图: 成功路径与作废路径都覆盖;JWT 验签往返
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): 登录成功签发 JWT + 作废账号 40103 REQ-USR-001"
Task 11: AuthController + 端到端 MockMvc 测试
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java - Create:
backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerTest.java
API shape:
-
AuthController—@RestController @RequestMapping("/api/v1/auth") POST /api/v1/auth/login方法login(@RequestBody @Valid LoginReq req) : Result<LoginVo>,委托给LoginService-
Step 1: 写失败测试
- 测试名(
AuthControllerTest用@SpringBootTest + MockMvc): post_login_success_returns200_andLoginVopost_login_badCredentials_returns401_code40101post_login_lockedAccount_returns423_code42301post_login_deletedAccount_returns401_code40103post_login_unknownCompany_returns400_code40004post_login_blankUsername_returns400_code40001- 意图: 6 个 HTTP 路径全部覆盖 spec § 验收 1-10
- 测试名(
Step 2: 实现最小代码
Step 3: 子会话验证 PASS
-
Step 4: Commit
git commit -m "feat(usr): POST /api/v1/auth/login controller + 端到端测试 REQ-USR-001"
提交计划
按 task 顺序产生 11 个 commit:
| Task | Commit message |
|---|---|
| 1 | feat(usr): bootstrap spring boot 后端骨架 REQ-USR-001 |
| 2 | feat(usr): 通用响应包装 + 全局异常处理 REQ-USR-001 |
| 3 | feat(usr): JWT 工具 + BCrypt 编码器 REQ-USR-001 |
| 4 | feat(usr): sys_user/sys_company/sys_employee entity + mapper REQ-USR-001 |
| 5 | feat(usr): LoginReq + LoginVo + UserInfoVo REQ-USR-001 |
| 6 | feat(usr): LoginService 接口骨架 REQ-USR-001 |
| 7 | feat(usr): 登录校验公司存在性 REQ-USR-001 |
| 8 | feat(usr): 登录用户名/密码错误统一返回 40101 + 累加失败计数 REQ-USR-001 |
| 9 | feat(usr): 登录失败 5 次锁定 30 分钟 REQ-USR-001 |
| 10 | feat(usr): 登录成功签发 JWT + 作废账号 40103 REQ-USR-001 |
| 11 | feat(usr): POST /api/v1/auth/login controller + 端到端测试 REQ-USR-001 |