账号与客户模块 迭代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环境、完整演示录屏仍待对应证据 |