# 美发师最小闭环登录设计 ## 背景 当前项目已经具备以下与登录相关的基础形态: - 前端已有美发师登录页 `frontend/src/pages/public/LoginPage.tsx`。 - 前端已有登录接口封装 `frontend/src/services/api/auth.ts`。 - 前端已有本地会话存储与路由守卫,分别位于 `frontend/src/services/http/session.ts` 和 `frontend/src/shared/RequireAuth.tsx`。 - 后端已有认证模块骨架与占位接口 `backend/src/modules/auth/`,其中 `POST /api/v1/auth/stylist/login` 尚未实现真实逻辑。 - 项目已经明确统一响应结构为 `{ ok, data, error }`,并坚持“零 Mock”原则。 现阶段项目开发方式改为 case-by-case,因此第一个 case 需要先把“美发师后台登录”做成一个真实可跑通的最小闭环,为后续 dashboard、customers、templates、simulations 等后台能力提供统一鉴权基础。 ## 目标 - 实现美发师基于 `email + password` 的真实登录能力。 - 登录成功后返回 token,并允许前端进入后台页面。 - 让前端在后续受保护请求中自动携带 token。 - 未登录访问后台时自动跳转登录页。 - token 失效或接口返回 `401` 时,前端自动清理登录态并要求重新登录。 - 提供幂等的退出登录接口,保证用户可以确定退出后台。 ## 非目标 - 不实现顾客侧登录或注册。 - 不实现忘记密码、重置密码、短信验证码、邮件验证码。 - 不实现 refresh token、续期机制、多设备会话管理。 - 不实现服务端 session 黑名单或踢出其他设备。 - 不在本 case 中扩展复杂风控,如登录失败次数限制、账户锁定、设备指纹识别。 ## Case 范围 本 case 只覆盖以下闭环: 1. 前端登录页提交邮箱和密码。 2. 后端校验美发师账号和密码。 3. 后端返回标准登录结果。 4. 前端保存 token 并进入后台。 5. 后续受保护页面和接口复用同一套鉴权机制。 6. `401` 自动清理登录态。 7. 用户主动退出登录。 本 case 不要求同时解决所有认证相关专题,而是优先保证“真实登录进入后台”这条主路径可用且稳定。 ## 设计原则 - 以最小闭环为目标,优先打通主路径,不提前引入 refresh token 体系。 - 继续对齐现有前后端契约,减少无效重构。 - 未实现的能力显式不做,不用占位假数据冒充完成。 - 认证职责集中在 `auth` 模块和通用鉴权层,不把 token 解析散落到各业务模块。 - 安全语义保持基础可信,避免泄露“邮箱不存在”或“密码错误”的区分信息。 ## 现状对齐 ### 前端现状 - `frontend/src/services/api/auth.ts` 已约定登录成功后返回 `token`、`tokenName`、`stylistId`、`name`、`email`。 - `frontend/src/services/http/client.ts` 已支持在请求头中自动附加 `Authorization: `。 - `frontend/src/services/http/session.ts` 当前使用 `localStorage` 保存 `token` 和 `tokenName`。 - `frontend/src/shared/RequireAuth.tsx` 当前只根据本地是否存在 token 判断是否允许进入后台页面。 - `frontend/src/pages/public/__tests__/login-page.test.tsx` 已覆盖登录错误提示区域的稳定渲染行为。 ### 后端现状 - `backend/src/modules/auth/dto/stylist-login.dto.ts` 已定义 `email`、`password`、`remember` 三个字段。 - `backend/src/modules/auth/dto/login-result.dto.ts` 已定义登录成功返回字段。 - `backend/prisma/schema.prisma` 已包含 `HairstylistUser` 模型,对应 `hairstylist_user` 表。 - `backend/src/modules/auth/auth.service.ts` 目前仍抛出 `NotImplementedException`,尚未接入真实账号校验。 因此,本设计优先补齐真实认证逻辑,而不是重新设计一套新的前后端接口。 ## 接口设计 ### 1. 登录接口 - 路径: `POST /api/v1/auth/stylist/login` - 作用: 校验美发师邮箱和密码,签发 access token,返回当前登录人最小身份信息。 请求体: ```json { "email": "stylist@example.com", "password": "plain-password", "remember": false } ``` 字段约束: - `email`: 必填,合法邮箱格式。 - `password`: 必填,最少 8 位。 - `remember`: 选填,布尔值。 成功响应: ```json { "ok": true, "data": { "token": "jwt-token", "tokenName": "Authorization", "stylistId": 1, "name": "Ogawa", "email": "stylist@example.com" }, "error": null } ``` 失败响应: ```json { "ok": false, "data": null, "error": { "code": "AUTH_INVALID_CREDENTIALS", "message": "邮箱或密码错误" } } ``` 约束说明: - 不区分“邮箱不存在”“账号已删除”“密码错误”的对外提示,统一返回登录失败。 - `tokenName` 固定返回 `Authorization`,与前端现有发送方式保持一致。 - 返回体只包含前端当前已经消费的字段,不附带大块业务数据。 ### 2. 退出登录接口 - 路径: `POST /api/v1/auth/logout` - 作用: 在本 case 中定义为幂等退出入口,允许前端形成完整交互闭环。 成功响应: ```json { "ok": true, "data": { "success": true }, "error": null } ``` 说明: - 本轮不维护服务端 session 黑名单。 - 后端不需要保存额外登出状态。 - 真正使用户退出的是前端删除本地 token。 - 即使后端暂时没有额外操作,这个接口仍然保留,便于前端后续保持统一调用路径。 ## 数据与鉴权设计 ### 账号来源 - 使用 `prisma.hairstylistUser` 查询 `hairstylist_user` 表。 - 只允许 `is_deleted = 0` 的账号参与登录。 - 查询条件以邮箱为主键语义,不依赖用户名登录。 ### 密码校验 - 数据库中的 `password` 视为加密后的口令,而不是明文密码。 - 服务端使用 `bcrypt` 做密码哈希比对。 - 登录流程中绝不记录或输出明文密码。 ### Token 设计 - 本 case 只签发单个 access token。 - token 类型采用 JWT。 - payload 只包含最小必要身份字段,例如 `sub`、`email`、`username`。 - `username` 供工作台欢迎区直接消费,不再额外请求 `/me` 接口。 - 不在 token 中塞入模板数量、客户统计等业务数据,避免 token 承担缓存职责。 ### Remember 语义 - 前端 `remember` 继续透传给后端,保持接口兼容。 - 本 case 中 `remember` 不触发不同的 token 生命周期策略。 - 也就是说,本轮不存在“勾选后长期登录、未勾选后短期登录”的差异化实现。 - 等后续真正引入 refresh token 或明确会话体系后,再赋予 `remember` 实际语义。 ## 后端模块职责 ### AuthController - 暴露 `POST /auth/stylist/login` 和 `POST /auth/logout`。 - 只负责接收 DTO 和返回 service 结果。 - 不在 controller 中直接写数据库查询或密码比对逻辑。 ### AuthService 负责 4 件核心工作: 1. 查询美发师账号。 2. 校验密码。 3. 签发 JWT。 4. 组装登录返回体。 这样可以把真实认证逻辑集中在一个地方,便于后续继续扩展注册、重置密码或审计能力。 ### JwtStrategy / JwtAuthGuard - 负责解析 `Authorization` 请求头。 - 校验 token 签名与过期时间。 - 将当前登录美发师的最小身份信息挂到 request 上。 - 供后续后台模块统一复用,不在 customers、templates、simulations 中重复实现 token 解析。 ## 前端交互闭环 ### 登录流程 1. 用户在登录页输入邮箱和密码。 2. 提交后页面进入 `submitting` 状态,禁止重复提交。 3. 前端调用 `stylistLogin()`。 4. 后端登录成功后返回 token 及最小身份信息。 5. 前端通过 `setSession()` 保存 `token` 和 `tokenName`。 6. 页面跳转到 `/{locale}/dashboard`。 ### 会话保存 - 当前实现继续使用 `localStorage` 保存 token。 - `RequireAuth` 仅判断本地 token 是否存在,不在前端解析 JWT 内容。 - 页面刷新后,如果 token 仍存在,用户继续保持“已登录”前端状态。 ### 受保护请求 - 后续业务请求统一由 `frontend/src/services/http/client.ts` 自动附加 `Authorization: `。 - 这样 customers、templates、simulations 后续 case 可以直接复用当前登录闭环,无需重新设计前端鉴权传递方式。 ### 401 失效处理 - 任何接口返回 `401` 时,前端统一执行 `clearSession()`。 - 当用户再次访问受保护页面时,由 `RequireAuth` 自动重定向回登录页。 - 这样不需要每个页面单独维护“token 失效”处理逻辑。 ### 登录失败提示 - 登录页只展示统一业务错误文案,例如“邮箱或密码错误”。 - 错误提示区域保持固定占位,避免表单高度跳动。 - 这一点与现有 `login-page.test.tsx` 的测试意图保持一致。 ### 退出登录 1. 用户在后台触发退出。 2. 前端调用 `POST /auth/logout`。 3. 不论接口成功或失败,前端最终都执行 `clearSession()`。 4. 页面跳回登录页。 这样可以保证用户感知上的退出动作是确定完成的,而不依赖服务端必须维护黑名单。 ## 错误处理语义 ### 登录失败 以下情况统一作为登录失败处理: - 邮箱不存在。 - 账号已被逻辑删除。 - 密码不正确。 对外统一返回: - HTTP 状态码: `401` - 业务错误码: `AUTH_INVALID_CREDENTIALS` - 错误文案: `邮箱或密码错误` ### Token 异常 以下情况统一返回 `401`: - 缺少 token。 - token 非法。 - token 过期。 前端面对这些情况的行为统一是: - 清理本地登录态。 - 重新进入登录页。 ### 参数错误 - 邮箱格式不合法、密码长度不足、类型不匹配等参数问题,继续通过全局校验机制返回统一错误结构。 - 这类错误与“认证失败”分开处理,不复用登录失败文案。 ## 安全与日志边界 - 服务端日志记录登录成功、登录失败、token 校验失败等关键事件。 - 日志中不打印明文密码。 - 日志中不打印完整 token,必要时只保留前缀或摘要用于排查。 - 登录失败提示不泄露账号是否存在,避免被用于账号枚举。 ## 测试与验收 ### 推荐测试范围 - 后端: - 登录成功测试。 - 错误密码登录失败测试。 - 删除账号或不存在账号登录失败测试。 - 受保护接口缺少 token 返回 `401` 的测试。 - 前端: - 登录成功后跳转到 dashboard。 - 登录失败时错误提示稳定显示。 - `401` 后清 session 的行为测试。 ### 验收标准 - 数据库存在有效美发师账号时,可以使用真实邮箱密码登录成功。 - 登录成功后进入 `dashboard`,刷新页面后仍保持已登录状态。 - 未登录访问受保护页面会被重定向到登录页。 - token 无效或过期时,受保护接口返回 `401`,前端自动清理登录态。 - 错误密码登录时,页面稳定显示错误信息,不出现假成功或 mock 数据。 - 点击退出登录后,用户返回登录页,且再次访问后台需要重新登录。 ## 实施提示 - 后端实现时可优先复用现有 `StylistLoginDto` 和 `LoginResultDto`,避免重复定义契约。 - 如果当前数据库中的 `hairstylist_user.password` 还不是 bcrypt 哈希,需要先明确初始化策略,再决定如何创建首批测试账号。 - `logout` 在本轮是“前端闭环接口”,不是“服务端会话撤销接口”,实现时不要过度设计。 ## 推荐后续 case 在本 case 完成后,建议按以下顺序继续推进: 1. 后台首页或顾客列表等首个受保护页面联调,验证 token 真正参与接口访问。 2. 忘记密码/重置密码专题。 3. refresh token 与会话续期专题。 4. 登录审计、安全增强与多设备策略专题。