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.
3.3 KiB
3.3 KiB
Fix:公开接口族代表行选取规则统一(列表/首页对齐详情)
日期:2026-09-02
类型:Bug 修复(fix)
影响面:apps/api/src/public/public.service.ts 及其测试
背景与问题
公开接口中,族(productFamily)对外只暴露一条"代表行",goodName、主图、国家、分类、标签、goodPriority、createdAt 等字段全部来自代表行。但三个接口的代表行选取规则不一致:
| 接口 | 代表行选取规则 | 位置 |
|---|---|---|
详情 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 中新增私有方法,组内显式选取代表行,getGoods 与 getHomeGoods 分组处统一调用:
/** 族代表行:与详情 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];
}
getGoods:const rep = this.pickFamilyRepresentative(goods);getHomeGoods:同上替换goods[0]。- 组序不受影响:同族成员共享国家/款,最高
goodPriority相同,仅平局细则变化,树序键 c1/c2/c3 与 max(priority) 均不变。
测试计划(TDD)
测试文件:apps/api/src/public/public.service.spec.ts(沿用现有夹具风格)。
- 列表 vs 详情一致性:构造同族两条 Good,
goodPriority相同、createdAt不同、名称不同 →GET /public/goods列表项的goodName=== 详情接口返回的goodName。 - 优先级优先:族内两条 Good 优先级不同 → 列表代表行取优先级最高的那条(即使它 id 更大/创建更早)。
- 价格排序下名称不漂移:
sort=PRICE_ASC时,价格较低但优先级低的成员不是代表行,列表goodName与 DEFAULT 排序一致。 - 首页一致性:位置配置同族多条 → 首页返回的
goodName与详情一致。 - 既有用例全部保持通过。
风险与回滚
- 纯内存选取逻辑变更,无 schema/数据变更,无部署数据风险。
- 回滚:还原 commit 即可。