From b947d88c2c29d1f16fc6a92dec85287d1336bfdb Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Thu, 27 Aug 2026 14:57:19 +0800 Subject: [PATCH] docs(data-cleaning): finalize transformation rules and API-driven approach --- data-cleaning/README.md | 153 +++++++++++++++++++++++++++++++--------- 1 file changed, 120 insertions(+), 33 deletions(-) diff --git a/data-cleaning/README.md b/data-cleaning/README.md index 98bed58..90a1560 100644 --- a/data-cleaning/README.md +++ b/data-cleaning/README.md @@ -62,54 +62,141 @@ 其他字段:goodPriority 几乎全部为 5;positionId 全部为空; 国家字段与名称里的国家前缀一致率 238/240(仅 `西班牙直发(...)` 两条归入了「西班牙」)。 -### 3. 从字符串解析出的隐含规则(草稿,待确认) +### 3. 人工操作流程(脚本要模拟的完整动作) -一条原产品字符串可以机械解析出以下信息(541/552 可完全机器解析): +后台右侧树有「仅未配置」筛选按钮,人工实际是**两步操作**: -| 解析项 | 来源 | 对应目标字段 | -|--------|------|--------------| -| 国家 | 第一个 `(` 之前文本(需处理:空格分隔、半角括号、「西班牙直发→西班牙」映射) | `countryId` | -| 物流渠道 | 括号内含「包邮」与否 → 包邮 / 不包邮 | tag: 物流渠道 | -| 工艺/印刷位置 | 最后一段的 关键词:单面印花→单面印、双面印花→双面印、直喷→直喷、烫画→烫画、光板/不打印→不打印 | tags: 印刷位置 + 印刷工艺 | -| 展示名 | 目标形态(用户口径):`品名 SKU`(最后一个 `-` 连接改为空格连接,丢弃工艺/仓库段) | `goodName` | +**第一步:配置(右→左创建)** +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)) -### 4. 待确认问题(规则定稿前必须回答) +**第二步:编辑改名(左侧已有记录上改)** +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)) -1. 展示名采用哪种?A 全拆(`200g纯棉T恤 KRTM001`,推荐—— majority 且官网展示干净)还是 B 保留国家括号? -2. 同一基础商品的「单面印花」「双面印花」是拆成两个 goods(现状),还是合并为一个 good 并用标签区分? - (注意 A 全拆后两者 goodName 完全相同,如 ITTM001 已出现重名两条) -3. 历史 R0 的 127 条保持原名的记录是否要统一重命名? -4. 未配置的 343 个 origin 是本次要全部自动配置的目标吗?(其中可能有不应上架的商品) -5. 仓库名后缀(第 4 段)只做清洗忽略即可,还是需要保留到某个字段? -6. 国家中文名 → countries 表的映射以 `countryName` 精确匹配为准?缺失国家(如法国 FRTM001 对应的国家若不存在)如何处理? +因此脚本的**实际操作序列**是:先 `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 驱动方案) ``` -[生产库] - │ ① 备份(pg_dump 全量,安全网) - ▼ -② apps/api/scripts/export-data.mjs 导出 JSON(现有脚本) - ▼ -③ sync-transform.mjs 转换脚本 + sync-rules.config.mjs 规则文件(本目录,待创建) - 读取导出 JSON → 按规则加工 → 输出待导入 JSON + 变更摘要报告(人工检查点) - ▼ -④ apps/api/scripts/import-data.mjs 导入生产库(现有脚本) - ▼ -⑤ 验证:行数对比 / 抽查商品配置 / 官网公开 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 抽查新记录 ``` -原则:三步式独立执行,每步可检查、可回滚;规则只改 `sync-rules.config.mjs`,不改代码; -导入前必须有全量备份;转换脚本为纯函数式(JSON in → JSON out),不直接连接数据库。 +原则: +- 不导出/导入数据库,全部通过 API 操作 +- ② 和 ④ 分离:先生成计划供人工审查,确认后再执行 +- 每步产物存 runs/(已 gitignore) +- token 30 分钟过期,脚本内自动用 /auth/login 刷新 ## 目录内容规划 | 文件 | 状态 | 说明 | |------|------|------| -| `sync-rules.config.mjs` | 待创建 | 同步规则文件(品类映射 / 国家分配 / 标签分配 / 优先级策略),规则未定,先占位 | -| `sync-transform.mjs` | 待创建 | 数据转换脚本 | -| `runs/` | 待创建 | 每次执行的导出、转换产物与变更摘要报告(按日期归档);已含 `survey-2026-08-27/` 调查快照(gitignore) | +| `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)。