Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 273064c10d | |||
| 18746ce6fc | |||
| b747e09af8 |
@@ -0,0 +1,5 @@
|
||||
# API 契约
|
||||
|
||||
第一迭代认证域的正式契约见 [openapi.yaml](openapi.yaml)(OpenAPI 3):注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表(40000/40100/40101/40102/40900/40901/42300),以及会话轮换与登录锁定策略说明。
|
||||
|
||||
约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义。
|
||||
@@ -0,0 +1,411 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Patbond API — Auth & Me(第一批公开接口)
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差)。
|
||||
|
||||
## 通用约定(development-plan 第 6 节)
|
||||
- 公开接口统一前缀 `/api/v1`;JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。
|
||||
- 所有时间字段为 ISO 8601 且带时区偏移(如 `2026-09-04T04:05:06.789Z`)。
|
||||
- 统一响应信封 `{"code": 0, "message": "success", "data": …}`;错误同时携带正确的
|
||||
HTTP 状态码与稳定业务码,业务码永不复用或改号。
|
||||
- `/internal/**` 为服务间接口,不属于本公开契约,需 `X-Internal-Token` 服务凭证,
|
||||
未携带或错误一律 401。
|
||||
|
||||
## 错误码表
|
||||
| 业务码 | HTTP | 场景 |
|
||||
| --- | --- | --- |
|
||||
| 0 | 200 | 成功 |
|
||||
| 40000 | 400 | 参数校验失败(含 JSON 不可解析;message 为首个字段错误) |
|
||||
| 40100 | 401 | 用户名或密码错误 |
|
||||
| 40101 | 401 | access token 无效或过期(缺失、伪造、篡改、过期) |
|
||||
| 40102 | 401 | refresh token 已失效或被重用(未知、过期、已轮换、已退出、家族已撤销) |
|
||||
| 40400 | 404 | 资源不存在 |
|
||||
| 40900 | 409 | 用户名已存在(大小写不敏感) |
|
||||
| 40901 | 409 | 手机号已被使用 |
|
||||
| 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) |
|
||||
| 50000 | 500 | 服务器内部错误 |
|
||||
| 50300 | 503 | 依赖服务暂不可用 |
|
||||
|
||||
## 会话模型(ADR-003,数值均为服务端配置项)
|
||||
- access token:JWT(RS256),有效期 15 分钟;由资源服务用公钥本地验签。
|
||||
- refresh token:不透明随机串,有效期 30 天;**每次刷新即轮换**,旧值立即失效。
|
||||
- 已轮换/已失效的 refresh token 再次被使用时,判定为重用,**整个 token family
|
||||
(该登录会话链)全部撤销**,持有者需重新登录。
|
||||
- 允许多设备并行会话;退出仅撤销当前会话(由所提交的 refreshToken 标识),
|
||||
其他设备不受影响。已签发的 access token 在剩余有效期内仍可用。
|
||||
- 登录失败限制:同一账号在 15 分钟窗口内密码错误累计 5 次(配置项),账号锁定
|
||||
15 分钟;锁定期间即使密码正确也返回 423/42300;一次成功登录重置计数窗口。
|
||||
|
||||
servers:
|
||||
- url: http://127.0.0.1:8081
|
||||
description: patbond-auth(本地开发,/api/v1/auth/**)
|
||||
- url: http://127.0.0.1:8082
|
||||
description: patbond-user(本地开发,/api/v1/me)
|
||||
|
||||
tags:
|
||||
- name: auth
|
||||
description: 注册 / 登录 / 刷新 / 退出(patbond-auth)
|
||||
- name: user
|
||||
description: 当前用户(patbond-user)
|
||||
|
||||
paths:
|
||||
/api/v1/auth/register:
|
||||
post:
|
||||
tags: [auth]
|
||||
summary: 注册并创建会话
|
||||
operationId: register
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 注册成功,返回令牌对
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthTokenEnvelope'
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'409':
|
||||
description: 用户名或手机号已被占用(code 40900 / 40901)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
usernameTaken:
|
||||
value: { code: 40900, message: 用户名已存在, data: null }
|
||||
phoneTaken:
|
||||
value: { code: 40901, message: 手机号已被使用, data: null }
|
||||
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
tags: [auth]
|
||||
summary: 登录并创建会话
|
||||
description: 多设备并行:每次登录开启独立会话(独立 token family),互不影响。
|
||||
operationId: login
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 登录成功,返回令牌对
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthTokenEnvelope'
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'401':
|
||||
description: 用户名或密码错误(code 40100)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
invalidCredentials:
|
||||
value: { code: 40100, message: 用户名或密码错误, data: null }
|
||||
'423':
|
||||
description: 登录失败次数过多,账号临时锁定(code 42300)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
locked:
|
||||
value: { code: 42300, message: 登录失败次数过多,账号已临时锁定, data: null }
|
||||
|
||||
/api/v1/auth/refresh:
|
||||
post:
|
||||
tags: [auth]
|
||||
summary: 轮换 refresh token
|
||||
description: |
|
||||
成功时返回全新令牌对,旧 refreshToken 立即失效(轮换)。提交已轮换或已失效的
|
||||
refreshToken 返回 401/40102,且视为重用攻击:该 token family 的全部会话被撤销。
|
||||
operationId: refresh
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RefreshRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 轮换成功,返回新的令牌对
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthTokenEnvelope'
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'401':
|
||||
description: refresh token 已失效或被重用(code 40102)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
invalidated:
|
||||
value: { code: 40102, message: refresh token 已失效或被重用, data: null }
|
||||
|
||||
/api/v1/auth/logout:
|
||||
post:
|
||||
tags: [auth]
|
||||
summary: 退出(撤销当前会话)
|
||||
description: |
|
||||
撤销 body 中 refreshToken 对应的会话;其他设备的会话不受影响(ADR-003)。
|
||||
需携带有效的 access token(从中取用户身份,防止跨账号撤销)。幂等:对已
|
||||
失效的 refreshToken 仍返回成功。
|
||||
operationId: logout
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LogoutRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 已退出
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VoidEnvelope'
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
'401':
|
||||
$ref: '#/components/responses/AccessTokenInvalid'
|
||||
|
||||
/api/v1/me:
|
||||
get:
|
||||
tags: [user]
|
||||
summary: 当前用户资料
|
||||
description: 由 patbond-user 提供;access token 以 RS256 公钥本地验签,无需经过 auth 服务。
|
||||
operationId: me
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 当前用户
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MeEnvelope'
|
||||
'401':
|
||||
$ref: '#/components/responses/AccessTokenInvalid'
|
||||
'404':
|
||||
description: 用户不存在(如已注销;code 40400)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: 'Authorization: Bearer <accessToken>(RS256 JWT)'
|
||||
|
||||
responses:
|
||||
ValidationError:
|
||||
description: 参数校验失败(code 40000)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
validation:
|
||||
value: { code: 40000, message: 参数校验失败, data: null }
|
||||
AccessTokenInvalid:
|
||||
description: access token 缺失、无效或过期(code 40101)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorEnvelope'
|
||||
examples:
|
||||
tokenInvalid:
|
||||
value: { code: 40101, message: token 无效或过期, data: null }
|
||||
|
||||
schemas:
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 32
|
||||
description: 用户名,大小写不敏感唯一
|
||||
example: demo_user
|
||||
phone:
|
||||
type: string
|
||||
pattern: '^\+[1-9][0-9]{7,14}$'
|
||||
description: 手机号,E.164 格式;可选,唯一
|
||||
example: '+8613800138000'
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
minLength: 6
|
||||
maxLength: 64
|
||||
example: secret123
|
||||
nickname:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 32
|
||||
description: 昵称;可选(冻结稿之外的可选扩展字段,前端可忽略)
|
||||
example: 小柴
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
example: demo_user
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
example: secret123
|
||||
|
||||
RefreshRequest:
|
||||
type: object
|
||||
required: [refreshToken]
|
||||
properties:
|
||||
refreshToken:
|
||||
type: string
|
||||
description: 当前持有的 refresh token(不透明随机串)
|
||||
example: Zx3v…43位base64url…Qk
|
||||
|
||||
LogoutRequest:
|
||||
type: object
|
||||
required: [refreshToken]
|
||||
properties:
|
||||
refreshToken:
|
||||
type: string
|
||||
description: 要撤销的当前会话的 refresh token
|
||||
example: Zx3v…43位base64url…Qk
|
||||
|
||||
AuthTokens:
|
||||
type: object
|
||||
description: 注册 / 登录 / 刷新共用的令牌对(冻结契约,恰好这 6 个字段)
|
||||
required:
|
||||
- userId
|
||||
- tokenType
|
||||
- accessToken
|
||||
- accessTokenExpiresAt
|
||||
- refreshToken
|
||||
- refreshTokenExpiresAt
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 019212aa-0000-7000-8000-000000000001
|
||||
tokenType:
|
||||
type: string
|
||||
enum: [Bearer]
|
||||
example: Bearer
|
||||
accessToken:
|
||||
type: string
|
||||
description: RS256 JWT,有效期 15 分钟(配置项)
|
||||
example: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI…
|
||||
accessTokenExpiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 带时区
|
||||
example: '2026-09-04T04:20:06.789Z'
|
||||
refreshToken:
|
||||
type: string
|
||||
description: 不透明随机串,有效期 30 天(配置项),每次刷新轮换
|
||||
example: Zx3v…43位base64url…Qk
|
||||
refreshTokenExpiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
example: '2026-10-04T04:05:06.789Z'
|
||||
|
||||
Me:
|
||||
type: object
|
||||
description: 当前用户资料(冻结契约,恰好这 4 个字段)
|
||||
required: [userId, username, createdAt]
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 019212aa-0000-7000-8000-000000000001
|
||||
username:
|
||||
type: string
|
||||
example: demo_user
|
||||
phone:
|
||||
type: string
|
||||
nullable: true
|
||||
description: E.164;未绑定时为 null
|
||||
example: '+8613800138000'
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
example: '2026-09-04T04:05:06.789Z'
|
||||
|
||||
AuthTokenEnvelope:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
enum: [0]
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthTokens'
|
||||
|
||||
MeEnvelope:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
enum: [0]
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
$ref: '#/components/schemas/Me'
|
||||
|
||||
VoidEnvelope:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
enum: [0]
|
||||
message:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
nullable: true
|
||||
example: null
|
||||
|
||||
ErrorEnvelope:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 稳定业务错误码(见顶部错误码表)
|
||||
example: 40101
|
||||
message:
|
||||
type: string
|
||||
example: token 无效或过期
|
||||
data:
|
||||
nullable: true
|
||||
example: null
|
||||
@@ -66,8 +66,35 @@
|
||||
|
||||
**决策**(2026-09-04):
|
||||
|
||||
- 自动化测试中的数据库一律通过 Testcontainers 使用 Docker 临时容器(`postgres:16`),不依赖开发者本机数据库;每次测试在干净实例上执行全量 Flyway 迁移。
|
||||
- 自动化测试中的数据库一律通过 Testcontainers 使用 Docker 临时容器(`postgres:18`,版本基线见 ADR-008),不依赖开发者本机数据库;每次测试在干净实例上执行全量 Flyway 迁移。
|
||||
- 最终交付将提供 Docker 镜像/编排包(对应开发计划 M6 的容器镜像项)。
|
||||
- 开发者本人手动测试/联调时可使用自己本机的 PostgreSQL,连接信息经环境变量注入,不入库。
|
||||
|
||||
**理由**:测试可复现、与目标版本(PostgreSQL 16)对齐、不受本机数据库账号/版本差异影响;交付形态与测试基础设施统一到 Docker。
|
||||
|
||||
## ADR-007 部署形态:容器化应用 + 分阶段数据库策略
|
||||
|
||||
**决策**(2026-09-04):
|
||||
|
||||
- 应用(auth、user 及后续模块)以 Docker 容器交付部署,**容器保持无状态**:文件走对象存储、会话在数据库,任何状态不落容器本地。
|
||||
- **MVP 阶段**:数据库使用 Docker 容器运行 PostgreSQL(数据挂载 volume 持久化),与应用同机以 docker compose 编排;配套每日 `pg_dump` 备份到独立存储,并演练过恢复流程。
|
||||
- **规模化阶段**:当用户量与数据重要性上升后,数据库迁移至云托管数据库(RDS 类)或独立数据库服务器,应用容器不动。
|
||||
- 数据库连接信息始终经环境变量注入(`PATBOND_DB_URL` 等),保证迁移数据库时应用层零改动。
|
||||
|
||||
**理由**:双人团队运维预算有限,容器数据库 + volume + 备份在 MVP 阶段完全够用,且与 Testcontainers 测试、Docker 交付包(ADR-006)同一体系;把高可用、故障转移等重运维在需要时外包给云托管,是成本与可靠性的最优路径。守住「应用无状态」这一条纪律,数据库放哪都可随时更换。
|
||||
|
||||
**注意**:数据库大版本升级即使在 Docker 中也需要数据迁移(`pg_upgrade` 或 dump/restore),属 ADR 级决策,不随镜像标签随意变更;小版本安全更新随镜像自动跟进。
|
||||
|
||||
## ADR-008 PostgreSQL 版本基线定为 18
|
||||
|
||||
**决策**(2026-09-04):数据库版本基线从开发计划最初的「PostgreSQL 16+」明确定为 **PostgreSQL 18**。Testcontainers 测试镜像、未来的 compose 编排与交付镜像统一锁 `postgres:18`。
|
||||
|
||||
**理由**:
|
||||
|
||||
- 项目尚无生产数据,大版本选择处于零成本窗口;一旦有数据,大版本升级即为一次真实迁移(见 ADR-007 注意事项)。
|
||||
- PostgreSQL 18 已是发布满一年的稳定版本,与团队本机开发库(18.6)一致,消除本机与基线的版本偏差。
|
||||
- 不违背开发计划「16+」的原始约束。
|
||||
|
||||
**验证**:切换当日 `./mvnw clean test` 全量 37 测试在 postgres:18(18.6)容器上通过,Flyway V1 baseline 迁移执行无兼容问题。
|
||||
|
||||
**影响**:后续大版本变更须以新 ADR 决策并附全量测试验证;数据库特性使用以 18 为可用上限参考。
|
||||
|
||||
@@ -116,7 +116,7 @@ Page/Widget -> Feature Controller -> Repository -> API Client
|
||||
|
||||
- JDK 17。不要使用更高版本 JDK 代替团队基线进行发布构建。
|
||||
- Maven 3.9+;M0 完成后改用仓库内 Maven Wrapper。
|
||||
- PostgreSQL 16+,现有本地库作为开发数据源。
|
||||
- PostgreSQL 18(版本基线见 ADR-008),现有本地库作为开发数据源。
|
||||
- Nacos,供当前 `patbond-auth` 发现 `patbond-user`。
|
||||
- Flutter/Dart 版本需满足 `patbond-flutter/pubspec.yaml`,M0 完成后通过版本管理工具锁定。
|
||||
|
||||
@@ -199,7 +199,7 @@ NACOS_SERVER_ADDR=127.0.0.1:8848
|
||||
|
||||
目标:让所有开发者能用一致方式启动、测试和联调。
|
||||
|
||||
- 确认 Java 17、Flutter SDK、PostgreSQL 16 的固定版本。
|
||||
- 确认 Java 17、Flutter SDK、PostgreSQL 18 的固定版本。
|
||||
- 增加 Maven Wrapper 和 Flutter 版本固定方案。
|
||||
- 提供可提交的 `application.yml` 默认配置,敏感值全部由环境变量注入。
|
||||
- 增加本地基础设施编排:PostgreSQL、Nacos;RabbitMQ 在异步任务阶段启用。
|
||||
@@ -322,7 +322,7 @@ flutter test
|
||||
mkdocs build --strict
|
||||
```
|
||||
|
||||
数据库迁移还需在全新 PostgreSQL 16 实例执行一次,并对升级路径执行一次。
|
||||
数据库迁移还需在全新 PostgreSQL 18 实例执行一次,并对升级路径执行一次。
|
||||
|
||||
### 可观测性与产品验证
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# 16 后端认证报告:JWT + refresh 会话 + /internal 鉴权 + OpenAPI(第一迭代·第三波)
|
||||
|
||||
- 执行人:Senior Developer
|
||||
- 日期:2026-09-04
|
||||
- 仓库:`patbond-api`(dev 分支,已提交);`patbond-doc` 仅新增 `docs/api/openapi.yaml` 与本报告(均不提交,由主会话收口)
|
||||
- 范围:T4(JWT RS256 access token、refresh 会话轮换与 family 撤销、`/api/v1` 前缀迁移、`/internal` 服务间鉴权、登录失败限制、expiresAt 时区修复)+ T6a(OpenAPI 3 正式契约)
|
||||
- 门禁:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` → **BUILD SUCCESS,73 测试 0 失败**(上一波 37 → 73),Testcontainers postgres:18,无遗留容器/进程
|
||||
|
||||
---
|
||||
|
||||
## 1. 采纳的架构(对齐 02 技术评估 §3 任务 4)
|
||||
|
||||
**会话逻辑全部下沉 `patbond-user`**(identity schema 唯一所有者),**`patbond-auth` 为薄入口**:校验参数、编排内部调用、签发 RS256 JWT。
|
||||
|
||||
- `identity.auth_sessions` 的读写只发生在 patbond-user(`session/SessionRepository`、`SessionService`、`/internal/sessions` 三个内部端点)。
|
||||
- jti 由 user 在建会话时铸造并落 `access_token_jti`,随响应带回给 auth 嵌入 JWT——一次内部调用完成建会话+对账,无需回写。
|
||||
- **`/api/v1/me` 由 patbond-user 直接验签**(RS256 公钥本地验证,`security/JwtVerifier` + `BearerAuthFilter`),请求不经过 auth。这正是选 RS256 而非 HS256 的理由:M2 起 pet/community 等资源服务同样只拿公钥即可本地验签,共享密钥不扩散。
|
||||
- auth 侧仅 logout 需要验签(取 sub 作为 userId,防跨账号撤销),用私钥推导出的公钥完成,auth 只需配置一个私钥。
|
||||
|
||||
## 2. 公开契约(冻结稿 → 实现,字段零偏差)
|
||||
|
||||
- 5 个端点:`POST /api/v1/auth/{register,login,refresh,logout}` + `GET /api/v1/me`,与冻结稿逐字段一致;`AuthTokenResponse` 恰好 6 个字段 `{userId, tokenType, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt}`,`/me` 恰好 `{userId, username, phone, createdAt}`。测试显式断言"多余字段不存在"(旧的 username/nickname/expiresAt 已从响应移除)。
|
||||
- **旧路径 `/auth/register`、`/auth/login` 直接删除,不做兼容保留**。理由:尚无任何已发布客户端,Flutter 端正按 `/api/v1` 冻结稿并行开发,保留旧路径只会产生第二套需要测试和废弃的入口。
|
||||
- 遗留修复:`expiresAt`(无时区 `LocalDateTime`)随响应重构消亡,两个时间字段均为 `OffsetDateTime`,序列化为 ISO 8601 带偏移(报告 10 §5.2 关闭)。
|
||||
- 契约之外的说明(已在 openapi.yaml 标注):register 仍接受**可选** `nickname`(上一波已有能力,字段名无冲突,前端可忽略);错误码新增 **42300(HTTP 423,登录锁定)**——工单第 6 项要求把锁定行为写进契约,冻结稿错误码表没有为它留码,属必要新增,见 §5。
|
||||
|
||||
## 3. Token 与会话实现(ADR-003,全部可配置)
|
||||
|
||||
### Access token(patbond-auth `security/JwtSigner`)
|
||||
- RS256(jjwt 0.12.6),claims:`sub`=userId、`jti`(=auth_sessions.access_token_jti)、`sid`=sessionId、`iss`/`iat`/`exp`;有效期 `patbond.jwt.access-ttl` 默认 **15m**。
|
||||
- 私钥经 `PATBOND_JWT_PRIVATE_KEY` 注入(PEM 文件路径或内联 PEM 皆可),未配置**启动即失败**;公钥同理注入 user(`PATBOND_JWT_PUBLIC_KEY`)。sample 与 README 给出 openssl 生成命令;仓库内无任何密钥材料(测试密钥每次运行时生成,经 `@DynamicPropertySource` 注入)。
|
||||
|
||||
### Refresh 会话(patbond-user `session/*`,表结构照 V1 实现)
|
||||
- 256-bit `SecureRandom` → base64url 不透明串;库中只存 **SHA-256 摘要**(满足 `ck_sessions_refresh_hash` 32 字节约束),测试逐字节比对摘要且断言明文不落库。TTL `patbond.session.refresh-ttl` 默认 **30d**。
|
||||
- **刷新即轮换**:同事务内插入新会话行 + 关闭旧行(`revoked_at`/`rotated_at`/`replaced_by_session_id` 链到新行,reason=`rotated`),新行沿用同一 `token_family_id`。关闭旧行的 UPDATE 带 `revoked_at IS NULL` 守卫,并发轮换同一 token 时只有一个成功,失败方按重用处理。
|
||||
- **重用检测**:已轮换/已撤销的 refresh token 再次出现 → 撤销该 family 全部存活会话(reason=`reuse_detected`,WARN 日志只记 family/user id,不记 token)→ 40102。过期、未知 token 同样 40102。
|
||||
- **退出**:按(verified userId + refresh 摘要)撤销单个会话(reason=`logout`),幂等;userId 取自验签后的 access token,他人 refresh token 撤销不掉(有专门测试)。多设备并行不互踢(有专门测试)。
|
||||
|
||||
### 登录失败限制(patbond-user,DB 落地)
|
||||
- 简化为**按用户名**计数(而非工单示例的"用户名+IP"):计数器在 `identity.user_credentials`(`failed_login_count`/`failure_window_started_at`/`locked_until`),单条原子 UPDATE 完成窗口重置/累加/触锁判定,多实例与重启安全——这是 IP 维度所不具备的(IP 需额外存储且 MVP 无反向代理拓扑,`X-Forwarded-For` 不可信)。策略:**15 分钟窗口内失败 5 次 → 锁 15 分钟**(三值均为配置项);锁定期间密码正确也返回 **423/42300**;成功登录重置计数并刷 `users.last_login_at`。行为已写入 openapi.yaml 顶部说明。
|
||||
|
||||
## 4. /internal 服务间鉴权
|
||||
|
||||
- `patbond-user` 侧 `InternalAuthFilter`(OncePerRequestFilter,注册于 `/internal/*`):校验 `X-Internal-Token` 与 `patbond.internal-token`(`PATBOND_INTERNAL_TOKEN` 注入;比较用 `MessageDigest.isEqual` 常数时间);缺失/错误/服务端未配置一律 **401**(信封 code 40101,语义"服务间凭证缺失或无效"——内部接口不在公开错误码表内,复用 401 族最贴切)。未配置时 fail-closed 并记 ERROR。
|
||||
- `patbond-auth` 侧 Feign `RequestInterceptor` 自动附头;本地开发两端默认值一致(`dev-only-internal-token`,sample 注明生产必须注入强随机值)。
|
||||
- `/api/v1/*` 由 `BearerAuthFilter` 保护(40101),`/internal/*` 由 InternalAuthFilter 保护,无 spring-security 依赖。
|
||||
|
||||
## 5. 相对冻结稿的偏差清单
|
||||
|
||||
**字段名/端点/信封:零偏差。** 两项显著标注的增补:
|
||||
|
||||
1. **★ 新增错误码 42300(HTTP 423)**:登录锁定。工单第 6 项要求锁定行为进契约,冻结稿错误码表无对应码;40100 会误导客户端提示"密码错误"。前端需增加一个分支(可先按通用错误提示处理)。
|
||||
2. **★ register 的可选 `nickname` 字段保留并写入 OpenAPI**:上一波已实现的能力,删除反而破坏既有内部契约;对只发送冻结稿三字段的客户端完全透明。
|
||||
|
||||
另:锁定策略按用户名而非"用户名+IP"(工单示例措辞为"如",视为允许的简化,理由见 §3)。
|
||||
|
||||
## 6. 本波挖出并修复的两个存量缺陷(E2E 的直接产出)
|
||||
|
||||
跨服务 E2E(`AuthE2eIntegrationTest`:同 JVM 启动真实 user 服务 + Testcontainers postgres:18,全程真实 HTTP)首次跑通了 auth→user 的真实失败链路,立刻暴露上一波"错误码不折叠"修复(审计 M1)**在真实调用中从未生效**——当时所有 auth 测试都 mock 了 UserClient:
|
||||
|
||||
1. **ErrorDecoder 注册位置错误**:`ApiErrorDecoder` 原以普通 `@Bean` 放在应用上下文,而 Feign 子上下文自带 `@ConditionalOnMissingBean` 的默认 ErrorDecoder(条件只看子上下文),父上下文的 bean 被遮蔽。修复:移入 `FeignInternalConfig` 并经 `@EnableFeignClients(defaultConfiguration=…)` 注册进每个 Feign 子上下文(该类刻意不加 `@Configuration`,注释已说明原因)。
|
||||
2. **JDK HttpURLConnection 读不到 401 错误体**:Feign 默认传输层对流式 POST 收到 401 时 `getErrorStream()` 为 null,错误信封不可读,一律折叠 503/50300。修复:auth 引入 `feign-hc5`(Apache HttpClient 5,版本随 Spring Cloud BOM),OpenFeign 自动启用。
|
||||
3. 顺带:ApiErrorDecoder 解析失败不再静默吞异常,改记一条不含响应体的 WARN(响应体可能回显请求数据,不落日志)。
|
||||
|
||||
修复后 40100/40102/40900/42300 均端到端原样透传(E2E 断言)。
|
||||
|
||||
## 7. 测试与验收执行记录
|
||||
|
||||
`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`:**73 测试,0 失败 0 错误**,BUILD SUCCESS(common 3 / user 40 / auth 30)。新增 36 个,对照工单第 8 项:
|
||||
|
||||
| 验收点 | 覆盖测试 |
|
||||
| --- | --- |
|
||||
| 注册→登录→me→刷新→旧 refresh 重用被拒且 family 撤销→退出后 refresh 失效 | `AuthE2eIntegrationTest.fullAuthVerticalFlow`(真实 HTTP 全链路)+ `SessionLifecycleIntegrationTest` 7 例(含轮换链 DB 断言、摘要比对、family 撤销后存活会话数=0) |
|
||||
| access 过期/伪造 → 40101 | E2E `expiredAndForgedAccessTokensAnswer40101` + `MeEndpointTest` 5 例(缺失/过期/伪造/垃圾 token)+ `JwtSignerTest` 5 例(过期/异钥/篡改/fail-fast) |
|
||||
| /internal 无密钥 → 401 | E2E `internalEndpointsRejectCallsWithoutTheServiceCredential` + `InternalAuthFilterTest` 3 例(缺失/错误/sessions 端点) |
|
||||
| 登录失败限制生效 | E2E `repeatedLoginFailuresLockTheAccount` + `LoginLockoutIntegrationTest` 3 例(锁定、成功重置窗口、锁过期恢复) |
|
||||
| 多设备并行/退出仅当前会话 | E2E `logoutOnOneDeviceKeepsOtherDevicesLoggedIn` + `SessionLifecycleIntegrationTest`(含"他人 userId 撤销不掉"用例) |
|
||||
| 冻结契约形状(字段恰好、ISO 8601 带偏移、JWT 格式) | `AuthControllerTest` 16 例(含 40102/42300 透传、logout 三种失败)+ E2E 时间断言 |
|
||||
|
||||
既有 37 个测试全部保留并通过(UserControllerTest 仅补服务凭证头)。日志红线复核:全部新增日志语句不含密码、token(含摘要)与手机号全文。`docker ps` 无遗留容器,无遗留后台进程。
|
||||
|
||||
## 8. 配置项汇总(新增)
|
||||
|
||||
| 环境变量 | 默认 | 服务 |
|
||||
| --- | --- | --- |
|
||||
| `PATBOND_INTERNAL_TOKEN` | dev-only-internal-token | 两端(生产必须注入强随机值) |
|
||||
| `PATBOND_JWT_PRIVATE_KEY` | 无(必填,fail-fast) | auth(PEM 路径或内联) |
|
||||
| `PATBOND_JWT_PUBLIC_KEY` | 无 | user(PEM 路径或内联;未配置时 /api/v1/** 返回 500 并记 ERROR) |
|
||||
| `PATBOND_ACCESS_TTL` / `PATBOND_REFRESH_TTL` | 15m / 30d | auth / user(ADR-003) |
|
||||
| `PATBOND_LOGIN_LOCK_MAX_FAILURES` / `_WINDOW` / `_DURATION` | 5 / 15m / 15m | user |
|
||||
|
||||
README 已更新(密钥生成步骤、环境变量表、新端点、postgres 16→18 文案对齐 ADR-008)。
|
||||
|
||||
## 9. 遗留问题
|
||||
|
||||
1. **access token 无主动吊销**:退出/family 撤销只影响 refresh,已签发 access 在剩余 ≤15 分钟内仍有效(行业常规,jti/sid 已入库,将来可加黑名单)。已在 openapi.yaml 说明。
|
||||
2. **`/internal` 为静态共享密钥**:02 评估建议的最小方案;换 mTLS 或 token exchange 留待后续 ADR。
|
||||
3. **auth_sessions 无清理任务**:过期/撤销行会累积,需要后续加定期清理(表已有 `ix_auth_sessions_active_expiry` 部分索引支撑)。
|
||||
4. **user 服务公钥未配置时不 fail-fast**(为测试上下文启动便利,/api/v1 请求时 500+ERROR 日志);若希望与 auth 一致改为启动即失败,是一行改动。
|
||||
5. **登录锁定不含 IP 维度**(§3 理由);埋点列 `last_failed_at` 已在写。
|
||||
6. **jjwt 0.12.6 / feign-hc5 版本**:jjwt 不在 Boot BOM 内、两模块各自 pin 同一版本;feign-hc5 随 Spring Cloud BOM。
|
||||
7. `GET /internal/users/by-username/{username}` 目前无调用方(上一波遗留),保留未动。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 17 · Flutter 登录纵切实现报告
|
||||
|
||||
> 作者:Frontend Developer
|
||||
> 日期:2026-09-04
|
||||
> 依据:12-ui-design-qa-and-assembly.md(组装稿)、ADR-003/ADR-004、开发计划 §4.2、接口契约冻结稿
|
||||
> 提交:`patbond-flutter` dev 分支 `8d890c0`(门禁全绿后提交,未 push)
|
||||
|
||||
---
|
||||
|
||||
## 1. 交付总览
|
||||
|
||||
登录纵切完整落地:网络层(dio)+ 认证会话(安全存储)+ Splash / 登录 / 注册三页 + 主壳真实退出登录,另完成 FIX-1 / FIX-2 / m2 三项顺带修复。门禁三连全绿:`dart format --output=none --set-exit-if-changed lib test`(0 changed)、`flutter analyze`(No issues)、`flutter test`(**30 passed**,其中新增 23 个)。
|
||||
|
||||
**契约偏差:零**。所有路径、请求/响应字段名、错误码与冻结稿逐字一致。额外附带两个契约外请求头(服务端可忽略):注册请求带 `Idempotency-Key`(每次提交生成 UUID,token 刷新后的自动重放沿用同一个键),所有请求带 `X-Device-Id`(首启生成、安全存储持久化的设备 UUID)。
|
||||
|
||||
## 2. 分层与文件
|
||||
|
||||
按开发计划 §4.2 的 Page → Repository → API Client 分层(登录表单状态照组装稿放页面 state,不引入独立 Controller 层):
|
||||
|
||||
| 层 | 文件 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| 网络 | `lib/core/network/api_client.dart` | dio 封装;base URL 经 `--dart-define=PATBOND_API_BASE_URL` 注入(默认 `http://127.0.0.1:8081`);`validateStatus` 全放行,错误信封统一解析;`AuthInterceptor` 附加 Bearer;鉴权请求遇 HTTP 401 / code 40101 → 单飞刷新后重放一次,重放仍失败清会话抛 `SessionExpiredException` |
|
||||
| 网络 | `lib/core/network/token_refresher.dart` | 单飞(single-flight)刷新:并发 401 只发一次 `POST /auth/refresh`;**仅 40102 / HTTP 401 清会话**,网络失败与 5xx 一律保留 token |
|
||||
| 网络 | `lib/core/network/api_exception.dart`、`api_envelope.dart` | 类型化异常(`ApiNetworkException` / `ApiBusinessException` / `ApiRateLimitException` / `SessionExpiredException`)+ 错误码常量 + 信封解析 |
|
||||
| 认证 | `lib/features/auth/session_manager.dart` | token 内存副本 + `flutter_secure_storage` 持久化(`TokenStore` 抽象,测试注入内存实现);认证状态机 unknown/authenticated/unauthenticated;**token 不进 SharedPreferences** |
|
||||
| 认证 | `lib/features/auth/auth_repository.dart` | `AuthRepository` 抽象 + `ApiAuthRepository`:login / register / logout / restoreSession / me;logout 服务端失败也保证本地清除 |
|
||||
| 页面 | `lib/features/auth/splash_page.dart`、`login_page.dart`、`register_page.dart` | 照组装稿逐项实现(见 §3) |
|
||||
| 根 | `lib/app/app.dart` | 认证状态机驱动 Splash ↔ 登录 ↔ 主壳,AnimatedSwitcher 300ms fade;测试注入口(sessionManager / authRepository 可注入) |
|
||||
| 导航 | `lib/core/navigation/fade_route.dart` | `PageRouteBuilder` + `FadeTransition` 300ms(登录 → 注册 push 用) |
|
||||
|
||||
## 3. 页面与状态覆盖
|
||||
|
||||
**Splash**(组装稿 §7):checking / failed 双态;BrandMark 与登录页同构保证过渡对位;spinner 等待 >300ms 才出现(占位保高度不跳动);最短停留 500ms;refresh 超时 5s;错误态「重试」+「改用账号登录」逃生口(清凭证进登录页);**网络失败不清 refresh token,仅服务端 401/40102 才清**。
|
||||
|
||||
**登录页**(组装稿 §5):垂直居中、无 Spacer;两字段仅非空校验(去首尾空格),Focus 包裹失焦校验 + 提交总校验;提交中整表单锁定(字段禁用、注册链接置 null、按钮 loading);错误三层映射——字段级 errorText(onChanged 即清)、40100 → 横幅「用户名或密码错误」+ `SemanticsService.sendAnnouncement` 播报、HTTP 429 → 横幅「尝试次数过多,请稍后再试」、网络 → SnackBar「网络异常,请检查网络后重试」+ 重试 action;成功后 `finishAutofillContext()`,状态机 300ms fade 进主壳;协议行与预留区一律不渲染(ADR-004)。
|
||||
|
||||
**注册页**(组装稿 §6):透明返回栏顶部左对齐;四字段(用户名/手机号/密码/确认密码)失焦校验 + 提交总校验,文案照 04 规范 §3.2;密码 helperText 走主题 muted(FIX-2);密码变更时确认密码已有值则重校验一致性;40900 → 用户名字段「该用户名已被使用」、40901 → 手机号字段「该手机号已注册,可直接登录」;注册成功即建立会话直接进首页(popUntil 首路由,不回登录页)。
|
||||
|
||||
**主壳/个人中心**:`ProfilePage` 的「切换账号或退出登录」接入真实 logout(`POST /auth/logout` Bearer + refreshToken,随后清会话,状态机自动回登录页)。
|
||||
|
||||
## 4. 顺带修复
|
||||
|
||||
- **FIX-1**:首页促销卡渐变改 `[primaryStrong, primary]`(深端在左承载白字,AA 达标;`brandGradient` 本身未动)。
|
||||
- **FIX-2**:`inputDecorationTheme` 补 `helperStyle: TextStyle(color: muted, fontSize: 12)`。
|
||||
- **m2**:README 验证命令补 `--output=none`。
|
||||
|
||||
## 5. 测试(30 通过 = 既有 7 + 新增 23)
|
||||
|
||||
| 文件 | 数量 | 覆盖 |
|
||||
| --- | --- | --- |
|
||||
| `test/core/network/token_refresher_test.dart` | 5 | 并发单飞(仅 1 次请求 + token 轮换)、40102 清会话抛 SessionExpired、网络失败保留 token、单飞复位可重刷、无本地 refresh 直接判失效 |
|
||||
| `test/features/auth/auth_repository_test.dart` | 10 | 登录成功存会话(含请求体逐字段断言)、40100 业务异常、注册 Idempotency-Key + 40900、40101 刷新后重放一次携带新 token、重放仍 401 清会话、登出网络失败也清本地、会话恢复两分支、5xx → 系统错误、429 → 限流异常(全部 mock dio 假 adapter) |
|
||||
| `test/features/auth/login_page_test.dart` | 4 | 初始 / loading(字段禁用+链接置灰)/ 字段错误(输入即清)/ 横幅错误(40100 文案 + 输入即清)四态 |
|
||||
| `test/features/auth/register_page_test.dart` | 4 | 渲染(含预留区不渲染断言)、空表单拦截、四字段格式文案逐项、合法提交调接口 |
|
||||
|
||||
既有 `widget_test.dart` 改为注入已认证会话 + 假仓库后 pump `App`,断言不变仍通过。任务描述中的「现有 13 个测试」与实际不符——工单开工时仓库为 **7 个**测试(上次提交信息「6 个 widget 测试」+ 1 个导航冒烟),7 个全部保持通过。
|
||||
|
||||
## 6. 遗留问题与备忘
|
||||
|
||||
1. **`SemanticsService.announce` 已废弃**:Flutter 3.44 标记 deprecated,横幅播报改用替代 API `sendAnnouncement(View.of(context), ...)`,行为等价,组装稿 §4 后续修订时可同步文案。
|
||||
2. **会话过期的 Splash 最短停留**:refresh 被服务端判 40102 时清会话即切登录页,该罕见分支可能早于 500ms 最短停留(正常成功/失败/无 token 三路均严格遵守);fade 过渡下无闪烁,判定可接受。
|
||||
3. **access token 过期时间未做本地预判**:当前依赖 401/40101 被动刷新(契约行为完备);`accessTokenExpiresAt` 已持久化,后续可加过期前主动刷新优化首个请求延迟。
|
||||
4. **未与真实后端联调**:后端按同一冻结契约并行实现中,本报告所有验证基于 mock dio;联调烟囱测试建议列入下一波工单。
|
||||
5. DEBT-1(TagPill 对比度)按 12 号报告裁决仍另开工单,本次未动。
|
||||
|
||||
---
|
||||
**Frontend Developer** · 2026-09-04 · 门禁:format 0 changed / analyze 0 issues / test 30 passed
|
||||
@@ -1,15 +1,15 @@
|
||||
# 第一迭代进展看板
|
||||
|
||||
> 目标:真实登录纵切(注册 → 登录 → 获取当前用户 → 退出),依据[开发实施计划](../../development-plan.md)第 8 节。
|
||||
> 更新日期:2026-09-04。本页是团队共享的进度事实来源,每波工作交付后更新。
|
||||
> 更新日期:2026-09-04(第三波交付后)。本页是团队共享的进度事实来源,每波工作交付后更新。
|
||||
|
||||
## 当前状态一览
|
||||
|
||||
| 状态 | 内容 |
|
||||
| --- | --- |
|
||||
| ✅ 已完成 | 开工分析(报告 01-06)、工程基线(第一波)、持久化纵切(第二波)、Git 工作流建章 |
|
||||
| 🔜 下一步 | 第三波:JWT + refresh 会话 → `/internal` 鉴权 → OpenAPI 冻结 → Flutter 登录页拼装 → 端到端用例 |
|
||||
| ⚠️ 未闭环 | token 仍为随机串(表已就绪)、`/internal` 无鉴权、CI 载体缺失、UI 待修 FIX-1/FIX-2、Flutter README 门禁参数(m2) |
|
||||
| ✅ 已完成 | 开工分析(01-06)、工程基线(第一波)、持久化纵切(第二波)、JWT 会话 + Flutter 登录纵切 + OpenAPI 契约(第三波)、ADR-001~008 |
|
||||
| 🔜 下一步 | 第四波:真实前后端联调与端到端验证 → CI 载体 → 本地编排(compose)→ 埋点落地 |
|
||||
| ⚠️ 未闭环 | access token 无主动吊销(≤15 分钟窗口)、/internal 为静态密钥、auth_sessions 无过期清理任务、前端全链路仅 mock 验证未联调、CI 载体缺失、TagPill 设计债 |
|
||||
|
||||
## 已完成(附提交)
|
||||
|
||||
@@ -21,22 +21,26 @@
|
||||
|
||||
**第二波:持久化纵切**
|
||||
|
||||
- Flyway V1 baseline(identity/media)、用户 UUIDv7 持久化到 PostgreSQL、统一异常与错误码透传(修复错误码折叠),测试 21 → 37,全部经 Testcontainers 验证(`patbond-api@bd20adc`,报告 10)。
|
||||
- 独立复核确认第一波声明属实(报告 11);UI 设计 QA + 登录/注册/Splash 组装稿(报告 12);埋点工程规范含 OpenAPI/DDL 草案(报告 13);mkdocs 门禁打通(报告 14)。
|
||||
- Git 工作流规范入档(`patbond-doc@027876a`,见 [Git 工作流规范](../../git-workflow.md)),报告 15。
|
||||
- Flyway V1 baseline(identity/media)、用户 UUIDv7 持久化到 PostgreSQL、统一异常与错误码透传,测试 21 → 37,全部经 Testcontainers 验证(`patbond-api@bd20adc`,报告 10)。
|
||||
- 独立复核确认第一波声明属实(报告 11);UI 设计 QA + 登录/注册/Splash 组装稿(报告 12);埋点工程规范(报告 13);mkdocs 门禁打通(报告 14);Git 工作流规范入档(`patbond-doc@027876a`,报告 15)。
|
||||
- ADR-006/007/008 入档:测试与交付容器化、部署形态、PostgreSQL 18 基线(Testcontainers 镜像切换 `patbond-api@43ab6c5`)。
|
||||
|
||||
## 下一步(第三波,未启动)
|
||||
**第三波:认证纵切两端交付**
|
||||
|
||||
1. **T4 JWT + refresh 会话**:按 ADR-003(access 15 分钟 / refresh 30 天轮换 / 多设备),`identity.auth_sessions` 表已随 V1 就绪;同时保护 `/internal/**`、整改 `expiresAt` 时区。
|
||||
2. **T6a OpenAPI 冻结**:错误码契约(报告 10)+ token 字段定型后出契约文档。
|
||||
3. **Flutter 登录页拼装**:照报告 12 组装稿实现,顺带修 FIX-1(促销卡渐变对比度)、FIX-2(helperStyle)、m2(README 门禁参数);接入 `--dart-define` 注入 API 地址。
|
||||
4. **端到端用例**:注册 → 登录 → 获取当前用户 → 退出,进 CI。
|
||||
- 后端 T4 + T6a(`patbond-api@4dc3dcd`,报告 16):JWT RS256(15m/30d 配置项)、refresh 轮换会话(auth_sessions 摘要 + token_family + 重用撤销全族)、多设备并行、登录锁定(42300)、`/api/v1` 前缀、`/internal` 共享密钥鉴权;测试 37 → 73,含双服务真实 HTTP E2E。附带修复两个存量缺陷:Feign 错误解码器被子上下文遮蔽、JDK HttpURLConnection 读不到 401 错误体(此前下游错误在真实链路折叠为 503)。
|
||||
- Flutter 登录纵切(`patbond-flutter@8d890c0` + 42300 映射 `da25804`,报告 17):dio 网络层(`--dart-define=PATBOND_API_BASE_URL`)、单飞 TokenRefresher、secure storage 会话、Splash/登录/注册三页照组装稿实现、真实退出入口;测试 7 → 30;契约零偏差;FIX-1/FIX-2/m2 一并修复。
|
||||
- OpenAPI 契约正式化:[docs/api/openapi.yaml](../../../api/openapi.yaml),契约先行原则见 [API 契约说明](../../../api/index.md)。
|
||||
|
||||
启动条件已满足(持久化纵切完成 + 复核无否决)。
|
||||
## 下一步(第四波,未启动)
|
||||
|
||||
1. **真实联调 + E2E(T8)**:起后端双服务 + compose postgres:18,Flutter 连真实 API 走通注册 → 登录 → me → 刷新 → 退出;按报告 14 的验收证据清单收集证据。前置:本地编排(T0-4,compose 拉起 postgres:18 与两个服务)。
|
||||
2. **CI 载体**:三仓门禁进 CI(命令表在 [Git 工作流规范](../../git-workflow.md))。
|
||||
3. **埋点落地**:按报告 13 实现 `/api/v1/events` + `platform.product_events` 迁移 + Flutter `lib/analytics/`。
|
||||
4. **杂项**:auth_sessions 过期清理任务、Flutter 版本锁定(T0-2)、TagPill 设计债(DEBT-1)。
|
||||
|
||||
## 环境与构建(新成员必读)
|
||||
|
||||
- 后端构建:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`(本机默认 JDK 版本过高,必须显式指 17;需 Docker 供 Testcontainers 起 postgres:16)。
|
||||
- 后端构建:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`(本机默认 JDK 版本过高,必须显式指 17;需 Docker 供 Testcontainers 起 postgres:18,见 ADR-008)。
|
||||
- Flutter 门禁:`dart format --output=none --set-exit-if-changed lib test` / `flutter analyze` / `flutter test`。
|
||||
- 文档门禁:`mkdocs build --strict`。
|
||||
- 测试数据库策略见 ADR-006:自动化测试一律 Testcontainers,个人手动联调用本机 PostgreSQL(环境变量注入连接信息)。
|
||||
|
||||
@@ -23,5 +23,9 @@ nav:
|
||||
- 13 埋点实现规范: development/iterations/iteration-1/13-tracking-implementation-spec.md
|
||||
- 14 里程碑证据档案: development/iterations/iteration-1/14-evidence-milestone-dossier.md
|
||||
- 15 Git 收尾报告: development/iterations/iteration-1/15-git-workflow-report.md
|
||||
- 16 后端认证会话报告: development/iterations/iteration-1/16-backend-auth-report.md
|
||||
- 17 Flutter 登录纵切报告: development/iterations/iteration-1/17-flutter-login-report.md
|
||||
- API:
|
||||
- 契约说明: api/index.md
|
||||
- 架构:
|
||||
- 技术决策记录: architecture/decisions.md
|
||||
|
||||
Reference in New Issue
Block a user