research.md 7.0 KB

Research: 用户端统一订单列表

Date: 2026-09-20 | Status: 决策已与用户确认,无未解决 NEEDS CLARIFICATION

决策来源:头脑风暴对话(方案对比/滚动加载确认/收件视角确认)+ 代码探索报告(现有列表接口、状态字段全集、写入点清单、索引现状)。

D1: 架构——Java 读时聚合(否决中间表/视图/ES)

  • Decision: 读侧新 Service 双路查询 + 内存归并;不建中间表、不建视图、不引入 ES;零 DB 结构变更。
  • Rationale: ① 中间表双写一致性风险大——探索确认两域状态写入点多达 20+ 处(商家四状态、骑手三连、用户取消、平台改状态、LINE/OMG 支付回调、闪送状态机六方法、两个自动任务),每处都要同步,且该区域遍布废弃代码(TestTask/ZaloPay 等),漏改即脏数据;② 视图把 tab 状态映射埋进 SQL DDL(每分支 CASE WHEN 30-60 行),不可单测,每次调口径要人工执行 DDL;③ ES 是亿级订单方案,本项目体量杀鸡用牛刀;④ 个人订单列表数据量小(单用户几百单级),读时聚合性能足够,数据永远实时,写侧零侵入。
  • Alternatives: ① 中间表统一存储(用户最初设想)——被一致性与维护成本否决;② MySQL UNION 视图——被可测性与变更成本否决;③ ES/搜索服务——体量不符,否决。将来若演进为订单搜索服务,仅替换读实现,接口契约不变。

D2: 分页——游标滚动(严格 keyset,含同秒跨源决胜)

  • Decision: cursor = 不透明字符串 时间毫秒_source_id;前端滚动加载透传 nextCursor。排序:createTime DESC → source 字典序(flash<takeaway) → id DESC。两表时间均为秒级 datetime,同秒跨源并列必须确定性决胜,否则翻页漏/重。
  • Rationale: 用户确认前端是滚动加载(非页码),keyset 每次滚动代价恒定(每路一条索引查询取 size+1 行);页码模式的深翻页浪费不存在。
  • Alternatives: 页码分页(前端现状不是页码,且深翻页两边各取 N×size 条浪费)——否决。

D3: tab 枚举与归类规则——沿用外卖现有语义

  • Decision: tab = all/unpaid/active/completed/cancelled/refund(与 UserOrderController.orderList 现有参数一致)。外卖侧条件照搬现有实现(unpaid: payStatus=0 且 payType∈{2,3} 且 state≠4 且无售后;active: state∈{0,1,2} 且无售后且已付或到付自取堂食;completed: state=3 无售后;cancelled: state=4 无售后;refund: 售后>0)。闪送侧映射:非终态(WAITING_ACCEPTANCE/ACCEPTED/PICKED_UP/DELIVERED)→active、COMPLETED→completed、CANCELLED→cancelled;unpaid/refund 闪送侧恒空。
  • Rationale: 前端 tab 参数名不变减少改动;外卖语义经过现网验证;归类规则抽成一份谓词(SQL 条件构造 + 内存判定同源),杜绝两处漂移(FR-007)。
  • Alternatives: 新造一套 tab 枚举——徒增前端映射,否决。

D4: 闪送收件人视角——纳入统一列表

  • Decision: 闪送路查询条件 (user_id=? OR receiver_user_id=?) 单条 OR 查询;role 判定:user_id 命中→sender,否则 receiver;寄件人=收件人只出一条(判 sender)。列表项带 role 字段,前端渲染"我收的"标签。
  • Rationale: 用户明确选择"统一进列表,前端通过字段区分";OR 单查询天然去重(不会一单两行)。
  • Alternatives: 收件视角留在闪送独立页面——用户否决。

D5: 统一卡片 DTO——公共字段 + 两源渲染块

  • Decision: 公共字段(sourceType/role/orderId/orderNo/unifiedStatus/createTime毫秒/amount/currency/hasAfterSale)+ 外卖块(type/state/deliveryStatus/payStatus/payType/afterSaleStatus 原值 + storeName)+ 闪送块(对齐 FlashDeliveryUserOrderListView 字段子集:status/serviceType/deliveryType/vehicleType/deliveryMode/预约窗/取收地址/packageType/quantity/tipAmount 等)。前端按 sourceType 复用现有两种卡片渲染。
  • Rationale: 状态族原值透传,前端现有渲染逻辑(数值/枚举→文案映射)零改动;详情跳转键外卖用业务单号 ddId、闪送用订单 ID/单号,直达现有详情接口。
  • Alternatives: 后端把状态渲染成文案——与现有"前端 i18n 渲染"分工不符,否决。

D6: 范围裁剪——不做角标、旧接口不动、只做用户端

  • Decision: 不做 tab 数量角标(未提出,YAGNI,后续可加 counts 接口);现有外卖/闪送两个列表接口原样保留(其它入口在用);商家/骑手端不涉及。
  • Rationale: 范围控制;旧接口零回归是 FR-008 的验证项。

D7: 索引——上线前核实,缺则补(唯一条件性 SQL)

  • Decision: 探索确认 flash_delivery_order 现有索引仅 PK + uk(order_no) + uk(user_id, client_request_id),缺 (user_id, create_time) 与 (receiver_user_id) 索引;pos_order 建表 DDL 不在 sql.md(历史表),索引需在库上核实。补索引语句写入 updatesql/sql.md 并标注"核实后执行",不直接执行。
  • Rationale: 读时聚合的每路查询 = WHERE user/角色 = ? AND 时间游标条件 ORDER BY 时间 DESC LIMIT n,有索引才是索引扫描;个人列表行数不大,无索引也能跑但属于隐患。备注:(user_id, client_request_id) 唯一索引的 user_id 前缀可部分复用,但不含 create_time 排序。
  • Alternatives: 不核实不补——性能隐患留坑,否决。

D8: 每路分页实现——MP Page<>(1, limit, false) 固定第一页

  • Decision: 游标推进等价于"永远查第一页",每路 new Page<>(1, size+1, false)(关闭 count 查询),取 size+1 行判定 hasMore。
  • Rationale: 游标模式下页码恒为 1;关 count 省一半查询;hasMore 由第 size+1 行是否存在判定。
  • Alternatives: PageHelper.startPage——项目新代码已统一 MP 分页(探索结论 7),且 PageHelper 的 count 对本场景多余,否决。

探索报告关键事实存档(实现时引用)

  • 外卖列表接口:GET /system/userOrder/orderList(UserOrderController.java:536-601),tab 条件 555-582 行;返回 MP Page 原始实体。
  • 闪送列表接口:GET /system/flashDelivery/orders(FlashDeliveryUserController.java:55-62 → FlashDeliveryApplicationService.userOrders),无 tab,role=sender/receiver。
  • 状态字段:外卖 state(0待处理1已接单2已出餐3已完成4已取消) + deliveryStatus(0/1/2/3) + payStatus(0未付1已付2已退) + afterSaleStatus(0-6);payType 常量 OrderLifecycleService.java:33-40(OFFLINE=1/OMG=2/LINE=3/CASH=4/APPLE_PAY=5/TRANSFER=6)。闪送 FlashDeliveryStatus 六态枚举。
  • 两表主键均为独立 AUTO_INCREMENT BIGINT,数值必然重叠,全局键必须带 source。
  • 外卖业务单号 ddId 为客户端传入 String;闪送 orderNo 服务端生成(FD+UUID24)。
  • 时间字段:外卖 cretim、闪送 create_time,均 Date(秒级)。