2026-05-15-REQ-USR-001.md 24.5 KB

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-tdd executes 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 常量类;GlobalExceptionHandlerBizExceptionResult.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(BCryptPasswordEncoder Bean)

业务层(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 = 5LOCK_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.ymlspring.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 项目的通用工程难题,本任务最小可行实现:测试用 @DynamicPropertySourceSystem.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 中通过 @DynamicPropertySourcespring.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.java
    • git 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 / ConstraintViolationExceptionResult.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 + body code=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.ymljwt.secret: ${JWT_SECRET:test-secret-256bit-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
  • 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,注入 SysUserMapperSysCompanyMapperSysEmployeeMapperBCryptPasswordEncoderJwtUtil
  • LoginServiceImpl 私有常量:MAX_FAILED_LOGIN_COUNT = 5LOCK_DURATION_MINUTES = 30LTOKEN_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:当 companyCodesys_company 查不到(不存在 OR iIsDeleted=1)→ 抛 BizException(ErrorCode.COMPANY_NOT_FOUND, "公司不存在或已删除")触碰 sys_user。

  • Step 1: 写失败测试

    • 测试名: LoginServiceImplTest#login_unknownCompany_throws40004 / login_softDeletedCompany_throws40004
    • 意图: 用 seed 中的 HQ(正常)vs NOPE(不存在)vs DEL_CO(软删)触发不同分支;断言抛 BizExceptioncode == 40004;同时断言 aliceiFailedLoginCount 仍为 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 == 5tLockUntil 不为空 ≥ 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 == 1BizException(ACCOUNT_DELETED, "账号已被作废,禁止登录"),HTTP 401;进入密码校验,累加失败
  • 成功路径(@Transactional):
    1. iFailedLoginCount = 0tLockUntil = NULLtLastLoginDate = NOW()(一次 UPDATE)
    2. 加载 sys_employee.sEmployeeName(若 iEmployeeId 非空)
    3. 构造 JWT claims(sub=userId, username, userType, companyCode, language, jti=UUID),通过 JwtUtil.issue(claims, TOKEN_TTL_SEC)
    4. 返回 LoginVo

判定顺序(先后明确):1) 公司校验 → 2) 用户查找 → 3) 作废校验 → 4) 锁定校验 → 5) 密码校验 → 6) 成功路径。

  • Step 1: 写失败测试

    • 测试名:
    • login_deletedUser_throws40103_noCountIncrement
    • login_success_returnsTokenAndClearsFailCount_andUpdatesLastLogin
    • login_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_andLoginVo
    • post_login_badCredentials_returns401_code40101
    • post_login_lockedAccount_returns423_code42301
    • post_login_deletedAccount_returns401_code40103
    • post_login_unknownCompany_returns400_code40004
    • post_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