---
req_id: REQ-USR-004
date: 2026-04-30
spec_ref: docs/superpowers/specs/2026-04-30-REQ-USR-004.md
---

# REQ-USR-004 用户登录 Implementation Plan

> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task.

**Goal:** Phase A 实现 POST /api/usr/auth/login + 失败计数锁定 + JWT 双 token；Phase B 闭环 stub permitAll → authenticated。

---

## Schema 改动

无。

## 文件变更清单

### 新增

- backend/src/main/java/com/xly/erp/module/usr/dto/LoginDTO.java
- backend/src/main/java/com/xly/erp/module/usr/vo/LoginVO.java
- backend/src/main/java/com/xly/erp/module/usr/vo/UserBriefVO.java
- backend/src/main/java/com/xly/erp/module/usr/security/LoginAttemptStore.java
- backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java
- backend/src/main/java/com/xly/erp/common/security/JwtAuthenticationEntryPoint.java
- backend/src/test/java/com/xly/erp/module/usr/security/LoginAttemptStoreTest.java
- backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java

### 修改

- common/security/JwtUtil.java — 加 `signRefresh(userNo)` + 区分 access/refresh TTL
- common/security/SecurityConfig.java — 重写路径规则 + 注册 entryPoint
- module/usr/mapper/UserMapper.java — 加 `selectByUserName` + `updateLastLoginDate`
- module/usr/service/UserService.java + impl — 加 `login(LoginDTO)`
- module/mod/service/impl/ModuleServiceImpl.java — 移除 stub fallback
- module/usr/service/impl/UserServiceImpl.java — 移除 stub fallback（create + update）
- ModuleControllerIT (4 处) + UserControllerIT (3 处) — 改 stub 期望

---

## 任务步骤

### Phase A: 登录接口本体

#### Task 1: JwtUtil 加 signRefresh

- 加 `Duration REFRESH_TTL = Duration.ofDays(30)` + `sign(userNo, ttl)` 通用方法 + `signRefresh(userNo)` 包装；现有 `sign(userNo)` 走 `Duration.ofHours(8)`
- 单测：`signRefresh_signsWithLongerTtl`
- Commit: `feat(usr): jwtutil signRefresh REQ-USR-004`

#### Task 2: LoginAttemptStore + 单测

- 新建 `@Component LoginAttemptStore`：`recordFailure(userNo) → newCount` / `isLocked(userNo) → Optional<Long secondsRemaining>` / `clearFailures(userNo)`；常量 `MAX_ATTEMPTS=5` / `LOCK_DURATION=Duration.ofMinutes(15)`
- 单测 5 用例覆盖隔离 / 累加 / 锁定 / 自动解锁 / 清空
- Commit: `feat(usr): login attempt store + lock logic REQ-USR-004`

#### Task 3: UserMapper 加 selectByUserName + updateLastLoginDate

- `@Select("SELECT ... FROM tUser WHERE sUserName = #{name}")` `User selectByUserName(String name)`
- `@Update("UPDATE tUser SET tLastLoginDate = #{ts} WHERE iIncrement = #{id}")` `int updateLastLoginDate(Integer id, LocalDateTime ts)`
- IT 2 用例
- Commit: `feat(usr): mapper selectByUserName + updateLastLoginDate REQ-USR-004`

#### Task 4: LoginDTO + LoginVO + UserBriefVO + UserService.login + 单测

- 6 个 service 单测（spec 列表）
- Commit: `feat(usr): login service + dto/vo REQ-USR-004`

#### Task 5: AuthController + IT (5 用例)

- 新建 `AuthController @RequestMapping("/api/usr/auth")`，`@PostMapping("/login")`
- IT 5 用例覆盖 spec
- Commit: `feat(usr): auth controller + login it REQ-USR-004`

### Phase B: Stub 闭环

#### Task 6: SecurityConfig 收紧 + AuthenticationEntryPoint

- `JwtAuthenticationEntryPoint`：未认证写 JSON `Result.fail(20001, "未认证")` + status 200
- SecurityConfig：
  - 移除 `/api/mod/**` + `/api/usr/**` permitAll
  - 加 `/api/usr/auth/login` permitAll
  - `anyRequest().authenticated()`
  - `exceptionHandling(eh -> eh.authenticationEntryPoint(authEntryPoint))`
- 移除 `// REQ-MOD-001 stub: see USR-004 follow-up` 注释
- Commit: `refactor(usr): tighten security to authenticated REQ-USR-004`

#### Task 7: ModuleServiceImpl + UserServiceImpl 移除 stub fallback

- `ModuleServiceImpl#create`：`sCreatedBy = SecurityContextHelper.currentUserNo()`，去掉 `?: stub.getStubUserNo()`；同 `UserServiceImpl#create` + `update`
- 不破坏 `StubSecurityProperties` bean 本身（仍可保留以防其他用途；本 REQ 仅停止 fallback 引用）
- 单测调整：MOD-001 `createWithValidDto_persistsWithStandardCols` 期望 sCreatedBy 不再是 STUB_ADMIN（而要在测试里手动 SecurityContextHolder.set principal）；其他类似
- Commit: `refactor(usr): remove stub fallback in services REQ-USR-004`

#### Task 8: 修改现有 stub IT 期望

- ModuleControllerIT 4 条：`postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN` / `putWithoutJwt_*` / `deleteWithoutJwt_*` / `getWithoutJwt_*` → 改期望 `code=20001`，DB 无新增行
- UserControllerIT 3 条：`postWithoutJwt_*` / `putWithoutJwt_*` / `getWithoutJwt_*` → 改期望 `code=20001`
- 全量回归绿
- Commit: `test(usr): update stub regression to authenticated REQ-USR-004`

## 提交计划

8 commits：6 feat + 2 refactor + 1 test（或合并为更少 commit 视进度）。
