2026-06-28-stylist-login-minimal-closed-loop-design.md 12 KB

美发师最小闭环登录设计

背景

当前项目已经具备以下与登录相关的基础形态:

  • 前端已有美发师登录页 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: <token>。
  • 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,返回当前登录人最小身份信息。

请求体:

{
  "email": "stylist@example.com",
  "password": "plain-password",
  "remember": false
}

字段约束:

  • email: 必填,合法邮箱格式。
  • password: 必填,最少 8 位。
  • remember: 选填,布尔值。

成功响应:

{
  "ok": true,
  "data": {
    "token": "jwt-token",
    "tokenName": "Authorization",
    "stylistId": 1,
    "name": "Ogawa",
    "email": "stylist@example.com"
  },
  "error": null
}

失败响应:

{
  "ok": false,
  "data": null,
  "error": {
    "code": "AUTH_INVALID_CREDENTIALS",
    "message": "邮箱或密码错误"
  }
}

约束说明:

  • 不区分“邮箱不存在”“账号已删除”“密码错误”的对外提示,统一返回登录失败。
  • tokenName 固定返回 Authorization,与前端现有发送方式保持一致。
  • 返回体只包含前端当前已经消费的字段,不附带大块业务数据。

2. 退出登录接口

  • 路径: POST /api/v1/auth/logout
  • 作用: 在本 case 中定义为幂等退出入口,允许前端形成完整交互闭环。

成功响应:

{
  "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: <token>。
  • 这样 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. 登录审计、安全增强与多设备策略专题。