merge: refactor/v2 完全并入 develop(v2 为准,v2 血统 20+ 提交收敛为主干)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# Fix:价格矩阵选项组按词表序输出(印花/工艺/物流按钮顺序稳定)
|
||||
|
||||
日期:2026-09-02
|
||||
类型:Bug 修复(fix)
|
||||
影响面:`apps/api/src/product-families/family-recompute.service.ts` 及其测试、一次性全库重算
|
||||
|
||||
## 背景与问题
|
||||
|
||||
详情接口 `family.priceMatrix` 的 `printCounts / crafts / logistics` 是物化进 JSONB 的字符串数组,H5 按数组原序渲染印花/工艺/物流按钮组(无前端排序)。数组顺序 = 重算时 Set 去重的"首次遇到序",且成员查询 `originGoods` 无 `orderBy`,导致:
|
||||
|
||||
1. 每个族的按钮顺序不一致(实测 goodId=1 为 `['双面印花','单面印花']`、`['不包邮','包邮']`,与词表相反);
|
||||
2. 同一族两次重算顺序理论上可能漂移(Postgres 无顺序保证)。
|
||||
|
||||
对比:尺码/颜色按钮组有 `variants.sortOrder`,顺序稳定;本修复把印花/工艺/物流对齐到同样可预期。
|
||||
|
||||
## 目标顺序(业务确认)
|
||||
|
||||
| 维度 | 展示顺序 |
|
||||
| --- | --- |
|
||||
| printCount | 单面印花 → 双面印花 |
|
||||
| craft | 烫画 → 直喷 → 不打印 |
|
||||
| logistics | 不包邮 → 包邮 |
|
||||
|
||||
无论族内实际出现哪个子集,相对顺序恒定;词表外自由文本(如 CUSTOM 的"海运")沉到最后,相互间保持首次遇到序。
|
||||
|
||||
## 数据边界(2026-09-02 全库实测)
|
||||
|
||||
- crafts 中出现 `双面印花`×2、logistics 中出现 `海运`×2:来自 3 条 CUSTOM 链接 craftLabel/logisticsLabel 自由文本(含维度填错,属数据清理问题,本修复只保证其沉底,不改数据)。
|
||||
|
||||
## 方案
|
||||
|
||||
1. `derivePriceMatrix`:聚合(Set 去重)+ 人工覆盖 append 之后,对三个数组做稳定排序:
|
||||
- 新增 `OPTION_DISPLAY_ORDER` 常量(printCount/craft/logistics 三组词表序,logistics 与 `DIM_VALUES` 顺序相反是业务要求;`DIM_VALUES` 组合生成语义不动);
|
||||
- 排序键 = 词表下标,未知值 = `canon.length`(沉底),稳定排序保持未知值间首次遇到序。
|
||||
2. `recomputeFamily` 成员查询加 `orderBy: { id: 'asc' }`,使"遇到序"确定(未知值之间顺序也确定)。
|
||||
3. 只影响 `autoManaged` 族物化;非自动族人工字段照旧不碰(现有不变量)。
|
||||
4. 一次性脚本:逐族调用 `recomputeFamily` 全库刷新存量(166 个族)。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改 API 输出结构(仍是 `string[]`),前端零改动;
|
||||
- 不清理 CUSTOM 维度填错数据(另行处理)。
|
||||
|
||||
## 测试计划(TDD)
|
||||
|
||||
测试文件:`apps/api/src/product-families/family-recompute.service.spec.ts`(`derivePriceMatrix` 为纯函数,可直接单测)。
|
||||
|
||||
1. **打乱输入仍输出词表序**:成员顺序颠倒,crafts 输出仍 `烫画→直喷→不打印`(修复前为遇到序,RED)。
|
||||
2. **子集保序**:只含 `直喷+烫画` → 输出 `烫画,直喷`;只含 `双面` → 单元素。
|
||||
3. **未知值沉底**:logistics 含 `海运` → `不包邮,包邮,海运`;未知值相互保持遇到序。
|
||||
4. **覆盖 append 值参与同一排序**:override 新增取值后数组仍词表序。
|
||||
5. **重算幂等(含顺序)**:同输入跑两遍输出 deep-equal。
|
||||
6. 既有用例全绿;全量 jest + tsc 通过。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- 纯物化顺序变化,无 schema/契约变更;回滚 = revert commit + 再跑一次全库重算(旧逻辑会把顺序"洗回"遇到序,仅顺序变化无数据损坏)。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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 即可。
|
||||
Reference in New Issue
Block a user