# 公开契约族化：goodId = 族ID + 五维价格矩阵

## 背景 / 动机

- 现状 `GET /public/goods/{goodId}` 以 SDS 链接 ID 寻址，`findFirst` 任选一条 Good；
  一个族在库内常配置多条 Good（92 个已配置族中 78 个是多条，如加拿大 CATM001 一族 4 条），
  列表因此出现同款重复，前端被迫理解"族背后有多少商品"。
- 价格矩阵把 印花数量+工艺 混在一个 `craft` 维（原始链接名第 3 段，含
  `直喷单面`/`光板不打印`/解析噪声，且 47 个成员为 null），不是严格的五维。

目标契约（用户拍板）：**前端只知道款号（族）**——

- `goodId` = 族 ID；前端不知道族背后有几条 Good / 几个链接
- 详情 = 款的公共信息（名称、主图、国家、分类、商品详情文案、尺码表、包装规格）
  + 价格矩阵
- 价格严格五维：**尺码 × 颜色 × 物流 × 工艺 × 印花数量 → 价格**

## 事实核查（2026-08-28）

- 0 个族跨国家/跨分类 → 国家、分类可作为族的公共属性
- `family_price_overrides` 0 行 → 加维度无数据回填负担
- 无族 Good 6 条（source=CUSTOM）→ 必须保留 sdsGoodId 寻址回退
- 链接级标签已全覆盖（536/536），三组派生标签是矩阵维度的可靠来源

## 方案

### A. priceMatrix 五维化（family-recompute.service.ts）

- `PriceMatrixRow` 增加 `printCount`；`PriceMatrix` 增加 `printCounts: string[]`
- 成员维度来源改为**有效标签**（OriginGoodTag ∋ 物流渠道/印花数量/印刷工艺三组；
  人工接管时用人工标签），三组各自取值做笛卡尔（通常 1×1×1）；任一组缺失则该成员不进矩阵
- 弃用 `craftLabel` 作为维度（保留字段用于展示回退）
- 同格子多来源取最低价、sources 全保留、人工覆盖并入 —— 规则不变

### B. FamilyPriceOverride 加 printCount（prisma migration）

- 新列 `print_count`（默认 `单面印花`，迁移时 0 行数据）
- 唯一键扩展为 (familyId, sizeId, colorId, printCount, craft, logistics)

### C. product-families.service 覆盖价三处五键化

- `PriceOverrideItemDto` + `putPriceOverrides` 校验（allowed.printCounts）
- `listPriceOverrides` 行匹配五键

### D. public 契约族化（public.service.ts）

1. `getGoods`：过滤条件不变（作用于 Good），**取全量匹配后内存按族分组**
   （规模 241 条 Good，注释说明；无族 Good 各自成组）。
   - 组代表 = 现有排序的第一条（priority desc → createdAt desc）
   - `goodId`：有族 → `familyId`；无族 → `sdsGoodId`（自定义商品）
   - `price`：族 minPrice（批量查 priceMatrix），回退 og price
   - `total` = 分组数，分页在分组后
2. `getGood(id)`：**纯数字先按族 ID 解析**（族 + 其 Good + 成员变体并集），
   未命中再回退 sdsGoodId 旧路径（自定义商品/向后兼容）。
   族详情：公共字段取代表 Good；detail 文案取主链接；尺码表/包装规格用族并集；
   media/mediaByColor/variants 全成员并集；`family` 块 = 五维矩阵 + minPrice。
   族下无在售 Good → 404。`PUBLIC_DETAIL_FROM_FAMILY=false` 应急开关行为不变。
3. `getHomeGoods`：按族去重（排序不变，同族保留最前一条）。

### E. admin 适配（GoodsEditDialog.vue）

- SKU 列：印花数量列直接读 `row.printCount`（不再从 craft 字符串猜测）
- `loadMemberPrices`：按 printCount+craft+logistics 三键匹配该成员格子；
  维度优先取成员 `originGoodTags`（人工接管后仍准确），回退 `linkDims(goodName)`
- 保存覆盖价 payload 带 printCount

### F. 存量族全量重算

一次性脚本：对所有 ProductFamily 逐个 `recomputeFamily`（矩阵物化 JSON 结构变更）。

### G. 测试与文档

- family-recompute spec：五维矩阵（含 人工标签 覆盖派生、缺组跳过、跨成员最低价）
- product-families spec：覆盖价五键校验
- public spec：列表按族去重（同族 N 条 → 1 条，goodId=familyId）、getGood(族ID)、
  自定义商品回退、home-goods 去重
- public-family-block spec：矩阵 printCounts
- admin：vue-tsc + 现有 22 测试
- 文档：docs/references/product-center.md、structs.md

## 风险 / 边界

- 数字族 ID 与数字 sdsGoodId 理论碰撞：族 ID 优先（sdsGoodId 为 SDS 系统 6 位数，
  实际不碰撞），Swagger 注明
- 列表内存分组在商品量级 10⁴ 前可接受，注释留优化方向（物化族表）
- 旧前端（website）以 sdsGoodId 寻址仍可达（回退路径），后续 website 侧再切换
