# 产品中心

官网产品中心位于 `apps/website/app/pages/product-center.vue`，访问路径为 `/product-center`。

## 默认展示

页面首次加载时展示全部商品和全部国家，不默认选择分类、物流或工艺标签。每页默认请求 20 条商品，用户操作筛选项后再按真实业务 ID 查询。

## 筛选与分页

- 左侧分类树：选择父分类或子分类。
- 国家：点击国家胶囊筛选，点击“全部”取消国家筛选。
- 物流与工艺：按标签组展示下拉选项，每个分组内暂为单选，不同分组可各选择一项。
- 筛选菜单在选择选项或点击组件外部后自动关闭；已选 Tag 使用独立标签行。仅选择工艺时标签从最左侧开始；物流与工艺同时选择时按物流、工艺顺序与上方筛选项对应排列。
- 搜索：输入关键词后按回车或点击“搜索”。
- 商品卡：整卡链接到 `https://inkpod.vip/portal/detail/{商品ID}`，在新窗口打开。
- 公共商品接口的 `id` 来自 `originGood.sdsGoodId`，即 InkPOD 商品 ID；`goods.good_id` 仅作为本站数据库内部主键使用。
- URL 国家筛选：支持 `/product-center?countryId={后台国家ID}`，仅接受后台已配置的国家 ID。
- 无结果：展示设计稿空状态，可通过“查看全部商品”或“清空筛选”恢复完整列表。
- 加载中：展示两行共八个商品骨架卡，筛选栏保持可操作。
- 分页：支持页码、前后翻页、每页数量和页码跳转。

所有查询通过 `useProductCenter.ts` 调用 `/api/backend/*` 代理路由，不在页面中硬编码业务 ID。

## 后台配置联动

- 品类树、国家、Tag 与 Tag Group 均来自 NestJS `/public/*` 接口。
- 国家旗帜读取 `countryIcon`，品类图标读取 `categoryIcon`；前端不提供外部旗帜 CDN 或固定名称兜底。
- `/uploads/*` 与 `/assets/*` 等后台相对资源地址由 `useProductCenter.ts` 根据 `runtimeConfig.public.backendUrl` 转为完整 URL。
- 物流和工艺筛选分别由后台名称包含“物流”和“工艺”的 Tag Group 生成；“印刷位置”等其他分组仍可用于商品卡片标签，但不会自动成为顶部筛选项。
- Figma 基准图标位于 `apps/api/public/product-center/`，通过后端 `/assets/product-center/*` 提供。
- 更新现有数据库图标配置：`pnpm --filter @inkreach/api configure:product-center-icons`。脚本按中文名称幂等更新，不创建缺失的国家或品类。

## 响应式

桌面采用 240px 分类栏，页面左右保留 `24–56px` 自适应留白，分类栏可滚动但隐藏可见 scrollbar。商品网格以 240px 为最小卡片宽度自动增加或减少列数：1440px 基准宽度保持四列，1920px 可显示六列。国家筛选区按内容自然增高，不保留固定空白。

左侧品类栏不显示最左侧边线，一级品类之间保留 10px 间距；国家筛选区及物流、工艺、搜索工具区的底部分隔线从内容区向右延伸至页面边缘。

宽度低于 1000px 后分类栏变为抽屉；移动端商品网格降为两列或单列，国家筛选仅在自身区域横向滚动，不会撑宽页面。

## 多源合并商品（已废弃，由产品族替代）

> **2026-08-28 起，该机制已被「产品族」完全替代**：多链接合并由族承载（尺码/包装并集 +
> 五维价格矩阵），配置/编辑商品不再写入 `mergedOriginGoodIds`；`good_origin_goods` 表
> 只读保留（历史数据仍可命中详情），观察期后删除。以下为历史行为记录：

- 数据层：主源存 `goods.origin_good_id`，副源存中间表 `good_origin_goods`。
- 详情可达性：官网上通过**任一**关联原产品的 `sdsGoodId` 都能访问到该商品详情（族机制下
  同样成立：族内任何成员链接的 sdsGoodId 均命中同一商品）。

> 该 Good 级合并能力保留可用；新一级的「产品族（SPU）合并」见下节，公开读路径接入族数据属于三期范围。

## 产品族（SPU 层，一期后端已上线）

设计文档：`docs/superpowers/specs/2026-08-28-product-family-merge-design.md`。

**模型**：SDS 叶子分类即产品模型（如分类 `DG001 180G纯棉T恤（JSA002）` 下 16 条链接），
族 = 同分类链接的合并体；链接名解析列（`国家（物流）品名-SKU-工艺[-仓库]`）提供
物流/工艺归因。例：DG001 族 = 16 链接 × 3 工艺 × 3 物流 × 9 尺码（S~XXXXXL 并集）× 20 颜色 ≈ 470 格价格矩阵，起价取光板（不打印）最低价。

**价格**：`价格 = f(尺码, 颜色, 物流, 工艺[含印花数量])`，全部从成员链接变体推导透传
（同格子多仓库取最低价，来源全保留）；`family_price_overrides` 表支持按格人工改价，
删覆盖即恢复推导价。

**关键端点用法示例**（JWT）：

```bash
# 1. 自动成族：先预览
curl -X POST /product-families/auto-group -H "Authorization: Bearer $T" \
  -d '{"apply": false}'
# → { applied: 0, groups: [{ groupKey: "cat:8240", familyName: "DG001 180G纯棉T恤（JSA002）", memberCount: 16, ... }] }

# 2. 应用（幂等，可重复执行）
curl -X POST /product-families/auto-group -H "Authorization: Bearer $T" -d '{"apply": true}'

# 3. 族详情：成员 + 并集尺码表/包装 + 五维价格矩阵
curl /product-families/12 -H "Authorization: Bearer $T"

# 4. 人工改价（维度必须存在于族矩阵选项，否则 400 并列出非法键）
curl -X PUT /product-families/12/price-overrides -H "Authorization: Bearer $T" \
  -d '{"items": [{"sizeId":"size_S","colorId":"color_blk","craft":"单面印花","logistics":"包邮","price": 23, "note": "促销"}]}'

# 5. 删覆盖恢复推导价
curl -X DELETE /product-families/12/price-overrides -H "Authorization: Bearer $T" \
  -d '{"cells": [{"sizeId":"size_S","colorId":"color_blk","craft":"单面印花","logistics":"包邮"}]}'

# 6. 族内创建自定义商品（人工补链接，物流/工艺归因必填）
curl -X POST /product-families/12/members/custom -H "Authorization: Bearer $T" \
  -d '{"goodName":"美国（海运）180g纯棉T恤-DG001-烫画","logisticsLabel":"海运","craftLabel":"烫画",
       "variants":[{"sku":"C-M","sizeId":"size_M","sizeName":"M","colorId":"color_red","colorName":"红色","price": 33}]}'
```

**同步联动**：商品同步落库时刷新解析列；新链接与已有族**同分类唯一命中**时自动挂族
（多族/零族留给管理员；锁定族只置 `stale`）；详情同步提交后异步重算受影响族
（进程内去重、幂等）。回填/修复脚本：

```bash
pnpm --filter @inkreach/api backfill:product-families
# → [1/2] 解析全部链接名 [2/2] 自动建族 [3/3] 全量族重算（幂等，可随时重跑）
```

**公开读路径（三期，默认开启）**：`PUBLIC_DETAIL_FROM_FAMILY=false` 可应急回退旧行为。
`GET /public/goods/:goodId` 行为：

- **变体并集（替代旧副源机制）**：`variants` = 主链接 ∪ 全体族成员 ∪ 旧副源（过渡期），
  按 `(链接, 变体)` 去重；`mediaByColor` 同源。任何族成员链接的 sdsGoodId 均命中同一商品。
- **族块**（默认输出）：`family` 含并集尺码表/包装 + 五维价格矩阵 + 族起价：

```jsonc
{
  "family": {
    "familyId": "12", "familyCode": "DG015", "familyName": "DG015 180G纯棉T恤",
    "sizes": [...], "colors": [...], "crafts": ["单面印花", ...], "logistics": ["包邮", ...],
    "sizeChart": { /* 并集 */ }, "packageSpecs": { /* 并集 */ },
    "priceMatrix": { /* 五维矩阵：rows 为 {sizeId, colorId, craft, logistics, price, manual, sources} */ },
    "minPrice": "29.5"
  }
}
```

实测（DG015 族）：4 工艺 × 2 物流 × 8 尺码 × 12 颜色 = 653 格矩阵，族起价 ¥29.5
（主源单链接价为 ¥68.74——族视角展示了光板/单面的更低档价格）。前端本地按五维联动
`priceMatrix` 即可实时算价，无需新增查价端点（设计 D4）。

**边界声明**：商品列表/筛选/排序仍基于主源 `goodPrice`（SQL 层无法廉价解析族矩阵 JSON，
且避免展示价与筛选价不一致）；`good_origin_goods` 转只读保留，观察期后另行删除。

**后台操作入口（二期已上线）**：商品中心新增「产品族」标签页（`/goods?tab=families`）：


- 列表：关键词/分页、成员数、人工改价数、自动托管/锁定状态、`stale` 待处理标记；
- 「自动成族」：先预览候选分组（SDS 分类聚合），确认后应用并逐族重算；
- 详情抽屉：canonical 字段编辑（名称/编码/主图/主链接）、`autoManaged` 开关、手动重算、
  成员增删（远程搜索无族链接）、族内新建自定义成员；
- 价格矩阵：按 (工艺 × 物流) 切换页签，行=尺码、列=颜色；点击任意格行内改价
  （人工格高亮并带「改」标），下方覆盖列表展示推导价对照与差额，删除即恢复推导价；
- 商品配置页（GoodsView）：右侧原产品树节点带族编码徽标与 `族待处理` 提示；
  配置商品时兄弟链接的自动勾选改为**按族匹配**（familyId 相同，无族时回退名称 3 段规则）。



## 验证

```bash
pnpm --filter @inkreach/website test --run
pnpm --filter @inkreach/website build
pnpm --filter @inkreach/website dev
pnpm --filter @inkreach/api test -- --runInBand
pnpm --filter @inkreach/api build
```
