Files
inkreach-official-website/plans/fix/public-goodname-representative-fix.md
T
yeuimu 63820a3e6f fix(public): unify family representative selection with detail endpoint
List/home endpoints picked the family representative row via goods[0],
which drifted with the list sort parameter (cheapest under PRICE_ASC,
lowest id under DEFAULT ties), causing goodName and other
representative-derived fields to differ from the detail endpoint.
Extract pickFamilyRepresentative (goodPriority desc -> createdAt desc
-> id asc, same as getGoodByFamilyId) and use it in getGoods and
getHomeGoods grouping. Group ordering and API contracts unchanged.
2026-09-02 14:58:42 +08:00

3.3 KiB
Raw Blame History

Fix:公开接口族代表行选取规则统一(列表/首页对齐详情)

日期:2026-09-02 类型:Bug 修复(fix 影响面:apps/api/src/public/public.service.ts 及其测试

背景与问题

公开接口中,族(productFamily)对外只暴露一条"代表行",goodName、主图、国家、分类、标签、goodPrioritycreatedAt 等字段全部来自代表行。但三个接口的代表行选取规则不一致:

接口 代表行选取规则 位置
详情 GET /public/goods/{goodId} goodPriority desc → createdAt desc → id asc getGoodByFamilyId
列表 GET /public/goods goods[0](随列表排序参数变化:DEFAULT 平局取 id 最小;价格排序取价格极值行;NEWEST 取最新行) getGoods 分组处
首页 GET /public/home-goods goods[0](按 position 顺序的第一条) getHomeGoods

后果:同一族内多条 Good 名称不同时,列表返回的 goodName 与详情不一致;且列表换排序参数后名称还会变。

目标

  • 列表与首页的族代表行选取规则与详情完全一致:goodPriority desc → createdAt desc → id asc
  • 列表排序逻辑(DEFAULT 树序 / PRICE / NEWEST)与分组顺序完全不变。
  • 公开 API 输入输出数据结构不变(硬性约束)。

非目标

  • 不改动详情逻辑、族去重契约、树序排序实现。
  • 不处理无族(自定义商品)——其分组内只有一条,不受影响。

方案(已确认:方案 A + 首页一起改)

public.service.ts 中新增私有方法,组内显式选取代表行,getGoodsgetHomeGoods 分组处统一调用:

/** 族代表行:与详情 getGoodByFamilyId 的 orderBy 保持一致
 *  goodPriority desc → createdAt desc → id asc),保证列表/首页/详情字段一致 */
private pickFamilyRepresentative<T extends { goodPriority: number | null; createdAt: Date; id: bigint }>(
  goods: T[],
): T {
  return [...goods].sort(
    (a, b) =>
      (b.goodPriority ?? 0) - (a.goodPriority ?? 0) ||
      b.createdAt.getTime() - a.createdAt.getTime() ||
      (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
  )[0];
}
  • getGoodsconst rep = this.pickFamilyRepresentative(goods);
  • getHomeGoods:同上替换 goods[0]
  • 组序不受影响:同族成员共享国家/款,最高 goodPriority 相同,仅平局细则变化,树序键 c1/c2/c3 与 max(priority) 均不变。

测试计划(TDD

测试文件:apps/api/src/public/public.service.spec.ts(沿用现有夹具风格)。

  1. 列表 vs 详情一致性:构造同族两条 GoodgoodPriority 相同、createdAt 不同、名称不同 → GET /public/goods 列表项的 goodName === 详情接口返回的 goodName
  2. 优先级优先:族内两条 Good 优先级不同 → 列表代表行取优先级最高的那条(即使它 id 更大/创建更早)。
  3. 价格排序下名称不漂移sort=PRICE_ASC 时,价格较低但优先级低的成员不是代表行,列表 goodName 与 DEFAULT 排序一致。
  4. 首页一致性:位置配置同族多条 → 首页返回的 goodName 与详情一致。
  5. 既有用例全部保持通过。

风险与回滚

  • 纯内存选取逻辑变更,无 schema/数据变更,无部署数据风险。
  • 回滚:还原 commit 即可。