# 商品数据清洗(Goods Data Cleaning) 本目录存放商品数据清洗相关的全部脚本与规则文件。清洗目标:把右侧 SDS 原产品库 (`origin_goods`)按规则批量配置为左侧官网商品(`goods`),替代后台手动逐个点击。 ## 转换规则调查(2026-08-27,基于生产环境只读数据) > 数据来源:`https://official.inkreach.cc/api` 生产接口(JWT 只读拉取): > `/origin-goods?page=1..3&pageSize=200`(共 552 条)、`/goods?page=1..2&pageSize=200`(共 240 条)、 > `/tags`、`/categories`、`/countries`。未做任何写操作。 > 原始 JSON 快照存于 `runs/survey-2026-08-27/`(已加入 .gitignore,不入库)。 ### 1. 右侧原产品(origin_goods)命名结构 右侧只有**一个字符串** `goodName`,整体模式: ``` 国家(物流备注)品名-SKU-工艺位置[-仓库名] ↑ ↑ ↑ ↑ ↑ | | | | └─ 可选第4段:美西洛杉矶一仓 / 美中亚特兰大仓 等 | | | └─ 单面印花 / 双面印花 / 直喷双面 / 直喷单面 / 不打印 ... | | └─ SKU 编码(字母+数字,如 DG001 / C1717 / KRHM003) | └─ 包邮 / 不包邮 / DHL包邮*运费结算订单时支付 / 不包邮光板 等变体 └─ 美国/德国/韩国...(全角括号为主,少量半角混用) ``` 统计(552 条): - **541 条**匹配 `国家(备注)其余` 模式;**11 条异常**: - 4 条自定义商品(`Goods Origin ...`,source=CUSTOM) - 2 条缺国家前缀:`(不包邮)230g水洗炒雪花T恤-双面印花` / `(不包邮)180g纯棉T恤成人款-...` - 1 条用空格代替括号:`法国 180g纯棉T恤-FRTM001-双面印花` - 2 条半角左括号混用:`美国(不包邮)女士高弹中长款瑜伽运动裤-JSD004-...` - 2 条同上变体 - 「备注)之后」按 `-` 分段:**387 条 = 3 段**(品名-SKU-工艺)、**154 条 = 4 段**(品名-SKU-工艺-仓库) - 物流备注出现的变体形态(示例):`包邮`、`不包邮`、`DHL包邮`、`DHL包邮*运费结算订单时支付`、`包邮*运费订单结算时支付`、`不包邮光板` - 国家前缀出现过的值:美国、德国、英国、意大利、西班牙、西班牙直发、韩国、日本、加拿大、墨西哥、巴西、波兰、澳大利亚 ### 2. 左侧已配置商品(goods)现状 —— 人工配置的实际结果 240 条 goods 记录,覆盖 **209 个**不同 origin_good_id(**343 个 origin 尚未配置**)。 **没有系统级拆分逻辑**:前端 [GoodsView.vue](../apps/admin/src/views/goods/GoodsView.vue) 提交的是完整原名, 后端 [goods.service.ts](../apps/api/src/goods/goods.service.ts) create/batchCreate 也原样入库。 因此左侧名称与右端的差异(113/240 条被改过)全部是**人工在编辑弹窗里改的**,且存在多种风格并存。 改名风格分布(113 条改名的归类): | 风格 | 数量 | 示例 | |------|------|------| | A. 全拆:去前缀去括号,品名+空格+SKU,丢弃工艺段 | 27 | `韩国(包邮)200g纯棉T恤-KRTM001-单面印花` → `200g纯棉T恤 KRTM001` | | B. 半改:仅把长备注缩成「X包邮」,其余原样保留 | 86 | `德国(DHL包邮*运费结算订单时支付)230g水洗T恤-DETM002-单面印花` → `德国(DHL包邮)230g水洗T恤-DETM002-单面印花` | | R0. 保持原名不动 | 127 | 多为后期配置(2026-06 后期 ~ 07),未加工 | 标签体系(生产库现有 3 组 7 个标签)与左侧使用情况: | 分组 | 标签(id) | 左侧使用次数 | |------|----------|--------------| | 物流渠道 | 包邮(30) / 不包邮(31) | 87 / 153 ← 基本每个商品都挂了物流标签 | | 印刷位置 | 双面印(35) / 单面印(34) | 14 / 14 ← 只有少数挂了 | | 印刷工艺 | 烫画(32) / 直喷(33) / 不打印(42) | 11 / 3 / 1 | 其他字段:goodPriority 几乎全部为 5;positionId 全部为空; 国家字段与名称里的国家前缀一致率 238/240(仅 `西班牙直发(...)` 两条归入了「西班牙」)。 ### 3. 人工操作流程(脚本要模拟的完整动作) 后台右侧树有「仅未配置」筛选按钮,人工实际是**两步操作**: **第一步:配置(右→左创建)** 1. 右侧找到未配置的原产品,点击「配置」 2. 弹窗显示:原产品名(**纯文本,不可编辑**,[L1716](../apps/admin/src/views/goods/GoodsView.vue#L1716))、预览图片(可改但默认回填) 3. 人工选择三项:**国家、分类、标签**(名称在此步不能改) 4. 确认提交 → `POST /goods`,goodName 直接传原产品完整原名([L403](../apps/admin/src/views/goods/GoodsView.vue#L403)) **第二步:编辑改名(左侧已有记录上改)** 1. 左侧找到刚配置的商品,点击「编辑」 2. 编辑弹窗有 `el-input` 绑定 goodName([L1821](../apps/admin/src/views/goods/GoodsView.vue#L1821)),这里才能改名 3. 保存 → `PATCH /goods/:id`([L690](../apps/admin/src/views/goods/GoodsView.vue#L690)) 因此脚本的**实际操作序列**是:先 `POST /goods`(用原名创建)→ 再 `PATCH /goods/:id`(改名) 弹窗字段与 API 参数对应: | 步骤 | 弹窗字段 | API 参数 | 说明 | |------|----------|----------|------| | 配置 | 国家 | `countryId` | 必填 | | 配置 | 分类 | `categoryId` | 必填 | | 配置 | 标签 | `tagIds[]` | 写入 good_tags 中间表 | | 配置 | 预览图片 | `goodImage` | 默认回填 origin_good.goodImage | | 配置 | 名称 | `goodName` | **不可编辑**,自动传原名 | | 编辑 | 名称 | `goodName` | **这一步才能改**,调 PATCH | | 编辑 | 其余字段 | 同上 | 也可在编辑时调整 | ### 4. 定稿转换规则 > 以下规则已确认,脚本按此执行。 #### 4.1 整体策略 - **方案:API 驱动**(不导出/导入 DB,避免 BigInt 自增序列和外键约束问题) - 脚本调用 `POST /goods` 逐条配置,走现有业务逻辑(校验、事务、标签关联全部由后端处理) - 幂等:已存在的 (originGoodId, countryId) 组合跳过 - scope:仅处理**未配置**的原产品(configuredCount === 0),已配置的不动 #### 4.2 名称解析与改名规则 原产品名格式:`国家(物流备注)品名-SKU-工艺位置[-仓库名]` **改名规则**:保留到 `-` 分隔的第 2 段,用空格连接,丢弃后续段(工艺、仓库)。 ``` 输入: 美国(包邮)180g纯棉T恤成人款-DG001-单面印花 解析: 品名="180g纯棉T恤成人款" SKU="DG001" 工艺="单面印花"(丢弃) 输出: goodName = "180g纯棉T恤成人款 DG001" ``` - 仓库名后缀(第 4 段,如「美西洛杉矶一仓」)直接丢弃 - 异常名称(缺国家前缀、半角括号等)单独输出到报告,不自动处理 #### 4.3 国家 - 从名称第一个 `(` 之前提取国家文本 - 通过 `GET /countries` 拿到 countries 表,按 `countryName` 精确匹配 → `countryId` - 特殊映射:「西班牙直发」→ 匹配「西班牙」 - 匹配不到的 → 输出到报告,不自动处理 #### 4.4 分类(品类) - 右侧树已经按 categories 树分组(通过 `origin_goods.sds_category_id` ↔ `categories.sds_category_id` 桥接) - 同一原产品的分类就是它在右侧树中所挂的分类节点,直接取该节点的 `categoryId` - 未分类的(sdsCategoryId 无匹配)→ 输出到报告 #### 4.5 标签(3 组,从名称解析) 标签体系(3 组 7 个): | 分组 | 标签 | id | 解析规则 | |------|------|----|----------| | 物流渠道 | 包邮 / 不包邮 | 30 / 31 | 括号内含「包邮」→ 包邮(30),否则 → 不包邮(31) | | 印刷位置 | 单面印 / 双面印 | 34 / 35 | 工艺段含「单面」→ 单面印(34),含「双面」→ 双面印(35) | | 印刷工艺 | 烫画 / 直喷 / 不打印 | 32 / 33 / 42 | 工艺段含「直喷」→ 直喷(33);含「不打印」或「光板」→ 不打印(42);其余默认 → 烫画(32) | 工艺段 = `-` 分段的最后一段(去仓库段后),如 `单面印花`、`直喷双面`、`不打印`、`烫画`。 解析示例: ``` 美国(不包邮)180g纯棉T恤成人款-DG001-单面印花 → 物流: 不包邮(31), 印刷位置: 单面印(34), 印刷工艺: 烫画(32) 美国(不包邮)207G重磅纯棉T恤-C1717-直喷双面 → 物流: 不包邮(31), 印刷位置: 双面印(35), 印刷工艺: 直喷(33) 美国(不包邮光板)180g纯棉T恤成人款-DG001-不打印 → 物流: 不包邮(31), 印刷位置: 跳过(工艺段=不打印无法判断单双面), 印刷工艺: 不打印(42) ``` #### 4.6 其他字段 - `goodImage`:直接用 origin_good.goodImage,不改 - `goodPriority`:默认 5(与现有 236/240 条一致) - `positionId`:不填(与现有 240/240 条一致) --- ## 执行流水线(API 驱动方案) ``` ① 拉取只读数据 调 GET /origin-goods/tree(含 configuredCount)、/tags、/categories、/countries 存到 runs//snapshot/ ② 本地解析脚本 sync-plan.mjs 读取快照 → 筛选未配置 → 逐条解析名称 → 生成待创建指令列表 输出: runs//plan.json(每条包含 originGoodId/countryId/categoryId/goodName/tagIds/goodImage) + runs//plan-report.md(人工检查点:共 N 条待创建、X 条异常需人工确认) ③ 人工审查 plan.json 和 plan-report.md ④ 执行脚本 sync-apply.mjs 读取 plan.json → 逐条 POST /goods(带 JWT,自动刷新 token) 每条打印 +/skip/error;失败自动重试 3 次;生成 apply-report.md ⑤ 验证:调 /origin-goods/tree 确认未配置数归零,调 /goods 抽查新记录 ``` 原则: - 不导出/导入数据库,全部通过 API 操作 - ② 和 ④ 分离:先生成计划供人工审查,确认后再执行 - 每步产物存 runs/(已 gitignore) - token 30 分钟过期,脚本内自动用 /auth/login 刷新 ## 目录内容规划 | 文件 | 状态 | 说明 | |------|------|------| | `sync-rules.config.mjs` | 待创建 | 国家映射/异常处理等配置项(解析规则已定稿在上文) | | `sync-plan.mjs` | 待创建 | 拉取 + 解析 + 生成 plan.json | | `sync-apply.mjs` | 待创建 | 读取 plan.json + 逐条 POST /goods | | `runs/` | 已创建 | 调查快照 survey-2026-08-27/ 已存在;后续每次运行产物按日期归档 | 详细实施计划见 [plans/feature/goods-data-cleaning-feature.md](../plans/feature/goods-data-cleaning-feature.md)。