账号与客户模块 迭代2 - xiaochu-dev/online-shopping-group5 GitHub Wiki

账号与客户模块(迭代 2)

负责人:胡成溯(组长) 对应需求:挑战需求 A.2.1(客户自由注册)× A.2.3(商家查看客户信息与购买历史) 代码包:auth/、buyer/、admin/ 独占数据表:seller(沿用)、buyer(新增) 依据:《工作任务表 v4.0》第四节「胡成溯 —— 账号与客户模块(13 点)」


一、模块边界

本迭代按「一人一模块」重排分工,本模块独占上述三个包与两张表,其他模块不得直接 import 本模块的 Entity / Mapper,也不得写这两张表的 SQL。

对外只暴露两个出口:

出口 使用方 作用
BuyerContext.current() 买家门户(周到) 当前登录买家;未登录返回 null,由门户跳登录页
IntentHistoryPort(本模块定义、portal 实现) 本模块的客户详情页 读某个买家的购买历史;portal 实现后自动注入

第二个出口方向是「倒过来」的:intent 表归 portal,所以由消费方定义接口、提供方实现, 保证 admin 只读接口、不碰对方的表和实体。


二、路由一览

买家侧

方法 路径 权限 说明
GET /buyer/register 公开 注册页
POST /buyer/register 公开 成功302 → /buyer/login?msg=…;失败重渲染并回填
GET /buyer/login 公开 买家登录页;已登录则跳 redirect 或 /buyer/me
POST /buyer/login 公开 成功下发 BUYER_SESSION,302 回原页
GET /buyer/logout 公开 清 cookie
GET /buyer/me 需买家登录 买家中心(用户名、电话、默认地点、注册时间)

卖家后台

方法 路径 说明
GET /admin/customers 客户列表:分页、用户名/电话检索、注册时间倒序
GET /admin/customers/{id} 客户详情 + 购买历史

后台权限由既有 LoginInterceptor(拦 /admin/**)保证;买家权限由 BuyerLoginInterceptor(拦 /buyer/me、/my/**)保证,两者互不通用。


三、数据模型:buyer

字段按《挑战需求A-澄清问答记录》定稿,只有五项业务字段:

列 类型 约束 说明
id INTEGER 主键自增
username TEXT NOT NULL UNIQUE 登录名
password_hash TEXT NOT NULL sha256(salt + 明文) 十六进制
salt TEXT NOT NULL 8 字节随机盐十六进制,改密时轮换
phone TEXT NOT NULL,但【不唯一】 澄清结论:电话可重复填、不校验格式
default_place TEXT 可空 默认交易地点,下单时可当次改写
created_at INTEGER NOT NULL 注册时间,毫秒时间戳

索引:idx_buyer_created(列表倒序分页)、idx_buyer_phone(后台按电话检索)。

与卖家账号的一致性:密码算法完全复用 auth/security/PasswordHasher(sha256 + 8 字节盐, 常量时间比较),澄清结论明确「不引入 BCrypt」。两个账号体系算法一致,将来升级只换一个实现类。

刻意不加的约束:phone 不加 UNIQUE。如果后人「顺手」加唯一索引,会直接违反澄清结论, 因此专门写了用例 电话可重复填_同一号码可注册多个账号 锁住这条行为。


四、登录态:两套 cookie 并存

卖家与买家的登录态必须分开,否则买家下单会把卖家踢下线(演示时必然撞上)。实现如下:

项 卖家 买家
载体 SHOP_SESSION(HttpSession) BUYER_SESSION(签名 cookie)
写入位置 服务端 Session 响应头 Set-Cookie
有效期 7 天 7 天
相互影响 同一浏览器可同时登录,互不覆盖 同左

BUYER_SESSION 的值为 buyerId:过期毫秒:HMAC-SHA256:

  • 带签名:用户手改 cookie 冒充他人会被拒(用例 伪造或篡改的买家cookie应被拒绝 已验证);
  • 常量时间比较:防时序侧信道;
  • HttpOnly:JS 读不到,降低 XSS 窃取风险;
  • 密钥每次启动随机生成:进程重启后旧 cookie 全部失效——与 HttpSession 行为一致,但演示前不要重启应用。

已知局限:无状态签名无法在服务端吊销单个 cookie,退出登录只清浏览器侧那份。若将来需要「强制下线」,要改成「会话表 + 服务端删除」,本迭代不做。


五、客户管理(对应 A.2.3)

列表页 /admin/customers

  • 展示字段固定为 用户名、电话、默认交易地点、注册时间,不做脱敏(卖家内部经营视图,脱敏反而影响联系客户);
  • 排序固定为注册时间倒序(新注册在前);
  • 检索按用户名或电话模糊匹配;
  • 顶部显示注册客户总数;空结果有区分提示(「没有匹配 xx」/「还没有客户注册」)。

详情页 /admin/customers/{id}

  • 上半部分:客户基本信息(同列表四项 + 客户 ID);
  • 下半部分:购买历史,由 IntentHistoryPort 提供。

当前状态:portal中的IntentHistoryProvider已实现并注册IntentHistoryPort,客户详情展示该客户的success/failed订单历史,基本信息与历史均已接入。原先无Provider时的占位提示仅是降级分支,不代表当前集成状态。

接口签名(详见 docs/契约-账号与客户-迭代2.md):

public interface IntentHistoryPort {
    List<IntentHistoryItem> listByBuyer(Long buyerId);
}
public record IntentHistoryItem(String productName, Long createdAt,
                                String result, Long finishedAt, String failReason) {}

六、提示语口径

场景 提示
用户名重复 用户名已被注册,请换一个
用户名长度不合法 用户名长度需在 2~20 个字符之间
密码不足 8 位 新密码长度不得少于 8 位
电话为空 请填写联系电话
登录失败 用户名或密码错误(不区分账号不存在,防用户名枚举)
未登录访问受保护页 页面 302 跳登录页并带 redirect;接口 401 + JSON

七、验收证据

本模块及基座相关41项包含在2026-10-21已记录的全量83项回归中(Failures 0 / Errors 0),本次文档同步未重跑测试。

测试类 条数 覆盖
AuthFlowTest 15 卖家登录、拦截、改密(迭代 1 遗留,重构后仍全绿)
BuyerAuthTest 18 注册 6、登录与隔离 4、客户管理 3、越权与 cookie 篡改 5
CommonLayerTest 6 实体 / Mapper / 分页 / 金额边界
SchemaIdempotencyTest 2 四份建表脚本连跑三遍:表结构不变、种子不翻倍

除单元测试外,还做了一次真实启动的端到端冒烟(非 Mock):

注册 302 → 买家登录 302 且拿到 BUYER_SESSION → /buyer/me 正常渲染
带买家 cookie 访问 /admin/customers → 302 拒到卖家登录页
卖家登录后 /admin/customers 能看到刚注册的客户

八、接入状态与遗留

事项 归属 当前状态
IntentHistoryPort 周到(portal) IntentHistoryProvider已实现,客户详情购买历史已接入
分类与商品SQL 黄程宇 parent_id、stock、off_reason与product_image已交付
意向SQL 周到 buyer_id、交易地点快照、交易结果与时间已交付;无quantity
重启后重新登录 现有会话实现 买家签名密钥每次启动生成,演示前重新登录
目标环境联调 团队 Java17环境、完整演示录屏仍待对应证据

返回项目 Wiki 首页 | 第2次个人汇报(胡成溯)