Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8903afdc82 | ||
|
|
fb85a56444 | ||
|
|
5f81b18388 | ||
|
|
f828cc0bd9 |
@@ -103,3 +103,4 @@ Prisma Postgres 最佳实践: prisma-postgres 技能
|
||||
- **跨套件分页断言要圈定夹具**:真实库上测"列表排序"时全库数据可能远超 pageSize,夹具根本进不了第一页;给夹具商品名加唯一前缀 + `keyword` 过滤圈定,断言既稳定又能看到完整顺序。
|
||||
- **"绝对排序键 + 子集过滤"模式**:需要"任意筛选组合下顺序都正确"时,给每条数据算好一组绝对排序键(如 国家→二级→款→priority,缺失沉底),筛选只做子集过滤不做特殊排序分支——比每个筛选组合写一套 orderBy 逻辑可靠得多。
|
||||
- **"代表行选取收敛为单一方法"**:多端点共享同一实体视图(如族化商品对外的代表行)时,代表行选取规则必须收敛为一个私有方法供所有端点复用,禁止各端点各自 `goods[0]`——否则排序参数不同会导致 goodName/主图等字段在列表与详情间漂移。
|
||||
- **"物化数组的顺序必须有确定性来源"**:把聚合结果物化进 JSONB(如 price_matrix 的 printCounts/crafts/logistics)时,"Set 去重保首现序"不是顺序保证——源查询没有 orderBy 时 Postgres 不承诺返回序,同一数据两次重算顺序可能漂移。必须:① 聚合后按业务词表序稳定排序(词表外自由文本沉底);② 源查询显式 `orderBy` 固定遍历序。测试用"打乱输入顺序断言输出全等"覆盖顺序确定性。
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { FamilyRecomputeService, derivePriceMatrix } from './family-recompute.service';
|
||||
import {
|
||||
FamilyRecomputeService,
|
||||
derivePriceMatrix,
|
||||
memberMatrixCombos,
|
||||
} from './family-recompute.service';
|
||||
import { ProductFamiliesService } from './product-families.service';
|
||||
import { OrganizeService } from './organize.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -441,7 +445,7 @@ describe('derivePriceMatrix 选项组排序', () => {
|
||||
it('幂等:同输入多跑两遍输出全等(含顺序),与成员遍历顺序无关', () => {
|
||||
const members = [
|
||||
mkMember(1, { tags: ['直喷', '双面印花', '不包邮'] }),
|
||||
mkMember(2, { source: 'CUSTOM', craftLabel: '丝印', logisticsLabel: '海运' }),
|
||||
mkMember(2, { source: 'CUSTOM', tags: ['双面印花'], craftLabel: '丝印', logisticsLabel: '海运' }),
|
||||
mkMember(3, { tags: ['烫画', '单面印花', '包邮'] }),
|
||||
];
|
||||
const first = derivePriceMatrix(members, []);
|
||||
@@ -451,3 +455,47 @@ describe('derivePriceMatrix 选项组排序', () => {
|
||||
expect(JSON.stringify(reversed)).toBe(JSON.stringify(first));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 印刷工艺新增"热转印":矩阵维度词表放开准入 + 展示序追加末尾。
|
||||
* 自动派生规则不改 —— SDS 链接只能人工打标;不打标的链接/族零影响。
|
||||
*/
|
||||
describe('热转印工艺', () => {
|
||||
const mkMember = (
|
||||
id: number,
|
||||
over: { tags?: string[]; source?: 'SDS' | 'CUSTOM'; craftLabel?: string | null; logisticsLabel?: string | null },
|
||||
) =>
|
||||
({
|
||||
id: BigInt(id),
|
||||
source: over.source ?? 'SDS',
|
||||
originGoodTags: (over.tags ?? []).map((t) => ({ tag: { tagName: t } })),
|
||||
variants: [],
|
||||
craftLabel: over.craftLabel ?? null,
|
||||
logisticsLabel: over.logisticsLabel ?? null,
|
||||
}) as any;
|
||||
|
||||
it('memberMatrixCombos:打标热转印的链接产出矩阵组合(此前被词表过滤为空)', () => {
|
||||
const combos = memberMatrixCombos({ tagNames: ['单面印花', '热转印', '包邮'] });
|
||||
expect(combos).toEqual([{ printCount: '单面印花', craft: '热转印', logistics: '包邮' }]);
|
||||
});
|
||||
|
||||
it('derivePriceMatrix:crafts 展示序为 烫画→直喷→不打印→热转印(词表内,不再沉底)', () => {
|
||||
const members = [
|
||||
mkMember(1, { tags: ['热转印', '单面印花', '包邮'] }),
|
||||
mkMember(2, { tags: ['烫画', '单面印花', '包邮'] }),
|
||||
mkMember(3, { tags: ['直喷', '单面印花', '不包邮'] }),
|
||||
mkMember(4, { tags: ['不打印', '单面印花', '包邮'] }),
|
||||
];
|
||||
const m = derivePriceMatrix(members, []);
|
||||
expect(m.crafts).toEqual(['烫画', '直喷', '不打印', '热转印']);
|
||||
});
|
||||
|
||||
it('词表外自由文本(丝印/水洗)仍然沉底,与热转印区分', () => {
|
||||
const members = [
|
||||
mkMember(1, { tags: ['热转印', '单面印花', '包邮'] }),
|
||||
mkMember(2, { source: 'CUSTOM', tags: ['单面印花'], craftLabel: '丝印', logisticsLabel: '包邮' }),
|
||||
];
|
||||
const m = derivePriceMatrix(members, []);
|
||||
expect(m.crafts).toEqual(['热转印', '丝印']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,20 +66,20 @@ type OverrideRow = Prisma.FamilyPriceOverrideGetPayload<{}>;
|
||||
/** 矩阵三个归因维度的合法取值(封闭集合,与派生标签组一致) */
|
||||
const DIM_VALUES = {
|
||||
printCount: ['单面印花', '双面印花'],
|
||||
craft: ['烫画', '直喷', '不打印'],
|
||||
craft: ['烫画', '直喷', '不打印', '热转印'],
|
||||
logistics: ['包邮', '不包邮'],
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
|
||||
type DimKey = keyof typeof DIM_VALUES;
|
||||
|
||||
/**
|
||||
* 选项组展示顺序(业务确认):printCount 单面→双面;craft 烫画→直喷→不打印;logistics 不包邮→包邮。
|
||||
* 选项组展示顺序(业务确认):printCount 单面→双面;craft 烫画→直喷→不打印→热转印;logistics 不包邮→包邮。
|
||||
* 与 DIM_VALUES 的"组合生成序"是两回事,物化前对聚合数组按此稳定排序;
|
||||
* 词表外自由文本(CUSTOM 标签,如"海运")沉底,相互间保持首次遇到序。
|
||||
*/
|
||||
const OPTION_DISPLAY_ORDER = {
|
||||
printCount: ['单面印花', '双面印花'],
|
||||
craft: ['烫画', '直喷', '不打印'],
|
||||
craft: ['烫画', '直喷', '不打印', '热转印'],
|
||||
logistics: ['不包邮', '包邮'],
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Feature:印刷工艺新增"热转印"选项
|
||||
|
||||
日期:2026-09-02
|
||||
类型:新功能(feature)
|
||||
影响面:`apps/api/src/product-families/family-recompute.service.ts`(两处词表常量)+ 测试 + `tags` 表新增一行
|
||||
|
||||
## 背景
|
||||
|
||||
业务需要新增印刷工艺"热转印"。标签由 `tags` 表驱动(`GET /tags` 全量返回,管理端打标用),但价格矩阵的维度词表在后端硬编码,需同步放开准入。
|
||||
|
||||
## 现状(2026-09-02 分析确认)
|
||||
|
||||
- `tags` 表:印刷工艺组(tag_group_id=3)现有 烫画(1)/直喷(2)/不打印(3),有 `POST /tags` 接口(JwtAuthGuard,重名 409)
|
||||
- 矩阵维度词表 [DIM_VALUES.craft](file:///opt/inkreach-v2/apps/api/src/product-families/family-recompute.service.ts) = `['烫画','直喷','不打印']` 硬编码,`memberMatrixCombos` 按它过滤链接标签——词表外的标签会导致链接不进矩阵
|
||||
- 展示序词表 `OPTION_DISPLAY_ORDER.craft` 同样硬编码,词表外沉底
|
||||
- 名称自动派生规则(`auto-tag-rules.ts` / admin `origin-name.ts`)**本次不改**:SDS 链接不会自动派生热转印,只能人工打标
|
||||
|
||||
## 方案
|
||||
|
||||
1. `DIM_VALUES.craft` 追加 `'热转印'`(末尾)——矩阵认可该工艺标签,SDS/CUSTOM 链接人工打标后均可进矩阵
|
||||
2. `OPTION_DISPLAY_ORDER.craft` 追加 `'热转印'`(末尾)——按钮组显示在 不打印 之后(业务确认排序表先不改,放最后)
|
||||
3. `POST /tags { tagName: '热转印', tagGroupId: '3', sortOrder: 4 }` 创建标签行(管理端标签栏立即可见/可选)
|
||||
4. **不给任何存量链接打标**——纯增量能力,存量族矩阵零变化
|
||||
|
||||
## 明确不做
|
||||
|
||||
- 不改自动派生规则(名称含"热转印"仍派生默认烫画,如需后续再议)
|
||||
- 不给存量链接打标、不重算存量族
|
||||
- 不动 自建商品 craftLabel 维度填错的历史数据
|
||||
|
||||
## 测试计划(TDD)
|
||||
|
||||
测试文件:`family-recompute.service.spec.ts`(memberMatrixCombos / derivePriceMatrix 纯函数)
|
||||
|
||||
1. `memberMatrixCombos`:链接标签 [单面印花, 热转印, 包邮] → 产出 `{ printCount:'单面印花', craft:'热转印', logistics:'包邮' }`(修复前被词表过滤 → combos 为空,RED)
|
||||
2. `derivePriceMatrix`:crafts 输出 `烫画→直喷→不打印→热转印`(热转印作为词表内值参与排序,不再沉底)
|
||||
3. 既有"未知自由文本沉底"用例(丝印/水洗)仍应沉底——热转印是词表内,丝印/水洗仍是词表外
|
||||
4. 全量 jest + tsc 通过
|
||||
|
||||
## 线上验证
|
||||
|
||||
1. `GET /tags` 返回 热转印(tagGroupId=3, sortOrder=4)
|
||||
2. 抽查存量族 priceMatrix.crafts 与改动前一致(无新增值)
|
||||
3. 无链接打标 → 公开详情 crafts 不含 热转印
|
||||
|
||||
## 回滚
|
||||
|
||||
- 代码:revert 两行常量(未打标状态零影响)
|
||||
- 标签:无引用可直接删(`origin_good_tags` 无该行)
|
||||
Reference in New Issue
Block a user