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

68 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 分组处统一调用:
```ts
/** 族代表行:与详情 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`(沿用现有夹具风格)。
1. **列表 vs 详情一致性**:构造同族两条 Good`goodPriority` 相同、`createdAt` 不同、名称不同 →
`GET /public/goods` 列表项的 `goodName` === 详情接口返回的 `goodName`
2. **优先级优先**:族内两条 Good 优先级不同 → 列表代表行取优先级最高的那条(即使它 id 更大/创建更早)。
3. **价格排序下名称不漂移**`sort=PRICE_ASC` 时,价格较低但优先级低的成员不是代表行,列表 `goodName` 与 DEFAULT 排序一致。
4. **首页一致性**:位置配置同族多条 → 首页返回的 `goodName` 与详情一致。
5. 既有用例全部保持通过。
## 风险与回滚
- 纯内存选取逻辑变更,无 schema/数据变更,无部署数据风险。
- 回滚:还原 commit 即可。