买家门户与交易模块 迭代2 - xiaochu-dev/online-shopping-group5 GitHub Wiki
买家门户与交易模块(迭代 2)
2026-10-21商品接口交接:PR #1以正式CategoryQueryService / ProductDetailService / StockService接入门户,移除商品结构探针与无限库存降级,并通过ProductQueueAdapter提供库存归零的队列失败处理。详见商品中心模块-迭代2。PR已合入iter2-java;本页正文已同步正式接入后的行为。
负责人:周到(产品经理) 对应需求:挑战需求 A.1.3(一件一单与库存扣减)× A.1.4(售罄与队列归零) × A.2.1(下单必须登录)× A.2.2(同买家同商品限一条)× A.2.5(商家确认交易与客户历史) 代码包:
portal/独占数据表:intent(新增 5 列) 依据:《工作任务表 v4.0》第五节「周到 —— 买家门户与交易(15 点)」
一、模块边界
本模块独占 portal/ 包与 intent 表,其他模块不得直接 import 本模块的 Entity / Mapper,
也不得写 intent 表的 SQL。客户历史对外出口如下(另有库存队列适配,见下表):
| 出口 | 使用方 | 作用 |
|---|---|---|
IntentHistoryProvider(实现 admin.port.IntentHistoryPort) |
卖家后台客户详情页 | 按买家 id 返回已结束订单,供货主查看客户购买历史 |
方向是「倒过来」的:intent 表归本模块,所以由消费方(账号与客户模块)定义接口、本模块实现,
后台只认接口,不碰 intent 的表结构与实体。
当前跨模块调用如下:
| 接口 | 正式提供方 | 当前行为 |
|---|---|---|
| BuyerContext.current() | 账号与客户模块 | 提供登录买家,未登录跳买家登录页 |
| ProductPort | product.ProductDetailService | 图片、库存、冻结、释放与扣减调用正式商品服务 |
| CategoryPort | product.CategoryQueryService | 启用的两级分类树,一级筛选包含二级子类 |
| IntentQueuePort | portal.ProductQueueAdapter | 向StockService提供pending批量失败及hasTrading查询 |
原商品结构探针与无限库存降级已移除。portal.ContractDefaults只保留卖家Session上下文适配;部分门户查询仍直接使用商品Entity/Mapper,不把这些旧查询描述成已完全解耦。
二、路由一览
买家侧
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | / |
公开 | 商品列表;page / size / categoryId / keyword,默认 12 条/页,上限 100 |
| GET | /products/{id} |
公开 | 商品详情;含图片、库存、当前排队人数、在售/交易中状态 |
| POST | /products/{id}/intents |
需买家登录 | 提交意向;未登录 302 → /buyer/login?redirect=/products/{id} |
| POST | /intents/cancel |
公开(凭口令码) | 基线兜底,买家页面已无入口 |
| GET | /my/orders |
需买家登录 | 我的订单,默认 10 条/页 |
卖家侧(LoginInterceptor 拦 /admin/**)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/intents |
交易与意向总览:我的商品 + 每个商品的未结束意向数 |
| GET | /admin/intents/{productId} |
单商品队列:交易中 1 条 + 排队若干,按提交时间正序 |
| POST | /admin/intents/{intentId}/confirm |
确认交易(只允许队首) |
| POST | /admin/intents/{intentId}/success |
交易成功:扣 1 件库存 |
| POST | /admin/intents/{intentId}/fail |
交易失败:requeue=true 塞回队尾,否则作废;reason 可空 |
三个动作都走 Post/Redirect/Get,执行完返回redirect视图,当前默认302回队列页,F5 不会重复成交。
三、数据模型:intent
本迭代对 intent 表做了 5 处改动(脚本 schema/03-intent.sql):
| 字段 | 类型 | 作用 | 为什么必须有 |
|---|---|---|---|
buyer_id |
INTEGER NOT NULL DEFAULT 0 | 下单人;0 = 迭代 1 遗留的匿名单 |
A.2.1 下单必须登录;历史只查 buyer_id = ?,匿名单永远不会串到任何客户账下 |
trade_address |
TEXT | 下单时的交易地点快照 | 买家之后改默认地点,历史订单要显示当时的约定地点 |
finished_at |
INTEGER | 交易时间 | 「我的订单」按这一单的时间排序 |
trade_result |
TEXT | success / failed |
与 status 分离:status 是流程位置,trade_result 是对外结果 |
fail_reason |
TEXT | 失败原因 | A.2.2 要求历史里说明为什么失败 |
同时:token_code 改为可空(主路径不再生成,存量数据仍可凭码撤销),没有 quantity 字段——
一次意向对应1件;同一买家要买3件,须在前一笔结束后再提交下一笔,不能同时挂3笔未结束意向。buyer_id是逻辑关联,DDL没有buyer外键。
索引与查询路径一一对应:
| 索引 | 服务于 |
|---|---|
idx_intent_queue (product_id, status, created_at) |
队列页 |
idx_intent_buyer (buyer_id, status, finished_at) |
我的订单 |
idx_intent_buyer_id (buyer_id, created_at) |
后台客户购买历史 |
uq_intent_buyer_active (buyer_id, product_id) WHERE status IN ('pending','trading') AND buyer_id > 0 |
库层兜底「一人一商品一条未结束」,防并发穿透 |
队列位次不落库:不存
position字段。有人结束或撤销,后面的人靠行号天然前移, 不需要批量 UPDATE,也不会出现「位次卡在 3 不动」的脏数据。
四、状态机与「冻结」语义
pending ──确认交易──→ trading ──交易成功──→ success (扣 1 件库存)
↑ │
└──塞回队尾───────────┴──交易失败──→ failed (库存不动,可选塞回)
当前卖家作废动作置failed(带原因);cancelled仍仅保留给旧口令码撤销。
三条容易写错的地方,也是本模块的三个判断:
判断一:整件冻结不需要 locked 字段。
A.1.3 是「一件一单」,库存 N 件时同一商品会在不同时间成交 N 次,任意时刻只有一笔在谈。
所以「交易中」是商品级互斥的状态位,用 product.status = frozen 就能表达,再开一个 locked
只会多一份需要同步的真相。冻结发生在「确认交易」,交易结束调用release;只有仍为frozen才会恢复,手动下架保持下架。
判断二:扣库存的时机是「交易成功」,不是「提交意向」。 提交意向时扣,失败一次就要回滚一次,还要处理并发超卖;放到成功那一刻, StockService按商品编号、库存大于0及允许成交的状态进行原子条件更新, 「扣不动就置不了成功」由数据库保证,事务里直接抛异常回滚。
判断三:售罄要主动清理队列。
库存扣到 0 以后,后面排队的人永远不会被确认。让他们继续挂着,等于给了一句「你先等着」的空承诺。
所以 markSuccess 扣完库存会回查剩余量,为 0 就把该商品所有 pending 批量置 failed,
StockService经ProductQueueAdapter在同一事务内结束pending,门户还保留零库存兜底;原因写「商品库存已售罄,本次排队自动结束」,买家在「我的订单」里能直接看到。
五、挑战需求 A 的落地对照
| 口径(澄清问答记录) | 落地位置 | 可验证的行为 |
|---|---|---|
| A.2.1 下单必须登录,浏览可不登录 | PortalController#submitIntent |
未登录点「登录后购买」跳登录页并带回跳地址 |
| A.1.3 一次意向 = 1 件,不设数量 | intent 结构 + submitOrder |
页面无数量输入,每笔成交扣1,提交不预扣或预占库存 |
| A.2.2 同一买家同一商品只能有 1 条未结束 | countActiveByBuyer + 部分唯一索引 |
第二次提交被拒绝并给出提示;并发下由索引拦住 |
| A.1.3 确认交易即冻结商品 | confirmTrade → freeze |
商品从列表消失,详情页显示「交易中」 |
| A.1.4 库存归零后队列批量置失败 | markSuccess → failPendingOfProduct |
最后一笔成交后,剩余排队单全部变失败并带原因 |
| A.2.2 历史只显示 success / failed | OrderHistoryService + selectFinishedByBuyer |
排队中、交易中的单不进「我的订单」 |
| A.2.5 商家按队列顺序处理 | confirmTrade 校验队首 |
确认非队首被拒绝 |
| A.2.5 失败可由卖家决定作废或重新排队 | markFailed(requeue, reason) |
塞回后该买家回到队尾,created_at 刷新 |
六、提示语口径
统一「说到位、不吓人」:所有提示都通过 ?msg= / ?error= 走 PRG,不弹独立错误页。
| 场景 | 文案 |
|---|---|
| 提交成功 | 购买意向已提交,等待卖家确认。可在「我的订单」查看已有结果的订单。 |
| 重复提交 | 你已对该商品提交过意向,请在卖家处理后再次购买 |
| 商品售罄 | 该商品已售罄,暂时无法提交意向 |
| 商品下架 | 商品不存在或已下架 |
| 确认交易 | 已确认交易,商品进入「交易中」,其他买家暂时无法下单 |
| 交易成功 | 已标记交易成功,库存扣减 1 件 |
| 交易失败 | 已标记交易失败并作废该买家的意向 / 已标记交易失败,该买家已塞回队尾重新排队 |
| 塞回冲突 | 该买家已另有一条排队中的意向,塞回会重复占位;请改选「作废」 |
页面上还有两处刻意的解释性文案:详情页写明「本系统按提交时间先到先得,越早提交排得越前」, 未登录时写明「浏览商品无需登录,提交购买意向时必须登录身份」——这两句是为了让验收人一眼看懂规则, 不用翻文档。
七、验收证据
测试(门户相关26项,包含在已记录的全量83项中)
逐条用例与实测结果见 买家门户与交易测试记录-迭代2。
| 测试类 | 用例 | 覆盖 |
|---|---|---|
PortalFlowTest |
14 | 列表筛选/搜索/分页、详情四种状态(在售/交易中/已下架/不存在)、未登录跳登录带回跳、下单成功与买家快照、重复提交拦截、交易地点默认值与缺失校验 |
MyOrdersFlowTest |
3 | 未登录跳登录、只显示已有结果的订单且按交易时间倒序、换号看不到他人订单 |
SellerTradeFlowTest |
9 | 未登录拦截、待处理数、队首校验、冻结时库存不动、不能开第二笔、成交扣 1 件并解冻、失败不扣库存、失败塞回队尾、队列只显未结束 |
既有门户截图(原模块记录:本地8080)
| # | 截图 | 说明 |
|---|---|---|
| 1 | images/zhou-dao/review2/product-list.png |
商品列表(免登录可浏览) |
| 2 | images/zhou-dao/review2/keyword-search.png |
关键词搜索命中名称与描述 |
| 3 | images/zhou-dao/review2/category-filter.png |
分类筛选 |
| 4 | images/zhou-dao/review2/detail-onsale.png |
详情:在售,显示排队人数 |
| 5 | images/zhou-dao/review2/login-redirect.png |
未登录下单 → 跳登录并带回跳地址 |
| 6 | images/zhou-dao/review2/detail-form.png |
已登录的下单表单 |
| 7 | images/zhou-dao/review2/order-submitted.png |
提交成功提示 |
| 8 | images/zhou-dao/review2/detail-frozen.png |
交易中的商品无法下单 |
| 9 | images/zhou-dao/review2/my-orders.png |
我的订单:只显示已有结果的单 |
| 10 | images/zhou-dao/review2/seller-intents.png |
卖家:商品与未结束意向数 |
| 11 | images/zhou-dao/review2/seller-queue.png |
卖家:队列与三个动作 |
八、接入状态与后续核对
| 项 | 当前状态 |
|---|---|
| 库存、扣减、自动下架 | 已接入StockService,零库存经队列适配批量失败 |
| 商品多图 | product_image已建表,按主图优先、sort_no与id展示;无上传使用占位图 |
| 两级分类 | parent_id与CategoryQueryService已交付,无缺列文本推导路径 |
| 卖家商品管理 | /admin/products负责库存与上下架;门户/admin/intents负责交易,不提供商品编辑 |
| 旧口令码撤销 | 接口保留,买家页面无入口;未承诺下一迭代删除 |
| 验证边界 | 已记录全量83项;团队Java17环境、外站视频播放和录屏待补 |
交易失败不会自动把下一位改为trading,需要卖家再次确认队首。发布和补货为0时自动下架;有交易时不允许直接将库存调为0;手动下架后的已有交易仍可完成,release不会将其恢复在售。
相关文档:挑战需求A-澄清问答记录(口径来源)、第2次阶段性评审报告(任务与进度)