Files
dianxiaomi-crossborder-play…/速卖通建品/RECIPE-2026-09-30.md
T

170 lines
11 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.
# 速卖通(AliExpress POP)「选品建listing」完整配方
> 2026-09-30 实测跑通:**4/4 进「待发布」,采集箱 0 残留**(RV8I478D / ZQI01T5J / XSSIERBV / XAGOKS9C)。
> 目标态 = 待发布(`dxmState=offline & dxmOfflineState=waitPublish`),**绝不点「发布」**。
## 0. 页面与接口事实
| 项 | 值 |
|---|---|
| 创建页 | `https://www.dianxiaomi.com/web/smt/add`(标题「店小秘--添加速卖通产品」) |
| 编辑页 | `/web/smt/edit?id=<id>` —— **实测打不开**:`GET /api/smtProduct/edit.json?id=` 无论 id 传什么都返回同一份 12,592 字节的**添加页引导包**(modelList/unitList),不含商品数据 → 不要指望它做回读 |
| 保存 | `POST /api/smtProduct/add.json`(点「保存」触发;**不点「发布」**) |
| 列表 | `POST /api/smtProduct/pageList.json`,body: `pageNo&pageSize&total&searchType=0&searchValue&shopId=-1&dxmState&dxmOfflineState` |
| 列表页 | 待发布 `/web/smt/smtProductList/offline`、采集箱 `…/draft`、在线 `…/online` |
| 类目 | 点「选择分类」→ 弹窗标题**「选择类目」**、搜索框 `input[placeholder*=搜索分类]`、结果行 `.search-result-item` |
| 单位 | **kg / cm**(Ozon 是 g / mm,别混) |
| 店铺 | 10 家:BlessLuckA、Laikeet、Sunbeam、AliMingPro1、Key、Deanoyal… |
## 1. 执行顺序(顺序错了就被挡)
```
① 店铺(必须最先!类目树随店铺/站点变)
② 类目(中文品类词搜索 → 点结果行,弹窗自动关)
③ 标题(insertText)
④ 来源URL(无 label,用 placeholder 定位)
⑤ 零售价格 / 库存数量 / 货值 / 商品编码
⑥ 重量(kg)、包装尺寸(cm) ×3、包装后尺寸(cm) ×3
⑦ 图片(选择图片 → 网络图片 → 粘 URL → 添加)
⑧ 物流属性(铅笔图标弹窗 → 普货 → 确定)
⑨ 是否原箱(下拉,必填)、报价是否含关税(radio value=1 不含关税报价)
⑩ PC端描述(CKEditor 4)
⑪ 保存 → 回读 pageList
```
## 2. 逐字段硬知识(每条都踩过)
1. **不选店铺 → 「选择分类」根本打不开**(弹窗不出现,表现为 `no-modal`)。脚本里店铺选择必须在类目前。
2. **类目搜索词必须是中文**:`旗帜→横幅(Banners)`、`托特包→Tote Bags`、`护照夹→护照套/护照夹`。
**英文搜不到**(`Tote Bags` → 命中 0 → 保存报「请选择分类」)。
结果行文本形如 `Luggage & Bags(箱包)/Kids' Bags(童包)/Tote Bags(托特包)`(分隔符 `/`,中英双语),
**点行即选中并关窗**,没有可用的「选择」确认按钮。
* 已知瑕疵:`托特包` 命中 3 条时会选到第一条「童包」,优先命中父级段(`Women Bags`)的逻辑已加,但速卖通树里未必存在该英文段 → 需要时人工在页面上点一次。
3. **图片门槛 单边 ≥400px**:喂 10 张图,速卖通**静默只收 6 张**(300×224 / 300×195 / 367×344 / 300×213 被丢)。
→ 建输入前先按真实尺寸过滤:`build-add-input.py --min-side 400`(会下载探尺寸)。
模块:`.productMainImgModule`,头按钮文案「选择图片」;弹层 tab:本地图片 / **网络图片** / 图片银行 / 引用采集图片;
网络图片 tab 的 textarea placeholder「请填写图片URL地址,多个地址用回车换行」→ 点「添加」。
回读口令:`主图最多选用6张,已经选用了6张`(上限 **6 张主图**)。
4. **保存必填项(缺一个就整单失败)**——错误清单来自 `MYJ.message.error` 钩子 + `.ant-form-item-explain-error`:
| 报错原文 | 要填什么 | 正确写法 |
|---|---|---|
| 请输入货值 | 货值(CNY) | 用成本价兜底(`cost_cny`) |
| 物流属性必填! | 物流属性 | **不是普通下拉**:点 `.single-logistics-attr-edit` 铅笔 → `.batch-edit-modal` → 选「普货」→ 确定 |
| 包装后尺寸请填写整数! | 包装后尺寸 | **整数 cm**;包装尺寸与包装后尺寸都要填,且**尺寸建议整体向上取整** |
| 请选择报价是否含关税! | radio | `input[type=radio][value=1]` = 不含关税报价 |
| 请输入重量 | 重量(kg) | **必须精确命中「重量(kg)」**:类目属性里还有「重量(Item Weight)」,用 `/重量/` 会填错字段(护照夹实测被拒) |
| 请选择分类 | 类目 | 见第 2 条 |
5. **重量下限**:46g → 0.046 → 字段只留 2 位小数变 `0.04` → 被拒「请输入重量」。
安全写法:`wkg = max(0.05, ceil(g/10)/100)` → 0.046→0.05、0.075→0.08、0.097→0.10。
6. **描述是 CKEditor 4 两个实例**:`ckeditor30`=PC端描述、`ckeditor31`=无线端描述(实测确认)。
**必须按实例所属 form-item 定位**:
```js
const hit = Object.keys(CKEDITOR.instances).find(k => {
const el = CKEDITOR.instances[k].element?.$; return el && formItem.contains(el);
});
CKEDITOR.instances[hit].setData(html); CKEDITOR.instances[hit].updateElement();
```
* editable 元素的 `id` **不等于**实例名 → 用 `CKEDITOR.instances[ed.id]` 会落空;
* 退化成「改 iframe innerHTML / contenteditable」时 model 不同步 = 白填。
* 实测成功回读:`ckeditor:ckeditor30:len=2290`。
7. **`pageList` 不返回 `detail` / `packageWeight` / `packageLength`**(100 行全空)→
**不能**用它判断描述/重量/尺寸是否存进去,只能看保存时有没有报错。别把空值当「没保存」。
### ⚠ 类目弹窗「点了行但没关窗」→ 后面所有字段都填不进去(2026-09-30 实测)
`pickCategoryBySearch` 点结果行后**有时弹窗不关**。不关的弹窗会挡住后续所有点击:
标题/价格/库存/来源URL 全部写不进去(但类目已选,看起来「像成功了」)——
上一版就是这样,保存时报的却是别的字段,或直接存成空字段。
**现在两处兜底**:① `closeCategoryModal()` 主动确认并 Esc/点「关闭」强关;
② **模型自检** `smtModelGaps()` 会在保存前报出 `subject(标题)/skuPrice(价格)/ipmSkuStock(库存)` 为空 → 跳过保存。
实测四个品修完后 `gaps=[]`(title 115–127 / price 5.3–15.9 / stock 999 / desc 1990–3252)。
## 3. 脚本
```bash
D=/home/admin9webs/dsh-dingtalk-workspace/listing-spec
# ① 生成输入(含 400px 图过滤 + 中文类目词)
python3 $D/platform-shared/build-add-input.py pending-package-*.json \
--platform 速卖通 --out /tmp/smt_input.json --min-side 400
# ② 干跑(只填不保存,验证选择器)
node $D/platform-smt/smtbatch.mjs --input /tmp/smt_input.json --only <oid> --dry
# ③ 正式建listing(只保存 → 待发布)
node $D/platform-smt/smtbatch.mjs --input /tmp/smt_input.json --only <oid>
# ④ 回读验收(待发布有没有 + 采集箱有没有本轮新草稿)
node $D/platform-smt/verify-landing.mjs --oids <oid1>,<oid2> --keywords "kw1,kw2"
```
* `platform-shared/dxm-add-lib.mjs`:公共交互库(CDP 真实点击/滚动、setByLabel(insertText)、
`setNumberByLabel`(原生 setter,避免小数被吃)、`pickCategoryBySearch`、`addImagesByUrl`、
`setRichTextByLabel`(CKEditor)、`setLogisticsAttr`、`listShopOptions`、`readErrors`)。
* `platform-shared/preflight-shops.mjs`:**每轮批量前先跑**——查每个平台有没有授权店铺。
* `platform-shared/build-add-input.py`:把待上架 package 转成建listing输入;图按真实尺寸过滤;
英文类目叶 → 中文搜索词映射表 `CAT_KW`(命中不了就补这张表,别在页面上手点)。
## 4. 验收口径(实测结果)
```
RV8I478D ✅ 待发布 1 | 草稿 0 | 横幅 ¥6.65 图6
ZQI01T5J ✅ 待发布 1 | 草稿 0 | 托特包 ¥5.30 图6
XSSIERBV ✅ 待发布 1 | 草稿 0 | 护照套/护照夹 ¥6.90 图6
XAGOKS9C ✅ 待发布 1 | 草稿 0 | 托特包 ¥15.90 图6
```
* 采集箱里**有**大量历史草稿(有些是 2022/2023 年的),按关键词搜会误报残留 →
回读脚本只把**最近 N 分钟**建的草稿算成本轮残留(`--since-min`,默认 180)。
* SKU 前缀不能单独当判据:RV8I478D 的 SKU 是 1688 供应商码 `ZSJLQ_1be`,不含 oid → 回读用「SKU 前缀 **或** 标题关键词」双条件。
## 5. 仍未解决 / 待补
* 类目分支优选(托特包落「童包」而非「女包」)——需要人工确认或补父级英文段。
* `包装后尺寸` 与 `包装尺寸` 目前都填同一组向上取整值;如需精算需接速卖通计抛规则。
* 变种(多变种)模式未做:本配方用单品/单 SKU 路径;带变种的类目需要补变种行填写。
* 运费模板/服务模板未设置(`freightTemplateId=null`),发布前需人工在页面选或做「运费推荐生成」。
## 附:账号级店铺盘点(一次接口,最快)
`platform-shared/dxm-add-lib.mjs` 的 `readAccountShops(c)`:`GET /api/userIn.json` → `data.shopMap`
(key=shopId,value 含 `name`/`platform`)。实测 2026-09-30(子账号):
```
256 家店 → smt:61 tiktok:98 tiktokGlobal:24 aliChoice:65 ozon:5 walmart:3
```
★ **页面上的店铺下拉是子集**:速卖通页只列 10 家,`shopMap` 里是 61 家。
判断「这个登录有没有某平台店铺」用 `readAccountShops`;判断「这个店能不能在这个页面选到」才用 `listShopOptions`。
## 附二:Vue 3 + Pinia 模型层自检(2026-09-30 加,**保存前必做**)
店小秘所有平台添加页都是 **Vue 3.5.4 + Pinia**(`#app.__vue_app__.config.globalProperties.$pinia`)。
**DOM 里显示的值 ≠ 表单模型里的值**;填了 DOM 而模型为空时点保存会「静默失败」或存成空字段。
→ 保存前读模型自检:`smtModelSnapshot(c)` + `smtModelGaps(model)`,**gaps 非空则跳过保存**。
速卖通的字段映射(实测,`smtAddBasicStore.formState` / `smtAddProductStore.formState`):
| 业务字段 | 模型路径 |
|---|---|
| 店铺 / 类目 / 标题 | `smtAddBasicStore.formState.{shopId,categoryId,subject}` |
| 价格 / **货值** / 库存 / 编码 | `smtAddProductStore.formState.{skuPrice,cargoPrice,ipmSkuStock,skuCode}` |
| 重量 / 尺寸(长宽高) | `packageWeight` / `packageLength,packageWidth,packageHeight` |
| 是否原箱 | `originalBox`("0"=否) |
| **物流属性** | `specialProductTypeList`(普货 = `["1983471271:2025103"]`) |
| 图片 | `imageList[]` |
| **描述** | `smtAddStore.describeInfoData.detail`(PC端)/ `mobileDetail`(无线端) |
一次干净的 dry 实测(RV8I478D):
```
shopId=135223 categoryId=201717001 subjectLen=121
skuPrice=6.65 cargoPrice=5.5 ipmSkuStock=999 skuCode=ZSJLQ_1be
packageWeight=0.10 dims=28/16/1 originalBox=0
specialProductTypeList=["1983471271:2025103"] images=6 descLen=1935 → gaps=[]
```
结论:**描述确实是写进模型的**(1935 字符,在 `describeInfoData.detail`);**物流属性的 UI 路径也确实会提交到模型**。
之前「描述可能没保存」的疑虑到此消除;`pageList` 不返回这些字段只是列表投影问题。
其它平台 store 命名同构(`<platform>` = smt / shopeeGlobal / tiktokGlobal / ozonProduct):
`<platform>AddStore`(编排+描述)· `BasicStore` · `ProductStore` · `AttrStore` · `OtherStore`
· `SkuDataStore`/`SkuAttrStore`(变种)· `PackageStore`/`DescStore`(Shopee/Ozon)。
公共:`activity` `global` `user` `userPlatforms` `batchImageTaskStore` `*Translate*`。
用 `vueStores(c)` 列全部、`readPinia(c, storeId, path)` 读任意字段。