Commit 15e63baff6d2af1954047809de30dcfad04159d8
1 parent
97a5d59d
索引文档修改
Showing
5 changed files
with
956 additions
and
122 deletions
Show diff stats
.gitignore
| ... | ... | @@ -0,0 +1,346 @@ |
| 1 | +# 商品导入模板说明文档 | |
| 2 | + | |
| 3 | +本文档完整描述了商品导入Excel模板的结构、字段说明和示例数据。 | |
| 4 | + | |
| 5 | +## 字段列表 | |
| 6 | + | |
| 7 | +| 列号 | 字段名称 | 字段说明 | | |
| 8 | +|------|----------|----------| | |
| 9 | +| 1 | 商品ID | (此行导入时不可删除 )商品 ID 是系统生成的唯一标识符,新增商品无需填写 | | |
| 10 | +| 2 | 创建时间 | 创建时间的时区为当前店铺设置的时区 | | |
| 11 | +| 3 | 商品标题* | 最多255字符(同一商品的子款式标题与商品标题需一致,且中间请勿插入其他商品) | | |
| 12 | +| 4 | 商品属性* | 1.多款式商品:<br>同一个商品的首行标题填入「M」(商品主体);<br>同一个商品的非首行标题填入「P」(子款式);<br>2.单一款式商品(Single):填入「S」 | | |
| 13 | +| 5 | 商品副标题 | 最多600字符 | | |
| 14 | +| 6 | 商品描述 | 上传不带样式,若需要样式,则输入html代码 | | |
| 15 | +| 7 | SEO标题 | 最多5000字符 | | |
| 16 | +| 8 | SEO描述 | 最多5000字符 | | |
| 17 | +| 9 | SEO URL Handle | 最多支持输入255字符<br>(SEO URL handle只对SEO URL的「URL参数」部分进行更改,即“products/”后的内容,如:products/「URL参数」<br>) | | |
| 18 | +| 10 | SEO URL 重定向 | 创建URL重定向,访问修改前链接可跳转到修改后的新链接页面<br>「Y」:TRUE <br>「N」:FALSE | | |
| 19 | +| 11 | SEO关键词 | 多个关键词请用「英文逗号」隔开 | | |
| 20 | +| 12 | 商品上架 | 「Y」:上架状态<br>「N」:下架状态<br>默认为「N」 | | |
| 21 | +| 13 | 需要物流 | 「Y」:需要<br>「N」:不需要<br>默认为「Y」 | | |
| 22 | +| 14 | 商品收税 | 「Y」:需要<br>「N」:不需要<br>默认为「N」 | | |
| 23 | +| 15 | 商品spu | 最多100字符 | | |
| 24 | +| 16 | 启用虚拟销量 | 「Y」:启用<br>「N」:不启用<br>默认为「N」 | | |
| 25 | +| 17 | 虚拟销量值 | 最多输入六位数的自然数 | | |
| 26 | +| 18 | 跟踪库存 | 「Y」:跟踪<br>「N」:不跟踪<br>默认为「N」 | | |
| 27 | +| 19 | 库存规则* | 若跟踪库存为Y,则该项必填:<br>填入「1」表示:库存为0允许购买<br>填入「2」表示:库存为0不允许购买<br>填入「3」表示:库存为0自动下架 | | |
| 28 | +| 20 | 专辑名称 | 请填入已创建的手动专辑名称;<br>多个专辑请用「英文逗号」隔开 | | |
| 29 | +| 21 | 标签 | 最多输入250个标签,每个不得超过500字符,多个标签请用「英文逗号」隔开 | | |
| 30 | +| 22 | 供应商名称 | 最多20字符 | | |
| 31 | +| 23 | 供应商URL | 请输入供应商URL | | |
| 32 | +| 24 | 款式1 | 最多255字符 | | |
| 33 | +| 25 | 款式2 | 最多255字符 | | |
| 34 | +| 26 | 款式3 | 最多255字符 | | |
| 35 | +| 27 | 商品售价* | 最多输入9位正整数,2位小数 | | |
| 36 | +| 28 | 商品原价 | 最多输入9位正整数,2位小数 | | |
| 37 | +| 29 | 成本价 | 最多输入9位正整数,2位小数 | | |
| 38 | +| 30 | 商品SKU | 最多255字符 | | |
| 39 | +| 31 | 商品重量 | 最多输入10位正整数,2位小数 | | |
| 40 | +| 32 | 重量单位 | 可选单位有:kg, lb, g, oz;<br>默认为kg | | |
| 41 | +| 33 | 商品条形码 | 最多100字符 | | |
| 42 | +| 34 | 商品库存 | 只支持导入默认地点库存,如需导入其他地点的库存,请到【库存列表】导入。最多输入9位整数 | | |
| 43 | +| 35 | 尺寸信息 | 按长宽高顺序填写,用「英文逗号」隔开,单位默认为英寸 | | |
| 44 | +| 36 | 原产地国别 | 请填写国家代码 | | |
| 45 | +| 37 | HS(协调制度)代码 | 6-12纯数字 | | |
| 46 | +| 38 | 商品图片* | 商品属性为M(商品主体)与S(单一款式商品)的行可填入商品图URL:<br>1.若商品主图字段有值,则填入的图片URL为商品副图(可选填)<br>2.若商品主图字段为空,则必须至少填入一个图片URL,且首个URL为商品主图URL,其他为商品副图<br>3.填多个URL可用「英文逗号」隔开<br><br>商品属性为P(子款式)的行可填入子款式图URL:<br>1.仅支持填入一个URL作为子款式图片,填入多个时默认导入第一个;若不填入,则默认不需要图片<br>2.同一商品的部分子款式上传图片,则其余款式也需要上传,否则默认填入商品主图<br> | | |
| 47 | +| 39 | 商品备注 | 最多输入500个字 | | |
| 48 | +| 40 | 款式备注 | 最多输入20个字 | | |
| 49 | +| 41 | 商品主图 | 1.只需商品属性为M(商品主体)与S(单一款式商品)的行填写(可选填)<br>2.仅能填入一个图片URL作为商品主图,填入多个时默认只导入第一个 | | |
| 50 | +| 42 | 如需导入元字段, 请在此行增添【元字段-命名空间和密钥】作为表头,例:元字段-test.111 | 1. 请确认导入的元字段在店铺后台已创建;<br>2. 只需商品属性为M(商品主体)与S(单一款式商品)的行填写元字段值(可选填)<br>3.各类型元字段填写规范详见帮助文档:https://helpcenter.shoplazza.com/hc/zh-cn/articles/37520605874457 | | |
| 51 | + | |
| 52 | +## 示例数据 | |
| 53 | + | |
| 54 | +模板中包含以下示例数据,展示了多款式商品(M+P)和单一款式商品(S)的格式: | |
| 55 | + | |
| 56 | +### 商品示例:Legendary Whitetails Men's Buck Camp Flannel Shirt | |
| 57 | + | |
| 58 | +#### 行 4 - 商品属性: M | |
| 59 | + | |
| 60 | +**基本信息:** | |
| 61 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 62 | +- 商品属性*: `M` | |
| 63 | +- 商品副标题: `100% Cotton | |
| 64 | +We recommend ordering a size down if you prefer a closer fit | |
| 65 | +Purchase from Amazon seller Legendary Whitetails to ensure you receive an authentic Legendary Whitetails branded Buck Camp F...` | |
| 66 | +- 商品描述: `<p>A hunter’s wardrobe is not complete without a great flannel. Our exclusive plaids are made from 100% cotton soft brushed flannel. Featuring double pleat back for ease of movement and contr...` | |
| 67 | + | |
| 68 | +**SEO信息:** | |
| 69 | +- SEO标题: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 70 | +- SEO描述: `Featuring double pleat back for ease of movement and contrasting corduroy lined collar and cuffs for a great look and lasting durability.` | |
| 71 | +- SEO URL Handle: `products/maternity-lace-cardigan-dress-photo-shoot_3a09` | |
| 72 | +- SEO关键词: `新品,热卖,爆款` | |
| 73 | + | |
| 74 | +**状态设置:** | |
| 75 | +- 商品上架: `Y` | |
| 76 | +- 需要物流: `Y` | |
| 77 | +- 商品收税: `Y` | |
| 78 | +- 启用虚拟销量: `N` | |
| 79 | +- 虚拟销量值: `100` | |
| 80 | +- 跟踪库存: `Y` | |
| 81 | +- 库存规则*: `1` | |
| 82 | + | |
| 83 | +**分类信息:** | |
| 84 | +- 专辑名称: `衬衣,热卖` | |
| 85 | +- 标签: `新品,热卖,爆款` | |
| 86 | +- 供应商名称: `Amazon` | |
| 87 | +- 供应商URL: `https://www.amazon.com/Legendary-Whitetails-Buck-Flannels-Large/dp/B01KTUMBOI/ref=sr_1_1?s=fashion-mens-intl-ship&ie=UTF8&qid=1543038722&sr=1-1` | |
| 88 | + | |
| 89 | +**款式信息:** | |
| 90 | +- 款式1: `SIZE` | |
| 91 | +- 款式2: `COLOR` | |
| 92 | + | |
| 93 | +**其他信息:** | |
| 94 | +- 商品图片*: `https://m.media-amazon.com/images/S/aplus-seller-content-images-us-east-1/ATVPDKIKX0DER/A1ABYS4IVXNT9X/27b061cf-ccd1-4c4c-82fd-6519a84b60c3.jpg` | |
| 95 | + | |
| 96 | +#### 行 5 - 商品属性: P | |
| 97 | + | |
| 98 | +**基本信息:** | |
| 99 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 100 | +- 商品属性*: `P` | |
| 101 | + | |
| 102 | +**款式信息:** | |
| 103 | +- 款式1: `S` | |
| 104 | +- 款式2: `red` | |
| 105 | + | |
| 106 | +**价格库存:** | |
| 107 | +- 商品售价*: `88.99` | |
| 108 | +- 商品原价: `149.99` | |
| 109 | +- 商品SKU: `LW-TS-RD-S1` | |
| 110 | +- 商品重量: `0.2` | |
| 111 | +- 重量单位: `kg` | |
| 112 | +- 商品库存: `100` | |
| 113 | + | |
| 114 | +#### 行 6 - 商品属性: P | |
| 115 | + | |
| 116 | +**基本信息:** | |
| 117 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 118 | +- 商品属性*: `P` | |
| 119 | + | |
| 120 | +**款式信息:** | |
| 121 | +- 款式1: `S` | |
| 122 | +- 款式2: `black` | |
| 123 | + | |
| 124 | +**价格库存:** | |
| 125 | +- 商品售价*: `88.99` | |
| 126 | +- 商品原价: `149.99` | |
| 127 | +- 商品SKU: `LW-TS-BK-S1` | |
| 128 | +- 商品重量: `0.2` | |
| 129 | +- 重量单位: `kg` | |
| 130 | +- 商品库存: `100` | |
| 131 | + | |
| 132 | +#### 行 7 - 商品属性: P | |
| 133 | + | |
| 134 | +**基本信息:** | |
| 135 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 136 | +- 商品属性*: `P` | |
| 137 | + | |
| 138 | +**款式信息:** | |
| 139 | +- 款式1: `S` | |
| 140 | +- 款式2: `army` | |
| 141 | + | |
| 142 | +**价格库存:** | |
| 143 | +- 商品售价*: `88.99` | |
| 144 | +- 商品原价: `149.99` | |
| 145 | +- 商品SKU: `LW-TS-AM-S1` | |
| 146 | +- 商品重量: `0.2` | |
| 147 | +- 重量单位: `kg` | |
| 148 | +- 商品库存: `100` | |
| 149 | + | |
| 150 | +#### 行 8 - 商品属性: P | |
| 151 | + | |
| 152 | +**基本信息:** | |
| 153 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 154 | +- 商品属性*: `P` | |
| 155 | + | |
| 156 | +**款式信息:** | |
| 157 | +- 款式1: `L` | |
| 158 | +- 款式2: `red` | |
| 159 | + | |
| 160 | +**价格库存:** | |
| 161 | +- 商品售价*: `88.99` | |
| 162 | +- 商品原价: `149.99` | |
| 163 | +- 商品SKU: `LW-TS-RD-M1` | |
| 164 | +- 商品重量: `0.2` | |
| 165 | +- 重量单位: `kg` | |
| 166 | +- 商品库存: `100` | |
| 167 | + | |
| 168 | +#### 行 9 - 商品属性: P | |
| 169 | + | |
| 170 | +**基本信息:** | |
| 171 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 172 | +- 商品属性*: `P` | |
| 173 | + | |
| 174 | +**款式信息:** | |
| 175 | +- 款式1: `L` | |
| 176 | +- 款式2: `black` | |
| 177 | + | |
| 178 | +**价格库存:** | |
| 179 | +- 商品售价*: `88.99` | |
| 180 | +- 商品原价: `149.99` | |
| 181 | +- 商品SKU: `LW-TS-BK-M1` | |
| 182 | +- 商品重量: `0.2` | |
| 183 | +- 重量单位: `kg` | |
| 184 | +- 商品库存: `100` | |
| 185 | + | |
| 186 | +#### 行 10 - 商品属性: P | |
| 187 | + | |
| 188 | +**基本信息:** | |
| 189 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 190 | +- 商品属性*: `P` | |
| 191 | + | |
| 192 | +**款式信息:** | |
| 193 | +- 款式1: `L` | |
| 194 | +- 款式2: `army` | |
| 195 | + | |
| 196 | +**价格库存:** | |
| 197 | +- 商品售价*: `88.99` | |
| 198 | +- 商品原价: `149.99` | |
| 199 | +- 商品SKU: `LW-TS-AM-M1` | |
| 200 | +- 商品重量: `0.2` | |
| 201 | +- 重量单位: `kg` | |
| 202 | +- 商品库存: `100` | |
| 203 | + | |
| 204 | +#### 行 11 - 商品属性: P | |
| 205 | + | |
| 206 | +**基本信息:** | |
| 207 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 208 | +- 商品属性*: `P` | |
| 209 | + | |
| 210 | +**款式信息:** | |
| 211 | +- 款式1: `XL` | |
| 212 | +- 款式2: `red` | |
| 213 | + | |
| 214 | +**价格库存:** | |
| 215 | +- 商品售价*: `88.99` | |
| 216 | +- 商品原价: `149.99` | |
| 217 | +- 商品SKU: `LW-TS-RD-L1` | |
| 218 | +- 商品重量: `0.2` | |
| 219 | +- 重量单位: `kg` | |
| 220 | +- 商品库存: `100` | |
| 221 | + | |
| 222 | +#### 行 12 - 商品属性: P | |
| 223 | + | |
| 224 | +**基本信息:** | |
| 225 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 226 | +- 商品属性*: `P` | |
| 227 | + | |
| 228 | +**款式信息:** | |
| 229 | +- 款式1: `XL` | |
| 230 | +- 款式2: `black` | |
| 231 | + | |
| 232 | +**价格库存:** | |
| 233 | +- 商品售价*: `88.99` | |
| 234 | +- 商品原价: `149.99` | |
| 235 | +- 商品SKU: `LW-TS-BK-L1` | |
| 236 | +- 商品重量: `0.2` | |
| 237 | +- 重量单位: `kg` | |
| 238 | +- 商品库存: `100` | |
| 239 | + | |
| 240 | +#### 行 13 - 商品属性: P | |
| 241 | + | |
| 242 | +**基本信息:** | |
| 243 | +- 商品标题*: `Legendary Whitetails Men's Buck Camp Flannel Shirt` | |
| 244 | +- 商品属性*: `P` | |
| 245 | + | |
| 246 | +**款式信息:** | |
| 247 | +- 款式1: `XL` | |
| 248 | +- 款式2: `army` | |
| 249 | + | |
| 250 | +**价格库存:** | |
| 251 | +- 商品售价*: `88.99` | |
| 252 | +- 商品原价: `149.99` | |
| 253 | +- 商品SKU: `LW-TS-AM-L1` | |
| 254 | +- 商品重量: `0.2` | |
| 255 | +- 重量单位: `kg` | |
| 256 | +- 商品库存: `100` | |
| 257 | + | |
| 258 | +--- | |
| 259 | + | |
| 260 | +### 商品示例:Jabra Move Wireless Stereo Headphones | |
| 261 | + | |
| 262 | +#### 行 14 - 商品属性: S | |
| 263 | + | |
| 264 | +**基本信息:** | |
| 265 | +- 商品标题*: `Jabra Move Wireless Stereo Headphones` | |
| 266 | +- 商品属性*: `S` | |
| 267 | +- 商品副标题: `Listen to your tunes and never miss a call | |
| 268 | +Ultra-lightweight and adjustable headband fits all head-types | |
| 269 | +Durable stainless steel headband and dirt resistant fabric for life on the move.40mm Dynamic...` | |
| 270 | +- 商品描述: `Clean Scandinavian design meets crisp digital sound. Get a perfect fit with the ultra-light, comfortable headband. Choose from four modern colors: Cayenne (Red), Cobalt (Blue), Coal (Black) and Gold.` | |
| 271 | + | |
| 272 | +**SEO信息:** | |
| 273 | +- SEO URL Handle: `products/maternity-rainbow-stripe-skinny-bodycon-dress_5027` | |
| 274 | + | |
| 275 | +**状态设置:** | |
| 276 | +- 商品上架: `Y` | |
| 277 | +- 需要物流: `Y` | |
| 278 | +- 商品收税: `N` | |
| 279 | +- 启用虚拟销量: `N` | |
| 280 | +- 虚拟销量值: `100` | |
| 281 | +- 跟踪库存: `N` | |
| 282 | +- 库存规则*: `2` | |
| 283 | + | |
| 284 | +**分类信息:** | |
| 285 | +- 专辑名称: `头戴式耳机` | |
| 286 | +- 标签: `耳机,头戴式,爆款` | |
| 287 | +- 供应商名称: `Amazon` | |
| 288 | +- 供应商URL: `https://www.amazon.com/Jabra-Move-Wireless-Stereo-Headphones/dp/B00MR8Z28S/ref=br_asw_pdt-2?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=&pf_rd_r=8NAPN9R973J85P2KDX4G&pf_rd_t=36701&pf_rd_p=a08731ea-e1c2-4f7f-a56e...` | |
| 289 | + | |
| 290 | +**价格库存:** | |
| 291 | +- 商品售价*: `49.99` | |
| 292 | +- 商品原价: `99.99` | |
| 293 | +- 商品SKU: `B00MR8Z28S` | |
| 294 | +- 商品重量: `0.3` | |
| 295 | +- 重量单位: `lb` | |
| 296 | +- 商品库存: `500` | |
| 297 | + | |
| 298 | +**其他信息:** | |
| 299 | +- 商品图片*: `https://images-na.ssl-images-amazon.com/images/I/416k5ZUd6lL.jpg` | |
| 300 | + | |
| 301 | +--- | |
| 302 | + | |
| 303 | + | |
| 304 | +## 重要说明 | |
| 305 | + | |
| 306 | +### 商品属性字段说明 | |
| 307 | + | |
| 308 | +商品属性字段是必填项,用于标识商品类型: | |
| 309 | + | |
| 310 | +- **M (商品主体)**: 多款式商品的首行,填写商品主体信息 | |
| 311 | + - 需要填写:商品标题、描述、SEO信息、分类等商品主体信息 | |
| 312 | + - 不需要填写:价格、库存、SKU等子款式信息 | |
| 313 | + | |
| 314 | +- **P (子款式)**: 多款式商品的非首行,填写子款式信息 | |
| 315 | + - 需要填写:商品标题(与M行一致)、款式值、价格、库存、SKU等 | |
| 316 | + - 不需要填写:商品描述、SEO信息等商品主体信息 | |
| 317 | + | |
| 318 | +- **S (单一款式商品)**: 单一款式商品,一行包含所有信息 | |
| 319 | + - 需要填写:所有商品信息,包括标题、描述、价格、库存等 | |
| 320 | + | |
| 321 | +### 字段填写规则 | |
| 322 | + | |
| 323 | +1. **商品ID**: 系统自动生成,新增商品无需填写 | |
| 324 | +2. **商品标题***: 必填,最多255字符。同一商品的子款式标题必须与商品主体标题一致 | |
| 325 | +3. **商品属性***: 必填,只能填写 M、P 或 S | |
| 326 | +4. **商品售价***: 必填,最多9位正整数,2位小数 | |
| 327 | +5. **商品SKU**: 必填,最多255字符 | |
| 328 | +6. **商品图片***: 必填(M和S类型),可填写多个图片URL,用逗号分隔 | |
| 329 | +7. **库存规则***: 当跟踪库存为Y时必填,可选值:1(库存为0允许购买)、2(库存为0不允许购买)、3(库存为0自动下架) | |
| 330 | + | |
| 331 | +### 数据格式要求 | |
| 332 | + | |
| 333 | +- 日期时间格式:`YYYY-MM-DD HH:MM:SS` | |
| 334 | +- 布尔值:使用 `Y` 或 `N` | |
| 335 | +- 多个值:使用英文逗号分隔 | |
| 336 | +- HTML内容:商品描述支持HTML代码 | |
| 337 | +- URL格式:图片URL和供应商URL需为完整URL | |
| 338 | + | |
| 339 | +### 注意事项 | |
| 340 | + | |
| 341 | +1. 同一商品的子款式行必须紧跟在商品主体行之后,中间不能插入其他商品 | |
| 342 | +2. 商品标题在所有子款式行中必须保持一致 | |
| 343 | +3. 图片URL必须是可访问的完整URL | |
| 344 | +4. 价格、库存等数值字段不能包含非数字字符(除小数点外) | |
| 345 | +5. 导入前请确保所有必填字段都已正确填写 | |
| 346 | + | ... | ... |
docs/索引字段说明.md
| ... | ... | @@ -16,10 +16,127 @@ |
| 16 | 16 | - 返回的结果格式约定为店匠系列的 SPU/SKU嵌套结构。 |
| 17 | 17 | - 支撑 facet/过滤/排序业务需求:用户可以选择任何一个 keyword 或 HKText 类型的字段做筛选、聚合;也可以选择任何一个数值型字段做 Range 过滤或排序。 |
| 18 | 18 | |
| 19 | -## 数据源调研 | |
| 19 | +## 关键字段 | |
| 20 | 20 | |
| 21 | -店匠的商品结构: | |
| 21 | +参考1:spu表 & sku表、数据源《商品导入模板》 | |
| 22 | +### SPU表(shoplazza_product_spu) | |
| 23 | + | |
| 24 | +主要字段: | |
| 25 | +- `id`: BIGINT - 主键ID | |
| 26 | +- `tenant_id`: BIGINT - 租户ID | |
| 27 | +- `handle`: VARCHAR(255) - URL handle | |
| 28 | +- `title`: VARCHAR(512) - 商品标题 | |
| 29 | +- `brief`: VARCHAR(512) - 商品简介 | |
| 30 | +- `description`: TEXT - 商品描述 | |
| 31 | +- `vendor`: VARCHAR(255) - 供应商/品牌 | |
| 32 | +- `category`: VARCHAR(255) - 类目 | |
| 33 | +- `tags`: VARCHAR(1024) - 标签 | |
| 34 | +- `seo_title`: VARCHAR(512) - SEO标题 | |
| 35 | +- `seo_description`: TEXT - SEO描述 | |
| 36 | +- `seo_keywords`: VARCHAR(1024) - SEO关键词 | |
| 37 | +- `image_src`: VARCHAR(500) - 图片URL | |
| 38 | +- `create_time`: DATETIME - 创建时间 | |
| 39 | +- `update_time`: DATETIME - 更新时间 | |
| 40 | +- `shoplazza_created_at`: DATETIME - 店匠创建时间 | |
| 41 | +- `shoplazza_updated_at`: DATETIME - 店匠更新时间 | |
| 42 | + | |
| 43 | +spu表全部字段 | |
| 44 | +"Field" "Type" "Null" "Key" "Default" "Extra" | |
| 45 | +"id" "bigint(20)" "NO" "PRI" "auto_increment" | |
| 46 | +"shop_id" "bigint(20)" "NO" "MUL" "" | |
| 47 | +"shoplazza_id" "varchar(64)" "NO" "" "" | |
| 48 | +"handle" "varchar(255)" "YES" "MUL" "" | |
| 49 | +"title" "varchar(500)" "NO" "" "" | |
| 50 | +"brief" "varchar(1000)" "YES" "" "" | |
| 51 | +"description" "text" "YES" "" "" | |
| 52 | +"spu" "varchar(100)" "YES" "" "" | |
| 53 | +"vendor" "varchar(255)" "YES" "" "" | |
| 54 | +"vendor_url" "varchar(500)" "YES" "" "" | |
| 55 | +"seo_title" "varchar(500)" "YES" "" "" | |
| 56 | +"seo_description" "text" "YES" "" "" | |
| 57 | +"seo_keywords" "text" "YES" "" "" | |
| 58 | +"image_src" "varchar(500)" "YES" "" "" | |
| 59 | +"image_width" "int(11)" "YES" "" "" | |
| 60 | +"image_height" "int(11)" "YES" "" "" | |
| 61 | +"image_path" "varchar(255)" "YES" "" "" | |
| 62 | +"image_alt" "varchar(500)" "YES" "" "" | |
| 63 | +"inventory_policy" "varchar(50)" "YES" "" "" | |
| 64 | +"inventory_quantity" "int(11)" "YES" "" "0" "" | |
| 65 | +"inventory_tracking" "tinyint(1)" "YES" "" "0" "" | |
| 66 | +"published" "tinyint(1)" "YES" "" "0" "" | |
| 67 | +"published_at" "datetime" "YES" "MUL" "" | |
| 68 | +"requires_shipping" "tinyint(1)" "YES" "" "1" "" | |
| 69 | +"taxable" "tinyint(1)" "YES" "" "0" "" | |
| 70 | +"fake_sales" "int(11)" "YES" "" "0" "" | |
| 71 | +"display_fake_sales" "tinyint(1)" "YES" "" "0" "" | |
| 72 | +"mixed_wholesale" "tinyint(1)" "YES" "" "0" "" | |
| 73 | +"need_variant_image" "tinyint(1)" "YES" "" "0" "" | |
| 74 | +"has_only_default_variant" "tinyint(1)" "YES" "" "0" "" | |
| 75 | +"tags" "text" "YES" "" "" | |
| 76 | +"note" "text" "YES" "" "" | |
| 77 | +"category" "varchar(255)" "YES" "" "" | |
| 78 | +"shoplazza_created_at" "datetime" "YES" "" "" | |
| 79 | +"shoplazza_updated_at" "datetime" "YES" "MUL" "" | |
| 80 | +"tenant_id" "bigint(20)" "NO" "MUL" "" | |
| 81 | +"creator" "varchar(64)" "YES" "" "" "" | |
| 82 | +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "" | |
| 83 | +"updater" "varchar(64)" "YES" "" "" "" | |
| 84 | +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP" | |
| 85 | +"deleted" "bit(1)" "NO" "" "b'0'" "" | |
| 22 | 86 | |
| 87 | + | |
| 88 | +### SKU表(shoplazza_product_sku) | |
| 89 | + | |
| 90 | +主要字段: | |
| 91 | +- `id`: BIGINT - 主键ID(对应variant_id) | |
| 92 | +- `spu_id`: BIGINT - SPU ID(关联字段) | |
| 93 | +- `title`: VARCHAR(500) - 变体标题 | |
| 94 | +- `price`: DECIMAL(10,2) - 价格 | |
| 95 | +- `compare_at_price`: DECIMAL(10,2) - 原价 | |
| 96 | +- `sku`: VARCHAR(100) - SKU编码 | |
| 97 | +- `inventory_quantity`: INT(11) - 库存数量 | |
| 98 | +- `option1`: VARCHAR(255) - 选项1 | |
| 99 | +- `option2`: VARCHAR(255) - 选项2 | |
| 100 | +- `option3`: VARCHAR(255) - 选项3 | |
| 101 | + | |
| 102 | +sku全部字段 | |
| 103 | +"Field" "Type" "Null" "Key" "Default" "Extra" | |
| 104 | +"id" "bigint(20)" "NO" "PRI" "auto_increment" | |
| 105 | +"spu_id" "bigint(20)" "NO" "MUL" "" | |
| 106 | +"shop_id" "bigint(20)" "NO" "MUL" "" | |
| 107 | +"shoplazza_id" "varchar(64)" "NO" "" "" | |
| 108 | +"shoplazza_product_id" "varchar(64)" "NO" "MUL" "" | |
| 109 | +"shoplazza_image_id" "varchar(64)" "YES" "" "" | |
| 110 | +"title" "varchar(500)" "YES" "" "" | |
| 111 | +"sku" "varchar(100)" "YES" "MUL" "" | |
| 112 | +"barcode" "varchar(100)" "YES" "" "" | |
| 113 | +"position" "int(11)" "YES" "" "0" "" | |
| 114 | +"price" "decimal(10,2)" "YES" "" "" | |
| 115 | +"compare_at_price" "decimal(10,2)" "YES" "" "" | |
| 116 | +"cost_price" "decimal(10,2)" "YES" "" "" | |
| 117 | +"option1" "varchar(255)" "YES" "" "" | |
| 118 | +"option2" "varchar(255)" "YES" "" "" | |
| 119 | +"option3" "varchar(255)" "YES" "" "" | |
| 120 | +"inventory_quantity" "int(11)" "YES" "" "0" "" | |
| 121 | +"weight" "decimal(10,2)" "YES" "" "" | |
| 122 | +"weight_unit" "varchar(10)" "YES" "" "" | |
| 123 | +"image_src" "varchar(500)" "YES" "" "" | |
| 124 | +"wholesale_price" "json" "YES" "" "" | |
| 125 | +"note" "text" "YES" "" "" | |
| 126 | +"extend" "json" "YES" "" "" | |
| 127 | +"shoplazza_created_at" "datetime" "YES" "" "" | |
| 128 | +"shoplazza_updated_at" "datetime" "YES" "" "" | |
| 129 | +"tenant_id" "bigint(20)" "NO" "MUL" "" | |
| 130 | +"creator" "varchar(64)" "YES" "" "" "" | |
| 131 | +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "" | |
| 132 | +"updater" "varchar(64)" "YES" "" "" "" | |
| 133 | +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP" | |
| 134 | +"deleted" "bit(1)" "NO" "" "b'0'" "" | |
| 135 | + | |
| 136 | +参考2:波哥 索引《店匠指南》 -> 商品详解 | |
| 137 | + | |
| 138 | + | |
| 139 | +参考3: 店匠的商品结构 - 民丰: | |
| 23 | 140 | 固定字段: |
| 24 | 141 | 1、必填:品名 |
| 25 | 142 | 2、非必填:副标题、类目、专辑、标签、供应商、市场 |
| ... | ... | @@ -33,7 +150,110 @@ |
| 33 | 150 | |
| 34 | 151 | 商品信息:品名、颜色、尺码,基本上就这三个信息 |
| 35 | 152 | |
| 36 | -spu/sku表: | |
| 153 | + | |
| 154 | +### 分类 | |
| 155 | +最多三级分类,商品可以指向任何级别的分类 | |
| 156 | +Field Type | |
| 157 | +category varchar(255) | |
| 158 | +category_id bigint(20) | |
| 159 | +category_google_id bigint(20) | |
| 160 | +category_level int(11) | |
| 161 | +category_path varchar(500) | |
| 162 | + | |
| 163 | + | |
| 164 | +### 属性 | |
| 165 | +1. 组织ES输入数据的时候,需要为sku拼接spu的 option1 option2 option3,作为属性名称(比如“颜色”),sku的 option1 option2 option3 作为属性值(比如“白色”) | |
| 166 | +2. 有以下方案: TODO 可以选择其中一种,或者2用于填充3用于搜索: | |
| 167 | +1)铺平展开,只支持三个 | |
| 168 | +attr1 | |
| 169 | +attr2 | |
| 170 | +attr3 | |
| 171 | +option1 | |
| 172 | +option2 | |
| 173 | +option3 | |
| 174 | +2)nested,支持多个,动态。 查询性能低 | |
| 175 | +"specifications": { | |
| 176 | + "type": "nested", | |
| 177 | + "properties": { | |
| 178 | + "name": { "type": "keyword" }, // "颜色", "容量" | |
| 179 | + "value": { "type": "keyword" } // "白色", "256GB" | |
| 180 | + } | |
| 181 | +}, | |
| 182 | +3)平铺展开。写入时从 specifications 提取并填充这些字段,查询性能高。 | |
| 183 | +"properties": { | |
| 184 | + "color": { "type": "keyword" }, | |
| 185 | + "capacity": { "type": "keyword" }, | |
| 186 | + "network": { "type": "keyword" }, | |
| 187 | + "edition": { "type": "keyword" } | |
| 188 | +} | |
| 189 | + | |
| 190 | +### status | |
| 191 | +1. 商品下架等状态 | |
| 192 | +2. 无库存,是用status记录,提升查询效率 | |
| 193 | + | |
| 194 | +### 多语言 | |
| 195 | +索引:中英文(两套),如果商品资料是中文,则我们系统使用 谷歌翻译(和平台使用的翻译工具对齐) 自动翻译为英文。 | |
| 196 | +检索:将非中英文,翻译成英文后,再检索英文。 | |
| 197 | + | |
| 198 | +## 分面 | |
| 199 | +1. 分类 | |
| 200 | +2. 标签。这个是个扁平的结构,不是像属性那样k-v-pair的 | |
| 201 | +不用考虑属性。这个 option1 2 3不能放在外面筛选。他的定位是spu内部的东西,不是外部用于筛选商品的。 即使外面要有那种 动态筛选 比如搜手机出品牌、款式 这种, 不是对应的这个字段,会是另外的字段。 | |
| 202 | + | |
| 203 | + | |
| 204 | +### 字段预处理 | |
| 205 | +需要用「英文逗号」隔开,作为list输入的字段: | |
| 206 | +SEO关键词 | |
| 207 | +专辑名称 | |
| 208 | +标签 | |
| 209 | +尺寸信息 | |
| 210 | + | |
| 211 | + | |
| 212 | +## SPU 与 SKU 的协同设计 | |
| 213 | + | |
| 214 | +以下方案: | |
| 215 | +1. sku为索引单位。使用 collapse 按 spu_id 折叠 | |
| 216 | +需要考虑大量的字段冗余 | |
| 217 | + | |
| 218 | +2. spu为单位。 sku的title作为 spu 的sku_titles 属性。 | |
| 219 | + 除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有一个title。所以,可以以spu为单位,sku的title作为spu的一个字段,以list形式灌入,假设一个spu有三个sku,那么这个sku_titles字段有三个值,打分的时候按max取得打分,并且我们可以得到这三个sku的title匹配的得分,因此好决定sku的排序。 | |
| 220 | + | |
| 221 | +3. sku 作为nested | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | +参考 [](https://blog.csdn.net/csdn_tom_168/article/details/150432666) | |
| 226 | +方案一:独立索引 | |
| 227 | +spu-catalog-*:用于品牌、类目、商品介绍等宏观搜索。 | |
| 228 | +sku-catalog-*:用于具体规格搜索、下单。 | |
| 229 | +优势:职责分离,查询更高效。 | |
| 230 | + | |
| 231 | +方案二:联合查询 | |
| 232 | +GET /spu-read,sku-read/_search | |
| 233 | +适用于“模糊搜索 → 跳转详情页”的场景。 | |
| 234 | + | |
| 235 | +方案三:父子文档(不推荐) | |
| 236 | +join 类型维护 SPU-SKU 关系。 | |
| 237 | +性能差,维护复杂,不适用于高并发搜索场景。 | |
| 238 | + | |
| 239 | + | |
| 240 | +## rank - 相关性 | |
| 241 | + | |
| 242 | + | |
| 243 | +## rank - 提权 | |
| 244 | + | |
| 245 | +function_score 提升相关性 | |
| 246 | +```json | |
| 247 | +"function_score": { | |
| 248 | + "functions": [ | |
| 249 | + { "field_value_factor": { "field": "sales_count", "factor": 0.001, "modifier": "log1p" } }, | |
| 250 | + { "gauss": { "listed_at": { "scale": "30d" } } } | |
| 251 | + ], | |
| 252 | + "boost_mode": "multiply" | |
| 253 | +} | |
| 254 | +``` | |
| 255 | + | |
| 256 | + | |
| 37 | 257 | |
| 38 | 258 | |
| 39 | 259 | ## 索引基本信息 |
| ... | ... | @@ -42,6 +262,17 @@ spu/sku表: |
| 42 | 262 | - **索引级别**: SPU级别(商品级别) |
| 43 | 263 | - **数据结构**: SPU文档包含嵌套的skus数组 |
| 44 | 264 | |
| 265 | + | |
| 266 | +## 分片与副本设置 | |
| 267 | +```json | |
| 268 | +# 分片数: 根据cpu数量和sku数量决定。 | |
| 269 | +"settings": { | |
| 270 | + "number_of_shards": 8, | |
| 271 | + "number_of_replicas": 1, | |
| 272 | + "refresh_interval": "15s" | |
| 273 | +} | |
| 274 | +``` | |
| 275 | + | |
| 45 | 276 | ## 索引类型与处理说明 |
| 46 | 277 | |
| 47 | 278 | ### 文本字段 |
| ... | ... | @@ -186,7 +417,7 @@ spu/sku表: |
| 186 | 417 | | 索引字段名 | ES字段类型 | 是否索引 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 数据预处理 | 说明 | |
| 187 | 418 | |-----------|-----------|---------|-----------|-----------|-------------|-----------|---------|-------------|------| |
| 188 | 419 | | vendor | HKText | 是 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | | 供应商/品牌,HKText字段自动提供 `vendor.keyword` 用于过滤、聚合 | |
| 189 | -| tags | HKText | 是 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | | 标签字段,支持模糊搜索;使用 `tags.keyword` 进行精确过滤 | | |
| 420 | +| tags | HKText | 是 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 按逗号分割为list | 标签字段,支持模糊搜索;使用 `tags.keyword` 进行精确过滤 | | |
| 190 | 421 | | category | HKText | 是 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | | 类目字段,使用 `category.keyword` 进行过滤/分面 | |
| 191 | 422 | |
| 192 | 423 | ### 价格字段 |
| ... | ... | @@ -306,124 +537,6 @@ spu/sku表: |
| 306 | 537 | 4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用 |
| 307 | 538 | 5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序 |
| 308 | 539 | |
| 309 | -## 数据来源表结构 | |
| 310 | - | |
| 311 | -### SPU表(shoplazza_product_spu) | |
| 312 | - | |
| 313 | -主要字段: | |
| 314 | -- `id`: BIGINT - 主键ID | |
| 315 | -- `tenant_id`: BIGINT - 租户ID | |
| 316 | -- `handle`: VARCHAR(255) - URL handle | |
| 317 | -- `title`: VARCHAR(512) - 商品标题 | |
| 318 | -- `brief`: VARCHAR(512) - 商品简介 | |
| 319 | -- `description`: TEXT - 商品描述 | |
| 320 | -- `vendor`: VARCHAR(255) - 供应商/品牌 | |
| 321 | -- `category`: VARCHAR(255) - 类目 | |
| 322 | -- `tags`: VARCHAR(1024) - 标签 | |
| 323 | -- `seo_title`: VARCHAR(512) - SEO标题 | |
| 324 | -- `seo_description`: TEXT - SEO描述 | |
| 325 | -- `seo_keywords`: VARCHAR(1024) - SEO关键词 | |
| 326 | -- `image_src`: VARCHAR(500) - 图片URL | |
| 327 | -- `create_time`: DATETIME - 创建时间 | |
| 328 | -- `update_time`: DATETIME - 更新时间 | |
| 329 | -- `shoplazza_created_at`: DATETIME - 店匠创建时间 | |
| 330 | -- `shoplazza_updated_at`: DATETIME - 店匠更新时间 | |
| 331 | - | |
| 332 | -spu表全部字段 | |
| 333 | -"Field" "Type" "Null" "Key" "Default" "Extra" | |
| 334 | -"id" "bigint(20)" "NO" "PRI" "auto_increment" | |
| 335 | -"shop_id" "bigint(20)" "NO" "MUL" "" | |
| 336 | -"shoplazza_id" "varchar(64)" "NO" "" "" | |
| 337 | -"handle" "varchar(255)" "YES" "MUL" "" | |
| 338 | -"title" "varchar(500)" "NO" "" "" | |
| 339 | -"brief" "varchar(1000)" "YES" "" "" | |
| 340 | -"description" "text" "YES" "" "" | |
| 341 | -"spu" "varchar(100)" "YES" "" "" | |
| 342 | -"vendor" "varchar(255)" "YES" "" "" | |
| 343 | -"vendor_url" "varchar(500)" "YES" "" "" | |
| 344 | -"seo_title" "varchar(500)" "YES" "" "" | |
| 345 | -"seo_description" "text" "YES" "" "" | |
| 346 | -"seo_keywords" "text" "YES" "" "" | |
| 347 | -"image_src" "varchar(500)" "YES" "" "" | |
| 348 | -"image_width" "int(11)" "YES" "" "" | |
| 349 | -"image_height" "int(11)" "YES" "" "" | |
| 350 | -"image_path" "varchar(255)" "YES" "" "" | |
| 351 | -"image_alt" "varchar(500)" "YES" "" "" | |
| 352 | -"inventory_policy" "varchar(50)" "YES" "" "" | |
| 353 | -"inventory_quantity" "int(11)" "YES" "" "0" "" | |
| 354 | -"inventory_tracking" "tinyint(1)" "YES" "" "0" "" | |
| 355 | -"published" "tinyint(1)" "YES" "" "0" "" | |
| 356 | -"published_at" "datetime" "YES" "MUL" "" | |
| 357 | -"requires_shipping" "tinyint(1)" "YES" "" "1" "" | |
| 358 | -"taxable" "tinyint(1)" "YES" "" "0" "" | |
| 359 | -"fake_sales" "int(11)" "YES" "" "0" "" | |
| 360 | -"display_fake_sales" "tinyint(1)" "YES" "" "0" "" | |
| 361 | -"mixed_wholesale" "tinyint(1)" "YES" "" "0" "" | |
| 362 | -"need_variant_image" "tinyint(1)" "YES" "" "0" "" | |
| 363 | -"has_only_default_variant" "tinyint(1)" "YES" "" "0" "" | |
| 364 | -"tags" "text" "YES" "" "" | |
| 365 | -"note" "text" "YES" "" "" | |
| 366 | -"category" "varchar(255)" "YES" "" "" | |
| 367 | -"shoplazza_created_at" "datetime" "YES" "" "" | |
| 368 | -"shoplazza_updated_at" "datetime" "YES" "MUL" "" | |
| 369 | -"tenant_id" "bigint(20)" "NO" "MUL" "" | |
| 370 | -"creator" "varchar(64)" "YES" "" "" "" | |
| 371 | -"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "" | |
| 372 | -"updater" "varchar(64)" "YES" "" "" "" | |
| 373 | -"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP" | |
| 374 | -"deleted" "bit(1)" "NO" "" "b'0'" "" | |
| 375 | - | |
| 376 | - | |
| 377 | - | |
| 378 | - | |
| 379 | -### SKU表(shoplazza_product_sku) | |
| 380 | - | |
| 381 | -主要字段: | |
| 382 | -- `id`: BIGINT - 主键ID(对应variant_id) | |
| 383 | -- `spu_id`: BIGINT - SPU ID(关联字段) | |
| 384 | -- `title`: VARCHAR(500) - 变体标题 | |
| 385 | -- `price`: DECIMAL(10,2) - 价格 | |
| 386 | -- `compare_at_price`: DECIMAL(10,2) - 原价 | |
| 387 | -- `sku`: VARCHAR(100) - SKU编码 | |
| 388 | -- `inventory_quantity`: INT(11) - 库存数量 | |
| 389 | -- `option1`: VARCHAR(255) - 选项1 | |
| 390 | -- `option2`: VARCHAR(255) - 选项2 | |
| 391 | -- `option3`: VARCHAR(255) - 选项3 | |
| 392 | - | |
| 393 | -sku全部字段 | |
| 394 | -"Field" "Type" "Null" "Key" "Default" "Extra" | |
| 395 | -"id" "bigint(20)" "NO" "PRI" "auto_increment" | |
| 396 | -"spu_id" "bigint(20)" "NO" "MUL" "" | |
| 397 | -"shop_id" "bigint(20)" "NO" "MUL" "" | |
| 398 | -"shoplazza_id" "varchar(64)" "NO" "" "" | |
| 399 | -"shoplazza_product_id" "varchar(64)" "NO" "MUL" "" | |
| 400 | -"shoplazza_image_id" "varchar(64)" "YES" "" "" | |
| 401 | -"title" "varchar(500)" "YES" "" "" | |
| 402 | -"sku" "varchar(100)" "YES" "MUL" "" | |
| 403 | -"barcode" "varchar(100)" "YES" "" "" | |
| 404 | -"position" "int(11)" "YES" "" "0" "" | |
| 405 | -"price" "decimal(10,2)" "YES" "" "" | |
| 406 | -"compare_at_price" "decimal(10,2)" "YES" "" "" | |
| 407 | -"cost_price" "decimal(10,2)" "YES" "" "" | |
| 408 | -"option1" "varchar(255)" "YES" "" "" | |
| 409 | -"option2" "varchar(255)" "YES" "" "" | |
| 410 | -"option3" "varchar(255)" "YES" "" "" | |
| 411 | -"inventory_quantity" "int(11)" "YES" "" "0" "" | |
| 412 | -"weight" "decimal(10,2)" "YES" "" "" | |
| 413 | -"weight_unit" "varchar(10)" "YES" "" "" | |
| 414 | -"image_src" "varchar(500)" "YES" "" "" | |
| 415 | -"wholesale_price" "json" "YES" "" "" | |
| 416 | -"note" "text" "YES" "" "" | |
| 417 | -"extend" "json" "YES" "" "" | |
| 418 | -"shoplazza_created_at" "datetime" "YES" "" "" | |
| 419 | -"shoplazza_updated_at" "datetime" "YES" "" "" | |
| 420 | -"tenant_id" "bigint(20)" "NO" "MUL" "" | |
| 421 | -"creator" "varchar(64)" "YES" "" "" "" | |
| 422 | -"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "" | |
| 423 | -"updater" "varchar(64)" "YES" "" "" "" | |
| 424 | -"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP" | |
| 425 | -"deleted" "bit(1)" "NO" "" "b'0'" "" | |
| 426 | - | |
| 427 | 540 | |
| 428 | 541 | ## TODO |
| 429 | 542 | 多语言问题。 | ... | ... |
| ... | ... | @@ -0,0 +1,355 @@ |
| 1 | +#!/usr/bin/env python3 | |
| 2 | +""" | |
| 3 | +Convert CSV data to Excel import template. | |
| 4 | + | |
| 5 | +Reads CSV file (goods_with_pic.5years_congku.csv.shuf.1w) and generates Excel file | |
| 6 | +based on the template format (商品导入模板.xlsx). | |
| 7 | + | |
| 8 | +Each CSV row corresponds to 1 SPU and 1 SKU, which will be exported as a single | |
| 9 | +S (Single variant) row in the Excel template. | |
| 10 | +""" | |
| 11 | + | |
| 12 | +import sys | |
| 13 | +import os | |
| 14 | +import csv | |
| 15 | +import random | |
| 16 | +import argparse | |
| 17 | +import re | |
| 18 | +from pathlib import Path | |
| 19 | +from datetime import datetime, timedelta | |
| 20 | +import pandas as pd | |
| 21 | +from openpyxl import load_workbook | |
| 22 | +from openpyxl.styles import Font, Alignment | |
| 23 | +from openpyxl.utils import get_column_letter | |
| 24 | + | |
| 25 | +# Add parent directory to path | |
| 26 | +sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| 27 | + | |
| 28 | + | |
| 29 | +def clean_value(value): | |
| 30 | + """ | |
| 31 | + Clean and normalize value. | |
| 32 | + | |
| 33 | + Args: | |
| 34 | + value: Value to clean | |
| 35 | + | |
| 36 | + Returns: | |
| 37 | + Cleaned string value | |
| 38 | + """ | |
| 39 | + if value is None: | |
| 40 | + return '' | |
| 41 | + value = str(value).strip() | |
| 42 | + # Remove surrounding quotes | |
| 43 | + if value.startswith('"') and value.endswith('"'): | |
| 44 | + value = value[1:-1] | |
| 45 | + return value | |
| 46 | + | |
| 47 | + | |
| 48 | +def parse_csv_row(row: dict) -> dict: | |
| 49 | + """ | |
| 50 | + Parse CSV row and extract fields. | |
| 51 | + | |
| 52 | + Args: | |
| 53 | + row: CSV row dictionary | |
| 54 | + | |
| 55 | + Returns: | |
| 56 | + Parsed data dictionary | |
| 57 | + """ | |
| 58 | + return { | |
| 59 | + 'skuId': clean_value(row.get('skuId', '')), | |
| 60 | + 'name': clean_value(row.get('name', '')), | |
| 61 | + 'name_pinyin': clean_value(row.get('name_pinyin', '')), | |
| 62 | + 'create_time': clean_value(row.get('create_time', '')), | |
| 63 | + 'ruSkuName': clean_value(row.get('ruSkuName', '')), | |
| 64 | + 'enSpuName': clean_value(row.get('enSpuName', '')), | |
| 65 | + 'categoryName': clean_value(row.get('categoryName', '')), | |
| 66 | + 'supplierName': clean_value(row.get('supplierName', '')), | |
| 67 | + 'brandName': clean_value(row.get('brandName', '')), | |
| 68 | + 'file_id': clean_value(row.get('file_id', '')), | |
| 69 | + 'days_since_last_update': clean_value(row.get('days_since_last_update', '')), | |
| 70 | + 'id': clean_value(row.get('id', '')), | |
| 71 | + 'imageUrl': clean_value(row.get('imageUrl', '')) | |
| 72 | + } | |
| 73 | + | |
| 74 | + | |
| 75 | +def generate_handle(title: str) -> str: | |
| 76 | + """ | |
| 77 | + Generate URL-friendly handle from title. | |
| 78 | + | |
| 79 | + Args: | |
| 80 | + title: Product title | |
| 81 | + | |
| 82 | + Returns: | |
| 83 | + URL-friendly handle (ASCII only) | |
| 84 | + """ | |
| 85 | + # Convert to lowercase | |
| 86 | + handle = title.lower() | |
| 87 | + | |
| 88 | + # Remove non-ASCII characters, keep only letters, numbers, spaces, and hyphens | |
| 89 | + handle = re.sub(r'[^a-z0-9\s-]', '', handle) | |
| 90 | + | |
| 91 | + # Replace spaces and multiple hyphens with single hyphen | |
| 92 | + handle = re.sub(r'[-\s]+', '-', handle) | |
| 93 | + handle = handle.strip('-') | |
| 94 | + | |
| 95 | + # Limit length | |
| 96 | + if len(handle) > 255: | |
| 97 | + handle = handle[:255] | |
| 98 | + | |
| 99 | + return handle or 'product' | |
| 100 | + | |
| 101 | + | |
| 102 | +def read_csv_file(csv_file: str) -> list: | |
| 103 | + """ | |
| 104 | + Read CSV file and return list of parsed rows. | |
| 105 | + | |
| 106 | + Args: | |
| 107 | + csv_file: Path to CSV file | |
| 108 | + | |
| 109 | + Returns: | |
| 110 | + List of parsed CSV data dictionaries | |
| 111 | + """ | |
| 112 | + csv_data_list = [] | |
| 113 | + | |
| 114 | + with open(csv_file, 'r', encoding='utf-8') as f: | |
| 115 | + reader = csv.DictReader(f) | |
| 116 | + for row in reader: | |
| 117 | + parsed = parse_csv_row(row) | |
| 118 | + csv_data_list.append(parsed) | |
| 119 | + | |
| 120 | + return csv_data_list | |
| 121 | + | |
| 122 | + | |
| 123 | +def csv_to_excel_row(csv_data: dict) -> dict: | |
| 124 | + """ | |
| 125 | + Convert CSV data row to Excel template row. | |
| 126 | + | |
| 127 | + Each CSV row represents a single product with one variant (S type in Excel). | |
| 128 | + | |
| 129 | + Args: | |
| 130 | + csv_data: Parsed CSV row data | |
| 131 | + | |
| 132 | + Returns: | |
| 133 | + Dictionary mapping Excel column names to values | |
| 134 | + """ | |
| 135 | + # Parse create_time | |
| 136 | + try: | |
| 137 | + created_at = datetime.strptime(csv_data['create_time'], '%Y-%m-%d %H:%M:%S') | |
| 138 | + create_time_str = created_at.strftime('%Y-%m-%d %H:%M:%S') | |
| 139 | + except: | |
| 140 | + created_at = datetime.now() - timedelta(days=random.randint(1, 365)) | |
| 141 | + create_time_str = created_at.strftime('%Y-%m-%d %H:%M:%S') | |
| 142 | + | |
| 143 | + # Generate title - use name or enSpuName | |
| 144 | + title = csv_data['name'] or csv_data['enSpuName'] or 'Product' | |
| 145 | + | |
| 146 | + # Generate handle - prefer enSpuName, then name_pinyin, then title | |
| 147 | + handle_source = csv_data['enSpuName'] or csv_data['name_pinyin'] or title | |
| 148 | + handle = generate_handle(handle_source) | |
| 149 | + if handle and not handle.startswith('products/'): | |
| 150 | + handle = f'products/{handle}' | |
| 151 | + | |
| 152 | + # Generate SEO fields | |
| 153 | + seo_title = f"{title} - {csv_data['categoryName']}" if csv_data['categoryName'] else title | |
| 154 | + seo_description = f"购买{csv_data['brandName']}{title}" if csv_data['brandName'] else title | |
| 155 | + seo_keywords_parts = [title] | |
| 156 | + if csv_data['categoryName']: | |
| 157 | + seo_keywords_parts.append(csv_data['categoryName']) | |
| 158 | + if csv_data['brandName']: | |
| 159 | + seo_keywords_parts.append(csv_data['brandName']) | |
| 160 | + seo_keywords = ','.join(seo_keywords_parts) | |
| 161 | + | |
| 162 | + # Generate tags from category and brand | |
| 163 | + tags_parts = [] | |
| 164 | + if csv_data['categoryName']: | |
| 165 | + tags_parts.append(csv_data['categoryName']) | |
| 166 | + if csv_data['brandName']: | |
| 167 | + tags_parts.append(csv_data['brandName']) | |
| 168 | + tags = ','.join(tags_parts) if tags_parts else '' | |
| 169 | + | |
| 170 | + # Generate prices (similar to import_tenant2_csv.py) | |
| 171 | + price = round(random.uniform(50, 500), 2) | |
| 172 | + compare_at_price = round(price * random.uniform(1.2, 1.5), 2) | |
| 173 | + cost_price = round(price * 0.6, 2) | |
| 174 | + | |
| 175 | + # Generate random stock | |
| 176 | + inventory_quantity = random.randint(0, 100) | |
| 177 | + | |
| 178 | + # Generate random weight | |
| 179 | + weight = round(random.uniform(0.1, 5.0), 2) | |
| 180 | + weight_unit = 'kg' | |
| 181 | + | |
| 182 | + # Use ruSkuName as SKU title, fallback to name | |
| 183 | + sku_title = csv_data['ruSkuName'] or csv_data['name'] or 'SKU' | |
| 184 | + | |
| 185 | + # Use skuId as SKU code | |
| 186 | + sku_code = csv_data['skuId'] or '' | |
| 187 | + | |
| 188 | + # Generate barcode | |
| 189 | + try: | |
| 190 | + sku_id = int(csv_data['skuId']) | |
| 191 | + barcode = f"BAR{sku_id:08d}" | |
| 192 | + except: | |
| 193 | + barcode = '' | |
| 194 | + | |
| 195 | + # Build description | |
| 196 | + description = f"<p>{csv_data['name']}</p>" if csv_data['name'] else '' | |
| 197 | + | |
| 198 | + # Build brief (subtitle) | |
| 199 | + brief = csv_data['name'] or '' | |
| 200 | + | |
| 201 | + # Excel row data (mapping to Excel template columns) | |
| 202 | + excel_row = { | |
| 203 | + '商品ID': '', # Empty for new products | |
| 204 | + '创建时间': create_time_str, | |
| 205 | + '商品标题*': title, | |
| 206 | + '商品属性*': 'S', # Single variant product | |
| 207 | + '商品副标题': brief, | |
| 208 | + '商品描述': description, | |
| 209 | + 'SEO标题': seo_title, | |
| 210 | + 'SEO描述': seo_description, | |
| 211 | + 'SEO URL Handle': handle, | |
| 212 | + 'SEO URL 重定向': 'N', # Default to N | |
| 213 | + 'SEO关键词': seo_keywords, | |
| 214 | + '商品上架': 'Y', # Published by default | |
| 215 | + '需要物流': 'Y', # Requires shipping | |
| 216 | + '商品收税': 'N', # Not taxable by default | |
| 217 | + '商品spu': '', # Empty | |
| 218 | + '启用虚拟销量': 'N', # No fake sales | |
| 219 | + '虚拟销量值': '', # Empty | |
| 220 | + '跟踪库存': 'Y', # Track inventory | |
| 221 | + '库存规则*': '1', # Allow purchase when stock is 0 | |
| 222 | + '专辑名称': csv_data['categoryName'] or '', # Category as album | |
| 223 | + '标签': tags, | |
| 224 | + '供应商名称': csv_data['supplierName'] or '', | |
| 225 | + '供应商URL': '', # Empty | |
| 226 | + '款式1': '', # Not used for S type | |
| 227 | + '款式2': '', # Not used for S type | |
| 228 | + '款式3': '', # Not used for S type | |
| 229 | + '商品售价*': price, | |
| 230 | + '商品原价': compare_at_price, | |
| 231 | + '成本价': cost_price, | |
| 232 | + '商品SKU': sku_code, | |
| 233 | + '商品重量': weight, | |
| 234 | + '重量单位': weight_unit, | |
| 235 | + '商品条形码': barcode, | |
| 236 | + '商品库存': inventory_quantity, | |
| 237 | + '尺寸信息': '', # Empty | |
| 238 | + '原产地国别': '', # Empty | |
| 239 | + 'HS(协调制度)代码': '', # Empty | |
| 240 | + '商品图片*': csv_data['imageUrl'] or '', # Image URL | |
| 241 | + '商品备注': '', # Empty | |
| 242 | + '款式备注': '', # Empty | |
| 243 | + '商品主图': csv_data['imageUrl'] or '', # Main image URL | |
| 244 | + } | |
| 245 | + | |
| 246 | + return excel_row | |
| 247 | + | |
| 248 | + | |
| 249 | +def create_excel_from_template(template_file: str, output_file: str, csv_data_list: list): | |
| 250 | + """ | |
| 251 | + Create Excel file from template and fill with CSV data. | |
| 252 | + | |
| 253 | + Args: | |
| 254 | + template_file: Path to Excel template file | |
| 255 | + output_file: Path to output Excel file | |
| 256 | + csv_data_list: List of parsed CSV data dictionaries | |
| 257 | + """ | |
| 258 | + # Load template | |
| 259 | + wb = load_workbook(template_file) | |
| 260 | + ws = wb.active # Use the active sheet (Sheet4) | |
| 261 | + | |
| 262 | + # Find header row (row 2, index 1) | |
| 263 | + header_row_idx = 2 # Row 2 in Excel (1-based, but header is at index 1 in pandas) | |
| 264 | + | |
| 265 | + # Get column mapping from header row | |
| 266 | + column_mapping = {} | |
| 267 | + for col_idx in range(1, ws.max_column + 1): | |
| 268 | + cell_value = ws.cell(row=header_row_idx, column=col_idx).value | |
| 269 | + if cell_value: | |
| 270 | + column_mapping[cell_value] = col_idx | |
| 271 | + | |
| 272 | + # Start writing data from row 4 (after header and instructions) | |
| 273 | + data_start_row = 4 # Row 4 in Excel (1-based) | |
| 274 | + | |
| 275 | + # Clear existing data rows (from row 4 onwards, but keep header and instructions) | |
| 276 | + # Find the last row with data in the template | |
| 277 | + last_template_row = ws.max_row | |
| 278 | + if last_template_row >= data_start_row: | |
| 279 | + # Clear data rows (keep header and instruction rows) | |
| 280 | + for row in range(data_start_row, last_template_row + 1): | |
| 281 | + for col in range(1, ws.max_column + 1): | |
| 282 | + ws.cell(row=row, column=col).value = None | |
| 283 | + | |
| 284 | + # Convert CSV data to Excel rows | |
| 285 | + for row_idx, csv_data in enumerate(csv_data_list): | |
| 286 | + excel_row = csv_to_excel_row(csv_data) | |
| 287 | + excel_row_num = data_start_row + row_idx | |
| 288 | + | |
| 289 | + # Write each field to corresponding column | |
| 290 | + for field_name, col_idx in column_mapping.items(): | |
| 291 | + if field_name in excel_row: | |
| 292 | + cell = ws.cell(row=excel_row_num, column=col_idx) | |
| 293 | + value = excel_row[field_name] | |
| 294 | + cell.value = value | |
| 295 | + | |
| 296 | + # Set alignment for text fields | |
| 297 | + if isinstance(value, str): | |
| 298 | + cell.alignment = Alignment(vertical='top', wrap_text=True) | |
| 299 | + elif isinstance(value, (int, float)): | |
| 300 | + cell.alignment = Alignment(vertical='top') | |
| 301 | + | |
| 302 | + # Save workbook | |
| 303 | + wb.save(output_file) | |
| 304 | + print(f"Excel file created: {output_file}") | |
| 305 | + print(f" - Total rows: {len(csv_data_list)}") | |
| 306 | + | |
| 307 | + | |
| 308 | +def main(): | |
| 309 | + parser = argparse.ArgumentParser(description='Convert CSV data to Excel import template') | |
| 310 | + parser.add_argument('--csv-file', | |
| 311 | + default='data/customer1/goods_with_pic.5years_congku.csv.shuf.1w', | |
| 312 | + help='CSV file path (default: data/customer1/goods_with_pic.5years_congku.csv.shuf.1w)') | |
| 313 | + parser.add_argument('--template', | |
| 314 | + default='docs/商品导入模板.xlsx', | |
| 315 | + help='Excel template file path (default: docs/商品导入模板.xlsx)') | |
| 316 | + parser.add_argument('--output', | |
| 317 | + default='商品导入数据.xlsx', | |
| 318 | + help='Output Excel file path (default: 商品导入数据.xlsx)') | |
| 319 | + parser.add_argument('--limit', | |
| 320 | + type=int, | |
| 321 | + default=None, | |
| 322 | + help='Limit number of rows to process (default: all)') | |
| 323 | + | |
| 324 | + args = parser.parse_args() | |
| 325 | + | |
| 326 | + # Check if files exist | |
| 327 | + if not os.path.exists(args.csv_file): | |
| 328 | + print(f"Error: CSV file not found: {args.csv_file}") | |
| 329 | + sys.exit(1) | |
| 330 | + | |
| 331 | + if not os.path.exists(args.template): | |
| 332 | + print(f"Error: Template file not found: {args.template}") | |
| 333 | + sys.exit(1) | |
| 334 | + | |
| 335 | + # Read CSV file | |
| 336 | + print(f"Reading CSV file: {args.csv_file}") | |
| 337 | + csv_data_list = read_csv_file(args.csv_file) | |
| 338 | + print(f"Read {len(csv_data_list)} rows from CSV") | |
| 339 | + | |
| 340 | + # Limit rows if specified | |
| 341 | + if args.limit: | |
| 342 | + csv_data_list = csv_data_list[:args.limit] | |
| 343 | + print(f"Limited to {len(csv_data_list)} rows") | |
| 344 | + | |
| 345 | + # Create Excel file | |
| 346 | + print(f"Creating Excel file from template: {args.template}") | |
| 347 | + print(f"Output file: {args.output}") | |
| 348 | + create_excel_from_template(args.template, args.output, csv_data_list) | |
| 349 | + | |
| 350 | + print(f"\nDone! Generated {len(csv_data_list)} product rows in Excel file.") | |
| 351 | + | |
| 352 | + | |
| 353 | +if __name__ == '__main__': | |
| 354 | + main() | |
| 355 | + | ... | ... |
| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | +# 激活环境 | |
| 2 | +source /home/tw/miniconda3/etc/profile.d/conda.sh | |
| 3 | +conda activate searchengine | |
| 4 | + | |
| 5 | +# # 基本使用(生成所有数据) | |
| 6 | +# python scripts/csv_to_excel.py | |
| 7 | + | |
| 8 | +# # 指定输出文件 | |
| 9 | +# python scripts/csv_to_excel.py --output tenant3_imports.xlsx | |
| 10 | + | |
| 11 | +# # 限制处理行数(用于测试) | |
| 12 | +# python scripts/csv_to_excel.py --limit 100 | |
| 13 | + | |
| 14 | +# 指定CSV文件和模板文件 | |
| 15 | +python scripts/csv_to_excel.py \ | |
| 16 | + --csv-file data/customer1/goods_with_pic.5years_congku.csv.shuf.1w \ | |
| 17 | + --template docs/商品导入模板.xlsx \ | |
| 18 | + --output tenant3_imports.xlsx | |
| 0 | 19 | \ No newline at end of file | ... | ... |