# 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( 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 即可。