Files
inkreach-official-website/docs/references/product-center.md
T
yeuimu f1e81872e7 fix(product-family): auto-merge SKUs after manual tag edits; fix session cookie path
问题2(SKU 无法自动合并,GBIU017 案例)四处叠加根因修复:
- 人工标签旧词「热转印」被封闭词表静默踢出合并矩阵 → CRAFT_TAG_ALIASES
  别名归一为「烫画」(矩阵归因与 CUSTOM 标签同路径生效)
- updateTags 缺任一定价维度组即整链掉出矩阵且人工接管后永不恢复 →
  缺啥补啥(按链接名派生补齐,人工勾选值不动,响应带 filledDimensionTags)
- autoGroup 只建新族从不并入已有族(产生 USIU005-2 类碎片)→ 并入已有
  autoManaged 同款族优先,无匹配才新建;同款仅人工锁定族则跳过并报告
- 名称回退分组键含物流备注,包邮/不包邮永不同组 → 新增族语义键
  familyNameKey(国家+品名+SKU),api/admin 两侧同构,合并默认勾选随之修复

附加:
- updateTags 后未归族链接自动并入匹配族(attachToMatchingFamily,响应带
  attachedFamilyId)
- 整理新增碎片族合并 consolidateFragments:纯碎片族并入带商品族并删除
  (保公开 goodId=族ID 稳定),带商品/覆盖价/人工锁定进人工复审报告
- admin:保存标签提示补齐明细,整理完成消息含并入/碎片合并/待人工数

问题1(后台频繁 Unauthorized 掉线):
- refresh cookie path '/auth' 与代理前缀(/api、/v2-api)不匹配导致浏览器
  永远带不上 refresh cookie → 两 cookie path 统一为 '/'
- v2 构建基址带尾斜杠 + 手工拼接产生 /v2-api//auth/refresh 双斜杠 404 →
  request.ts 规范拼接

测试:api jest 187/187、admin vitest 22/22、双侧 tsc 0 错误;
public.service 夹具改为自包含(不依赖共享库既有数据)。
2026-09-02 18:39:35 +08:00

240 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 产品中心
官网产品中心位于 `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 分类栏,页面左右保留 `2456px` 自适应留白,分类栏可滚动但隐藏可见 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. 自动成族:先预览(action 标注 create=新建 / merge=并入已有族 / skip=同款仅人工锁定族)
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, action: "create", ... }] }
# 2. 应用(幂等,可重复执行)。已有同款 autoManaged 族时**并入**(不再新建 -2 碎片族);
# 同款仅有人工锁定族(autoManaged=false)时跳过该组并计入 skipped。
curl -X POST /product-families/auto-group -H "Authorization: Bearer $T" -d '{"apply": true}'
# → { applied: 新建族数, merged: 并入链接数, skipped: ["cat:..."], groups: [...] }
# 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}]}'
```
**三层架构(解析去运行时化)**
1. **同步 = 纯镜像**:SDS 给什么存什么(名称原文/图/价/分类 ID),不解析、不归族、不写解析列;
2. **整理 = 显式人工动作**(所有解析/派生集中于此,可审查可重跑):回填解析列 → 派生链接标签
(未人工接管的 SDS 链接按名称刷新;人工接管永不覆盖)→ 自动建族(**并入已有同款族优先,
无匹配才新建**)→ 碎片族合并(同分类多族时,纯碎片族并入带商品的族并删除,带商品/覆盖价/
人工锁定的进人工复审报告)→ 全量重算。返回含 `familiesMerged / fragmentsConsolidated /
fragmentsManualReview`。入口二选一:
```bash
pnpm --filter @inkreach/api organize # CLI
curl -X POST /product-families/organize -H "$AUTH" # 后台「整理」按钮(同一逻辑)
```
3. **重算 = 自动的结构化聚合**:归族/成员变化/详情同步/标签调整自动触发;输入只有
成员变体 + 链接标签 + 覆盖价,**不解析名称**——上游改名永远不会倒灌已派生的标签与矩阵;
未整理(无标签)的 SDS 成员不进矩阵,整理后齐全。
派生默认(脚本层假设,显式可审查):工艺无关键字 → 烫画;印花数量无单/双面且工艺非不打印 →
单面印花;不打印/光板 无印花面不补(矩阵中印花数量以单面占位,纯结构化规则)。
「恢复自动」(链接标签重置)本身是显式人工动作,同样走整理服务的单链接派生。
**人工标签缺啥补啥 + 旧词别名**2026-09-02 起):
- `PUT /origin-goods/:id/tags` 保存人工标签时,三个定价维度组(印花数量/工艺/物流)缺哪组
就按链接名称自动补哪组(并入人工集合一起存为 manual 行,人工勾选值永不被动),响应带
`filledDimensionTags`;未归族链接随后自动并入匹配族(同 SDS 分类优先,回退族语义名称键),
响应带 `attachedFamilyId`
- 工艺旧词「热转印」按别名归一为「烫画」进价格矩阵(`CRAFT_TAG_ALIASES`),标签字典本身不动。
历史背景:曾有 16 条人工链接因勾了旧词「热转印」被封闭词表静默踢出合并矩阵。
- 名称回退分组键为**族语义键** `familyNameKey`(第 1 段剥离物流备注、保留 国家+品名+SKU):
包邮/不包邮、单面/双面印花是矩阵维度,不再拆族;不同国家/不同款号仍不同族。
**公开读路径(四期族化契约,默认开启)**`PUBLIC_DETAIL_FROM_FAMILY=false` 可应急回退旧行为。
公开端点**以款(族)为一等公民**:
- `GET /public/goods`:**一族一条**(族内多条 Good 去重,代表行=排序第一条),
`goodId` = **族 ID**`total` = 族数,分页作用于分组后;`price` = 族矩阵最低价(起价),
价格排序按族起价重排;**无族商品(自定义)完全不返回**(列表/首页/分类树/标签统计同理)。
- `GET /public/goods/:goodId``goodId` 即**族 ID**(唯一公开寻址键;旧 SDS 链接 ID 与
自定义商品 sdsGoodId 均 404)。响应为款级聚合:
- 公共信息取代表 Good(名称/主图/国家/分类/标签)+ 主链接(详情文案);
- `variants` = 全体族成员 ∪ 旧副源(过渡期),按 `(链接, 变体)` 再按 `颜色+尺码` 去重;
`mediaByColor` 同源;`options/media` 并入族成员 detail
- `sizeChart` / `packageSpecs` = 族物化并集(空并集回退主链接合并结果);
- **族块**(默认输出):`family` 含并集尺码表/包装 + **严格五维价格矩阵** + 族起价:
```jsonc
{
"family": {
"familyId": "12", "familyCode": "DG015", "familyName": "DG015 180G纯棉T恤",
"sizes": [...], "colors": [...],
"printCounts": ["单面印花", "双面印花"], "crafts": ["烫画", "直喷", "不打印"], "logistics": ["包邮", "不包邮"],
"sizeChart": { /* 并集 */ }, "packageSpecs": { /* 并集 */ },
"priceMatrix": { /* rows {sizeId, colorId, printCount, craft, logistics, price, manual, sources} */ },
"minPrice": "29.5"
}
}
```
矩阵维度来源是**链接级标签**(印花数量/印刷工艺/物流渠道三组;人工接管后按人工标签),
不再用原始链接名的第 3 段(`craftLabel` 含「直喷单面/光板不打印」等噪声)。名称派生规则已
覆盖裸「单面/双面」写法(如「直喷双面」→ 双面印花 + 直喷)。光板/不打印链接无印花面概念,
矩阵中印花数量维回退「单面印花」占位。前端本地按五维联动 `priceMatrix` 即可实时算价
(设计 D4)。
**边界声明**:价格区间筛选(minPrice/maxPrice)仍作用于链接 `goodPrice`(族内任一链接命中即
返回该族;SQL 层无法廉价解析族矩阵 JSON);`good_origin_goods` 转只读保留,观察期后另行删除。
**后台操作入口(族替代旧主源/副源,界面保持原有布局)**
- 商品配置页布局不变(左树=官网商品、右树=原产品库分类平铺);配置弹窗保持原「合并同名」
勾选流程,提交时**静默**把勾选链接与主链接归入同一族(无族自动成族);族内**不区分主次**
(成员列表无设为主链接按钮,主链接仅作为详情数据源的内部实现);
- 编辑弹窗的「**关联原产品**」区块:显示族编码与链接数,成员行显示**原始链接名**(便于核对)
并可**逐个展开**——查看成员详情(SDS ID / 链接价格 / SKU 数 / 物流备注 / 工艺位置 / 仓库)、
**配置该链接的标签**(保存后人工接管;「恢复自动」回到按名称派生)、
**改价格**(尺码/颜色/价格 可编辑表格,写入族价格覆盖)、
**查看成员自己的详情**(商品详情 / 尺码表 / 包装规格 / SKU 四个页签,懒加载
`GET /origin-goods/:id`,SKU 表带 物流/工艺/印花数量 维度列;未同步时可在面板内直接同步);
SDS 商品不再在弹窗底部展示单一链接的旧详情区(自定义商品的编辑表单仍保留在底部);
- 成员列表下方有**族级并集视图**页签:尺码表 / 包装规格(族物化并集)与
**SKU(全族动态合并)**——SKU 即价格矩阵行,各链接变体按 物流×工艺×印花数量×尺码×颜色
归并(同组合取最低价,「链接数」列显示来源数,人工价带标签),无需额外请求;
- 编辑弹窗商品「名称」预填解析后的「品名 型号」,保存即采用解析名;右侧原产品库与配置弹窗
始终显示**原始链接名**
- 配置弹窗「合并同名」区块**常显**,候选为**全局同款链接**:按 链接款号(名称第 2 段,
如 BRTF004)或族归属跨分类匹配,同款不同工艺/物流/仓库均在列(含已配置链接),
支持搜索过滤;无候选时显示空态提示。
- 右树徽标语义(**族感知**,公开契约一族一款):**已配置 N**(绿,本链接已配置为官网商品,
N=国数)/ **同款已配置**(绿,本链接未单独建商品但所属族已配置,无需再配)/ **成族**(蓝,
已归族且全族未配置)/ **未配置**(橙);「定位到官网商品」在自身或同族已配置时出现(按
主链接 → 同族 → 旧副源顺序定位并高亮);「配置」按钮仅在本链接及其同款族均未配置时出现,
拖拽到左侧分类/国家的路径同样被拦截(提示同款已配置、不允许重复配置);
- 族分组头部计数与「仅未配置」筛选同样按族语义(族已配置 → 计入已配置)。
- **左树一族一行**(国家/品类模式):同款多条配置只显示排序最前的一条,名称旁常显
「M 个配置」徽标(M=该族官网商品数;单配置也显示,避免漏看),悬浮列出全部配置
(名称/国家·分类/打开),分组头部计数仍按全部商品统计。
- 人工改价/自动成族等族管理 API`/product-families/*`)保留,供脚本或后续界面使用;
- 页面组件化:`GoodsView.vue` 拆出 `components/` 下的 GoodsEditDialog(编辑弹窗)、
GoodsConfigDialog(配置弹窗)、CustomGoodDialog(自定义商品)、TagFilterPopover(标签筛选弹层)。
> 注意:公开契约一族一款,同款(同族)已配置后**不允许再配置**——配置按钮隐藏、拖拽路径拦截,
> 避免产生官网不可见的冗余商品;历史重复商品(如早期 DG120/AUTM003 测试期间产生的)仍建议清理。
**链接级标签(自动派生 + 人工修正,2026-08 规则改版)**
- 标签与**产品链接一一对应**(每条链接因印花数量/工艺/物流不同而价格不同),
落库在链接级 `origin_good_tags` 表(`manual` 标记人工/派生);商品的自动组标签是其
**主链接标签的镜像**
- 解析规则(`apps/api/src/product-families/auto-tag-rules.ts`,与 admin 端
`utils/origin-name.ts#deriveLinkTagNames` 同构):
- **印花数量**:名称含「双面印花」→ `双面印花`;否则含「单面印花」→ `单面印花`(组「印花数量」);
- **工艺**:名称含「直喷」→ `直喷`;含「不打印」或「光板」→ `不打印`**光板即为不打印**
组「印刷工艺」);都不含 → 默认 `烫画`
- **物流**:含「不包邮」→ `不包邮`;否则含「包邮」→ `包邮`(组「物流渠道」,先判不包邮防子串误命中);
- 每次族重算/成员变更/商品创建更新时:未接管的 SDS 链接按名称刷新派生行;
**人工接管的链接(`tagsManual=true`)永不被覆盖**;商品镜像其链接的有效标签;
缺失的组与标签自动补建;
- **人工修正入口**:编辑弹窗成员行展开 → 修改标签 → 保存标签(全量替换为人工行,
`PUT /origin-goods/:id/tags`);「恢复自动」清掉人工行回到派生(`DELETE /origin-goods/:id/tags`);
- 其他分组(如风格类)不受影响,保持人工管理;自定义链接(无名称可解析)同样支持人工配置;
- 实测:`美国(包邮)…-DG001-单面印花``包邮 / 烫画 / 单面印花`
`美国(不包邮光板)…-JSA002-不打印``不包邮 / 不打印 / 光板`
`美国(不包邮)…-DG501-双面印花``不包邮 / 烫画 / 双面印花`
**链接名称展示解析**:后台所有链接名展示位(左右树、族成员列表、配置/编辑弹窗、搜索候选)
统一解析为「品名 型号」(如 `美国(包邮)180g纯棉T恤成人款-DG001-单面印花`
`180g纯棉T恤成人款 DG001`),悬停 tooltip 保留原始全名;搜索同时匹配原始名与解析名
`utils/origin-name.ts#cleanLinkName`)。
## 验证
```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
```