Commit 39e63ad19d8868fec602ba933af34d676faf0790

Authored by tangwang
1 parent 1ad371d1

docs

docs/店匠app安装-前端开发部署-全流程-笔记.md 0 → 100644
... ... @@ -0,0 +1,1287 @@
  1 +# 店匠(Shoplazza)APP 开发完整指南
  2 +
  3 +## 参考
  4 +https://xp0y6kmku6.feishu.cn/wiki/UsFiwDyGgiDWDwkAe5jcKHbSnHd
  5 +
  6 +## 目录
  7 +
  8 +1. [概述](#概述)
  9 +2. [阶段 1:在店匠 Partner 后台创建新 APP](#阶段-1在店匠-partner-后台创建新-app)
  10 +3. [阶段 2:创建前端项目](#阶段-2创建前端项目)
  11 +4. [阶段 3:开发后端服务](#阶段-3开发后端服务)
  12 +5. [阶段 4:前端开发详解](#阶段-4前端开发详解)
  13 +6. [阶段 5:测试与部署](#阶段-5测试与部署)
  14 +7. [常见问题与最佳实践](#常见问题与最佳实践)
  15 +
  16 +---
  17 +
  18 +## 概述
  19 +
  20 +本指南将帮助您从零开始创建一个完整的店匠(Shoplazza)Public App,包括:
  21 +
  22 +- **前端**:使用 Liquid 模板和 Lessjs 组件库开发主题扩展
  23 +- **后端**:实现 OAuth 2.0 授权流程、HMAC 验证、API 调用
  24 +- **多语言支持**:支持 16 种语言的国际化
  25 +- **主题集成**:与店匠主题深度集成
  26 +
  27 +### 前置要求
  28 +
  29 +- Node.js 14.14.0 或更高版本
  30 +- [Shoplazza 合作伙伴账户](https://partners.shoplazza.com/)
  31 +- [Shoplazza CLI](https://www.shoplazza.dev/docs/overview-3)(用于部署)
  32 +- 后端服务(Java/Spring Boot 或 Node.js/Express)
  33 +- 测试店铺(用于开发测试)
  34 +
  35 +---
  36 +
  37 +## 阶段 1:在店匠 Partner 后台创建新 APP
  38 +
  39 +### 步骤 1.1:登录 Partner 后台
  40 +
  41 +1. 访问:https://partners.shoplazza.com/
  42 +2. 使用合作伙伴账号登录
  43 +3. 进入管理页面:https://partners.shoplazza.com/{partner_id}
  44 +
  45 +### 步骤 1.2:创建新 APP
  46 +
  47 +1. 左侧导航:**Apps**
  48 +2. 点击:**Create App** 或 **创建应用**
  49 +3. 填写信息:
  50 + - **App Name**:智能推荐1.0(或自定义名称)
  51 + - **App Type**:**Public App**(公共应用)
  52 + - **Category**:选择合适分类(如 Product Recommendations)
  53 +4. 点击:**Create** 或 **创建**
  54 +
  55 +### 步骤 1.3:记录关键信息
  56 +
  57 +系统会生成 **Client credentials**(APP 的公共标识符,OAuth 流程以及与 Shoplazza API 互动都会需要):
  58 +
  59 +- **Client ID**:例如 `x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY`
  60 +- **Client Secret**:例如 `LF71s5gqtPws2i08C9y43uIYXpF_c91ZnqRmS1z8Ekk`
  61 +
  62 +**⚠️ 重要**:请妥善保存这些凭证,后续无法再次查看 Client Secret。
  63 +
  64 +### 步骤 1.4:配置 APP 信息
  65 +
  66 +在 APP 详情页面配置以下信息:
  67 +
  68 +#### 1.4.1 App URL(安装入口)
  69 +
  70 +当商家从应用市场点击"安装"时,店匠会跳转到此 URL:
  71 +
  72 +```
  73 +https://saas-ai-api.essa.top/recommend/oauth/install
  74 +```
  75 +
  76 +**请求参数**:
  77 +- `shop`:店铺域名,例如 `47167113-1.myshoplaza.com`
  78 +- `hmac`:HMAC 签名(用于验证请求来源)
  79 +- `install_from`:安装来源,例如 `app_store`
  80 +- `store_id`:店铺 ID
  81 +
  82 +**完整示例**:
  83 +```
  84 +https://saas-ai-api.essa.top/recommend/oauth/install?
  85 + hmac=c4caf9b08bdeff7531bb12712ffea860264ec24f5fd953832505c5024d19edca&
  86 + install_from=app_store&
  87 + shop=rwerwre.myshoplaza.com&
  88 + store_id=1339409
  89 +```
  90 +
  91 +#### 1.4.2 Redirect URL(OAuth 回调地址)
  92 +
  93 +商家授权后,店匠会重定向到此 URL:
  94 +
  95 +```
  96 +https://saas-ai-api.essa.top/recommend/oauth/callback
  97 +```
  98 +
  99 +**请求参数**:
  100 +- `code`:授权码(只能使用一次)
  101 +- `hmac`:HMAC 签名
  102 +- `shop`:店铺域名
  103 +- `state`:防 CSRF 攻击的随机值(可选)
  104 +
  105 +#### 1.4.3 Scopes(权限范围)
  106 +
  107 +根据应用功能需求选择权限:
  108 +
  109 +| Scope | 说明 | 是否必需 |
  110 +|-------|------|---------|
  111 +| `read_shop` | 读取店铺信息 | ✅ 推荐 |
  112 +| `read_product` | 读取商品信息 | ✅ 推荐 |
  113 +| `read_customer` | 读取客户信息 | 可选 |
  114 +| `read_order` | 读取订单信息 | 可选 |
  115 +| `read_app_proxy` | 读取应用代理 | 可选 |
  116 +| `write_cart_transform` | 修改购物车 | 可选 |
  117 +
  118 +**推荐配置**(智能推荐 APP):
  119 +```
  120 +read_shop read_product read_customer
  121 +```
  122 +
  123 +### 步骤 1.5:记录 Extension ID
  124 +
  125 +创建主题扩展后,系统会生成 Extension ID,例如:`580182366958388163`
  126 +
  127 +此 ID 需要在 `shoplazza.extension.toml` 中配置。
  128 +
  129 +---
  130 +
  131 +## 阶段 2:创建前端项目
  132 +
  133 +### 步骤 2.1:创建项目目录
  134 +
  135 +```bash
  136 +# 进入父目录
  137 +cd /home/tw/saas
  138 +
  139 +# 创建新项目目录
  140 +mkdir shoplazza-recommend-app
  141 +cd shoplazza-recommend-app
  142 +```
  143 +
  144 +### 步骤 2.2:初始化项目
  145 +
  146 +#### 方式 1:使用 Shoplazza CLI(推荐)
  147 +
  148 +```bash
  149 +# 安装 Shoplazza CLI(如果未安装)
  150 +npm i -g shoplazza-cli
  151 +
  152 +# 生成项目结构
  153 +shoplazza app generate
  154 +```
  155 +
  156 +#### 方式 2:手动创建
  157 +
  158 +按照以下结构手动创建文件:
  159 +
  160 +```
  161 +shoplazza-recommend-app/
  162 +├── package.json
  163 +├── shoplazza.app.toml
  164 +├── README.md
  165 +├── .gitignore
  166 +└── extensions/
  167 + └── recommend/
  168 + ├── shoplazza.extension.toml
  169 + ├── package.json
  170 + ├── blocks/
  171 + │ └── recommend.liquid
  172 + ├── assets/
  173 + │ ├── recommend.css
  174 + │ └── assets-manifest.json
  175 + ├── snippets/
  176 + │ └── recommend.liquid
  177 + └── locales/
  178 + ├── zh-CN.json
  179 + └── en-US.json
  180 +```
  181 +
  182 +### 步骤 2.3:创建根目录配置文件
  183 +
  184 +#### 2.3.1 package.json
  185 +
  186 +```json
  187 +{
  188 + "name": "智能推荐1.0",
  189 + "version": "1.0.0",
  190 + "scripts": {
  191 + "shoplazza": "shoplazza",
  192 + "info": "shoplazza app info",
  193 + "generate": "shoplazza app generate",
  194 + "deploy": "shoplazza app deploy"
  195 + },
  196 + "author": "",
  197 + "license": "ISC",
  198 + "private": true,
  199 + "workspaces": [
  200 + "extensions/*"
  201 + ]
  202 +}
  203 +```
  204 +
  205 +#### 2.3.2 shoplazza.app.toml
  206 +
  207 +使用步骤 1.3 中获取的 Client ID:
  208 +
  209 +```toml
  210 +# 使用步骤 1.3 中获取的 Client ID
  211 +client_id = "x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY"
  212 +
  213 +# 权限范围(与 Partner 后台配置保持一致)
  214 +scopes = "read_shop read_product read_customer"
  215 +```
  216 +
  217 +#### 2.3.3 .gitignore
  218 +
  219 +```gitignore
  220 +# Dependencies
  221 +node_modules/
  222 +package-lock.json
  223 +yarn.lock
  224 +
  225 +# Build outputs
  226 +app-deploy/
  227 +dist/
  228 +build/
  229 +
  230 +# Environment
  231 +.env
  232 +.env.local
  233 +
  234 +# IDE
  235 +.vscode/
  236 +.idea/
  237 +*.swp
  238 +*.swo
  239 +
  240 +# OS
  241 +.DS_Store
  242 +Thumbs.db
  243 +
  244 +# Logs
  245 +*.log
  246 +npm-debug.log*
  247 +```
  248 +
  249 +#### 2.3.4 README.md
  250 +
  251 +```markdown
  252 +# 智能推荐 1.0
  253 +
  254 +店匠(Shoplazza)智能推荐 APP 前端项目。
  255 +
  256 +## 快速开始
  257 +
  258 +### 安装依赖
  259 +
  260 +```bash
  261 +npm install
  262 +```
  263 +
  264 +### 部署应用
  265 +
  266 +```bash
  267 +npm run deploy
  268 +```
  269 +
  270 +### 查看应用信息
  271 +
  272 +```bash
  273 +npm run info
  274 +```
  275 +
  276 +## 项目结构
  277 +
  278 +- `extensions/recommend/`:推荐扩展
  279 + - `blocks/recommend.liquid`:主组件
  280 + - `assets/recommend.css`:样式文件
  281 + - `locales/`:多语言文件
  282 +```
  283 +
  284 +### 步骤 2.4:创建 Extension 配置文件
  285 +
  286 +#### 2.4.1 extensions/recommend/shoplazza.extension.toml
  287 +
  288 +```toml
  289 +# Extension ID(从 Partner 后台获取,或部署后系统生成)
  290 +id = "580182366958388163"
  291 +
  292 +# Extension 名称(与目录名保持一致)
  293 +name = "recommend"
  294 +
  295 +# Extension 类型(theme 表示主题扩展)
  296 +type = "theme"
  297 +```
  298 +
  299 +#### 2.4.2 extensions/recommend/package.json
  300 +
  301 +```json
  302 +{
  303 + "name": "recommend",
  304 + "version": "1.0.0",
  305 + "private": true
  306 +}
  307 +```
  308 +
  309 +---
  310 +
  311 +## 阶段 3:开发后端服务
  312 +
  313 +### 步骤 3.1:OAuth 2.0 授权流程
  314 +
  315 +店匠使用标准的 OAuth 2.0 授权码(Authorization Code)流程:
  316 +
  317 +```
  318 +1. 商家在应用市场点击"安装"
  319 + ↓
  320 +2. 店匠跳转到 APP URL(步骤 1.4.1)
  321 + ↓
  322 +3. 后端验证 HMAC 和 shop 参数
  323 + ↓
  324 +4. 后端重定向到店匠授权页面
  325 + ↓
  326 +5. 商家在授权页面点击"授权"
  327 + ↓
  328 +6. 店匠回调 Redirect URL(步骤 1.4.2),携带 code
  329 + ↓
  330 +7. 后端验证 HMAC,用 code 换取 access_token
  331 + ↓
  332 +8. 保存 token 到数据库,完成安装
  333 +```
  334 +
  335 +### 步骤 3.2:实现 APP URL 处理(步骤 3)
  336 +
  337 +#### 3.2.1 请求示例
  338 +
  339 +```
  340 +GET https://saas-ai-api.essa.top/recommend/oauth/install?
  341 + hmac=c4caf9b08bdeff7531bb12712ffea860264ec24f5fd953832505c5024d19edca&
  342 + install_from=app_store&
  343 + shop=rwerwre.myshoplaza.com&
  344 + store_id=1339409
  345 +```
  346 +
  347 +#### 3.2.2 安全检查(必须)
  348 +
  349 +在继续之前,必须执行以下安全检查:
  350 +
  351 +1. **验证 HMAC 签名**
  352 + - 从查询字符串中移除 `hmac` 参数
  353 + - 按字典顺序排序剩余参数
  354 + - 使用 `CLIENT_SECRET` 计算 HMAC-SHA256
  355 + - 与请求中的 `hmac` 值比较
  356 +
  357 +2. **验证 shop 参数**
  358 + - 必须是有效的店铺主机名
  359 + - 必须以 `.myshoplaza.com` 结尾
  360 +
  361 +#### 3.2.3 Java 实现示例
  362 +
  363 +```java
  364 +@RestController
  365 +@RequestMapping("/recommend/oauth")
  366 +public class RecommendOAuthController {
  367 +
  368 + @Autowired
  369 + private ShoplazzaOAuthService oAuthService;
  370 +
  371 + /**
  372 + * 处理 APP 安装请求(步骤 3)
  373 + */
  374 + @GetMapping("/install")
  375 + public ResponseEntity<Void> handleInstall(
  376 + @RequestParam("shop") String shop,
  377 + @RequestParam("hmac") String hmac,
  378 + @RequestParam(value = "install_from", required = false) String installFrom,
  379 + @RequestParam(value = "store_id", required = false) String storeId,
  380 + HttpServletRequest request) {
  381 +
  382 + // 1. 验证 HMAC
  383 + Map<String, String> params = new HashMap<>();
  384 + params.put("shop", shop);
  385 + params.put("hmac", hmac);
  386 + if (installFrom != null) params.put("install_from", installFrom);
  387 + if (storeId != null) params.put("store_id", storeId);
  388 +
  389 + if (!oAuthService.verifyHmac(params, hmac)) {
  390 + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
  391 + }
  392 +
  393 + // 2. 验证 shop 参数
  394 + if (!shop.endsWith(".myshoplaza.com")) {
  395 + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
  396 + }
  397 +
  398 + // 3. 生成授权 URL 并重定向(步骤 4)
  399 + String authUrl = oAuthService.buildAuthorizationRedirect(shop);
  400 + return ResponseEntity.status(HttpStatus.FOUND)
  401 + .header(HttpHeaders.LOCATION, authUrl)
  402 + .build();
  403 + }
  404 +}
  405 +```
  406 +
  407 +#### 3.2.4 Node.js 实现示例
  408 +
  409 +```javascript
  410 +const express = require("express");
  411 +const crypto = require("crypto");
  412 +const app = express();
  413 +
  414 +const CLIENT_ID = "x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY";
  415 +const CLIENT_SECRET = "LF71s5gqtPws2i08C9y43uIYXpF_c91ZnqRmS1z8Ekk";
  416 +const REDIRECT_URI = "https://saas-ai-api.essa.top/recommend/oauth/callback";
  417 +
  418 +// HMAC 验证函数
  419 +function verifyHmac(queryParams, receivedHmac) {
  420 + const map = { ...queryParams };
  421 + delete map["hmac"];
  422 +
  423 + const sortedKeys = Object.keys(map).sort();
  424 + const message = sortedKeys.map(key => `${key}=${map[key]}`).join('&');
  425 +
  426 + const calculatedHmac = crypto
  427 + .createHmac("sha256", CLIENT_SECRET)
  428 + .update(message)
  429 + .digest("hex");
  430 +
  431 + return crypto.timingSafeEqual(
  432 + Buffer.from(calculatedHmac),
  433 + Buffer.from(receivedHmac)
  434 + );
  435 +}
  436 +
  437 +// 处理安装请求
  438 +app.get("/recommend/oauth/install", (req, res) => {
  439 + const { shop, hmac, install_from, store_id } = req.query;
  440 +
  441 + // 1. 验证 HMAC
  442 + if (!verifyHmac(req.query, hmac)) {
  443 + return res.status(400).send("HMAC validation failed");
  444 + }
  445 +
  446 + // 2. 验证 shop 参数
  447 + if (!shop || !shop.endsWith(".myshoplaza.com")) {
  448 + return res.status(400).send("Invalid shop parameter");
  449 + }
  450 +
  451 + // 3. 生成 state(防 CSRF)
  452 + const state = crypto.randomBytes(16).toString("hex");
  453 +
  454 + // 4. 构建授权 URL
  455 + const scopes = "read_shop read_product read_customer";
  456 + const authUrl = `https://${shop}/admin/oauth/authorize?` +
  457 + `client_id=${CLIENT_ID}&` +
  458 + `scope=${encodeURIComponent(scopes)}&` +
  459 + `redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
  460 + `response_type=code&` +
  461 + `state=${state}`;
  462 +
  463 + // 5. 重定向到授权页面
  464 + res.redirect(authUrl);
  465 +});
  466 +```
  467 +
  468 +### 步骤 3.3:实现 OAuth 回调处理(步骤 7-9)
  469 +
  470 +#### 3.3.1 请求示例
  471 +
  472 +```
  473 +GET https://saas-ai-api.essa.top/recommend/oauth/callback?
  474 + code=wBe-NWHzW21e94YqD4bRKBsJsE2GcZlDzP4oW9w2ddk&
  475 + hmac=4c396fac1912057b65228f5bbd4a65255961d85a60fba1f1105ddbf27f17b58f&
  476 + shop=rwerwre.myshoplaza.com&
  477 + state=abc123
  478 +```
  479 +
  480 +#### 3.3.2 Java 实现示例
  481 +
  482 +```java
  483 +/**
  484 + * 处理 OAuth 回调(步骤 7-9)
  485 + */
  486 +@GetMapping("/callback")
  487 +public ResponseEntity<String> handleCallback(
  488 + @RequestParam("code") String code,
  489 + @RequestParam("hmac") String hmac,
  490 + @RequestParam("shop") String shop,
  491 + @RequestParam(value = "state", required = false) String state) {
  492 +
  493 + // 1. 验证 HMAC
  494 + Map<String, String> params = new HashMap<>();
  495 + params.put("code", code);
  496 + params.put("hmac", hmac);
  497 + params.put("shop", shop);
  498 + if (state != null) params.put("state", state);
  499 +
  500 + if (!oAuthService.verifyHmac(params, hmac)) {
  501 + return ResponseEntity.status(HttpStatus.BAD_REQUEST)
  502 + .body("HMAC validation failed");
  503 + }
  504 +
  505 + // 2. 用 code 换取 access_token
  506 + ShoplazzaTokenResponse tokenResponse = oAuthService.exchangeForToken(shop, code);
  507 +
  508 + // 3. 保存 token 到数据库
  509 + ShopConfig shopConfig = shopConfigService.saveOrUpdate(shop, tokenResponse);
  510 +
  511 + // 4. 返回安装成功页面
  512 + String successPage = buildSuccessPage(shopConfig);
  513 + return ResponseEntity.ok()
  514 + .contentType(MediaType.TEXT_HTML)
  515 + .body(successPage);
  516 +}
  517 +
  518 +/**
  519 + * 用 code 换取 access_token
  520 + */
  521 +public ShoplazzaTokenResponse exchangeForToken(String shop, String code) {
  522 + String tokenUrl = "https://" + shop + "/admin/oauth/token";
  523 +
  524 + Map<String, String> requestBody = new HashMap<>();
  525 + requestBody.put("client_id", CLIENT_ID);
  526 + requestBody.put("client_secret", CLIENT_SECRET);
  527 + requestBody.put("code", code);
  528 + requestBody.put("grant_type", "authorization_code");
  529 + requestBody.put("redirect_uri", REDIRECT_URI);
  530 +
  531 + // 发送 POST 请求
  532 + RestTemplate restTemplate = new RestTemplate();
  533 + HttpHeaders headers = new HttpHeaders();
  534 + headers.setContentType(MediaType.APPLICATION_JSON);
  535 +
  536 + HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody, headers);
  537 + ResponseEntity<ShoplazzaTokenResponse> response = restTemplate.postForEntity(
  538 + tokenUrl, entity, ShoplazzaTokenResponse.class);
  539 +
  540 + return response.getBody();
  541 +}
  542 +```
  543 +
  544 +#### 3.3.3 Token 响应格式
  545 +
  546 +```json
  547 +{
  548 + "token_type": "Bearer",
  549 + "expires_at": 1550546245,
  550 + "access_token": "eyJ0eXAiOiJKV1QiLCJh...",
  551 + "refresh_token": "def502003d28ba08a964e...",
  552 + "store_id": "1339409",
  553 + "store_name": "rwerwre"
  554 +}
  555 +```
  556 +
  557 +### 步骤 3.4:实现 API 调用
  558 +
  559 +获取 access_token 后,可以调用店匠 API:
  560 +
  561 +```java
  562 +/**
  563 + * 调用店匠 API 示例:获取商品列表
  564 + */
  565 +public List<Product> getProducts(String shop, String accessToken) {
  566 + String apiUrl = "https://" + shop + "/openapi/2022-01/products";
  567 +
  568 + HttpHeaders headers = new HttpHeaders();
  569 + headers.set("Access-Token", accessToken);
  570 + headers.set("Content-Type", "application/json");
  571 +
  572 + HttpEntity<?> entity = new HttpEntity<>(headers);
  573 +
  574 + RestTemplate restTemplate = new RestTemplate();
  575 + ResponseEntity<ProductListResponse> response = restTemplate.exchange(
  576 + apiUrl,
  577 + HttpMethod.GET,
  578 + entity,
  579 + ProductListResponse.class
  580 + );
  581 +
  582 + return response.getBody().getProducts();
  583 +}
  584 +```
  585 +
  586 +---
  587 +
  588 +## 阶段 4:前端开发详解
  589 +
  590 +### 步骤 4.1:创建 Liquid 模板
  591 +
  592 +#### 4.1.1 extensions/recommend/blocks/recommend.liquid
  593 +
  594 +```liquid
  595 +{% comment %}
  596 + 推荐商品组件
  597 + 使用 {% use %} 引入 CSS 文件
  598 +{% endcomment %}
  599 +{% use "recommend.css" %}
  600 +
  601 +<div id="recommend" class="recommend-container">
  602 + {% comment %} 版本标识(开发测试用){% endcomment %}
  603 + <div class="recommend-badge">智能推荐 APP</div>
  604 +
  605 + {% comment %} 标题(使用多语言){% endcomment %}
  606 + <h2>{{ 'recommend.title' | t }}</h2>
  607 +
  608 + {% comment %} 商品列表容器 {% endcomment %}
  609 + <div class="recommend-products" id="recommend-products">
  610 + <div class="loading">{{ 'recommend.loading' | t }}</div>
  611 + </div>
  612 +</div>
  613 +
  614 +<script>
  615 +(function() {
  616 + {% comment %} 配置信息(店匠会自动注入 shop 对象){% endcomment %}
  617 + window.RECOMMEND_CONFIG = {
  618 + storeId: "{{ shop.domain }}",
  619 + apiEndpoint: "https://saas-ai-api.essa.top/app-api/app/shoplazza/recommend/products",
  620 + locale: "{{ shop.locale }}"
  621 + };
  622 +
  623 + {% comment %} 获取店铺语言 {% endcomment %}
  624 + const shopLocale = "{{ shop.locale }}" || "zh-CN";
  625 +
  626 + {% comment %} 翻译文本(通过 Liquid 的 t 过滤器获取){% endcomment %}
  627 + const translations = {
  628 + title: "{{ 'recommend.title' | t }}",
  629 + loading: "{{ 'recommend.loading' | t }}",
  630 + noResults: "{{ 'recommend.no_results' | t }}",
  631 + error: "{{ 'recommend.error' | t }}",
  632 + addToCart: "{{ 'recommend.add_to_cart' | t }}",
  633 + viewDetails: "{{ 'recommend.view_details' | t }}"
  634 + };
  635 +
  636 + {% comment %} 加载推荐商品 {% endcomment %}
  637 + async function loadRecommendations() {
  638 + const container = document.getElementById('recommend-products');
  639 +
  640 + try {
  641 + const response = await fetch(
  642 + `${window.RECOMMEND_CONFIG.apiEndpoint}?storeId=${window.RECOMMEND_CONFIG.storeId}`,
  643 + {
  644 + method: 'GET',
  645 + headers: {
  646 + 'Content-Type': 'application/json'
  647 + }
  648 + }
  649 + );
  650 +
  651 + if (!response.ok) {
  652 + throw new Error('加载失败: ' + response.status);
  653 + }
  654 +
  655 + const data = await response.json();
  656 +
  657 + if (data.products && data.products.length > 0) {
  658 + renderProducts(data.products);
  659 + } else {
  660 + container.innerHTML = `<div class="no-results">${translations.noResults}</div>`;
  661 + }
  662 + } catch (error) {
  663 + console.error('加载推荐商品失败:', error);
  664 + container.innerHTML = `<div class="error">${translations.error}</div>`;
  665 + }
  666 + }
  667 +
  668 + {% comment %} 渲染商品列表 {% endcomment %}
  669 + function renderProducts(products) {
  670 + const container = document.getElementById('recommend-products');
  671 +
  672 + const html = products.map(product => {
  673 + const imageUrl = product.image_url || product.image || '/assets/no-image.png';
  674 + const productUrl = product.url || product.handle || '#';
  675 + const price = formatPrice(product.price || product.price_formatted || '0.00');
  676 +
  677 + return `
  678 + <div class="product-item" data-product-id="${product.id}">
  679 + <a href="${productUrl}" class="product-link">
  680 + <div class="product-image-wrapper">
  681 + <img
  682 + src="${imageUrl}"
  683 + alt="${escapeHtml(product.title || product.name || '未知商品')}"
  684 + class="product-image"
  685 + onerror="this.src='/assets/no-image.png'"
  686 + />
  687 + </div>
  688 + <div class="product-info">
  689 + <h3 class="product-title">${escapeHtml(product.title || product.name || '未知商品')}</h3>
  690 + <div class="product-price">${price}</div>
  691 + ${product.vendor ? `<div class="product-vendor">${escapeHtml(product.vendor)}</div>` : ''}
  692 + </div>
  693 + </a>
  694 + <button class="add-to-cart-btn" onclick="addToCart('${product.id}')">
  695 + ${translations.addToCart}
  696 + </button>
  697 + </div>
  698 + `;
  699 + }).join('');
  700 +
  701 + container.innerHTML = html;
  702 + }
  703 +
  704 + {% comment %} 工具函数:转义 HTML {% endcomment %}
  705 + function escapeHtml(text) {
  706 + const div = document.createElement('div');
  707 + div.textContent = text;
  708 + return div.innerHTML;
  709 + }
  710 +
  711 + {% comment %} 工具函数:格式化价格 {% endcomment %}
  712 + function formatPrice(price) {
  713 + if (typeof price === 'number') {
  714 + return '¥' + price.toFixed(2);
  715 + }
  716 + if (typeof price === 'string') {
  717 + const num = parseFloat(price);
  718 + if (!isNaN(num)) {
  719 + return '¥' + num.toFixed(2);
  720 + }
  721 + }
  722 + return price || '¥0.00';
  723 + }
  724 +
  725 + {% comment %} 添加到购物车(示例){% endcomment %}
  726 + window.addToCart = function(productId) {
  727 + console.log('添加到购物车:', productId);
  728 + // 实现添加到购物车逻辑
  729 + };
  730 +
  731 + {% comment %} 初始化 {% endcomment %}
  732 + if (document.readyState === 'loading') {
  733 + document.addEventListener('DOMContentLoaded', loadRecommendations);
  734 + } else {
  735 + loadRecommendations();
  736 + }
  737 +})();
  738 +</script>
  739 +
  740 +{% comment %} Schema 配置(用于主题编辑器){% endcomment %}
  741 +{% schema %}
  742 +{
  743 + "name": {
  744 + "en-US": "recommend(dev)",
  745 + "zh-CN": "智能推荐(开发)"
  746 + },
  747 + "settings": [
  748 + {
  749 + "type": "text",
  750 + "id": "title",
  751 + "label": {
  752 + "en-US": "Title",
  753 + "zh-CN": "标题"
  754 + },
  755 + "default": {
  756 + "en-US": "Recommended Products",
  757 + "zh-CN": "推荐商品"
  758 + }
  759 + },
  760 + {
  761 + "type": "range",
  762 + "id": "product_count",
  763 + "label": {
  764 + "en-US": "Number of products",
  765 + "zh-CN": "商品数量"
  766 + },
  767 + "min": 4,
  768 + "max": 20,
  769 + "step": 1,
  770 + "default": 8
  771 + }
  772 + ]
  773 +}
  774 +{% endschema %}
  775 +```
  776 +
  777 +### 步骤 4.2:创建样式文件
  778 +
  779 +#### 4.2.1 extensions/recommend/assets/recommend.css
  780 +
  781 +```css
  782 +/* 推荐商品容器 */
  783 +.recommend-container {
  784 + width: 100%;
  785 + max-width: 1200px;
  786 + margin: 0 auto;
  787 + padding: 20px;
  788 + font-family: var(--font-body-family, sans-serif);
  789 + color: var(--color-text, #333);
  790 + background: var(--color-background, #fff);
  791 +}
  792 +
  793 +/* 版本标识(开发测试用)*/
  794 +.recommend-badge {
  795 + position: fixed;
  796 + top: 10px;
  797 + right: 10px;
  798 + background: #ff6b6b;
  799 + color: white;
  800 + padding: 5px 10px;
  801 + border-radius: 4px;
  802 + font-size: 12px;
  803 + font-weight: bold;
  804 + z-index: 9999;
  805 + box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  806 +}
  807 +
  808 +/* 标题 */
  809 +.recommend-container h2 {
  810 + font-size: 24px;
  811 + font-weight: 600;
  812 + margin-bottom: 20px;
  813 + text-align: center;
  814 +}
  815 +
  816 +/* 商品网格 */
  817 +.recommend-products {
  818 + display: grid;
  819 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  820 + gap: 20px;
  821 + margin-top: 20px;
  822 +}
  823 +
  824 +/* 商品项 */
  825 +.product-item {
  826 + border: 1px solid var(--color-border, #e0e0e0);
  827 + border-radius: 8px;
  828 + overflow: hidden;
  829 + transition: transform 0.2s, box-shadow 0.2s;
  830 + background: var(--color-background, #fff);
  831 +}
  832 +
  833 +.product-item:hover {
  834 + transform: translateY(-4px);
  835 + box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  836 +}
  837 +
  838 +/* 商品链接 */
  839 +.product-link {
  840 + text-decoration: none;
  841 + color: inherit;
  842 + display: block;
  843 +}
  844 +
  845 +/* 商品图片 */
  846 +.product-image-wrapper {
  847 + width: 100%;
  848 + padding-top: 100%; /* 1:1 比例 */
  849 + position: relative;
  850 + overflow: hidden;
  851 + background: #f5f5f5;
  852 +}
  853 +
  854 +.product-image {
  855 + position: absolute;
  856 + top: 0;
  857 + left: 0;
  858 + width: 100%;
  859 + height: 100%;
  860 + object-fit: cover;
  861 +}
  862 +
  863 +/* 商品信息 */
  864 +.product-info {
  865 + padding: 12px;
  866 +}
  867 +
  868 +.product-title {
  869 + font-size: 14px;
  870 + font-weight: 500;
  871 + margin: 0 0 8px 0;
  872 + line-height: 1.4;
  873 + display: -webkit-box;
  874 + -webkit-line-clamp: 2;
  875 + -webkit-box-orient: vertical;
  876 + overflow: hidden;
  877 +}
  878 +
  879 +.product-price {
  880 + font-size: 16px;
  881 + font-weight: 600;
  882 + color: var(--color-price, #e74c3c);
  883 + margin-bottom: 4px;
  884 +}
  885 +
  886 +.product-vendor {
  887 + font-size: 12px;
  888 + color: var(--color-text-secondary, #999);
  889 +}
  890 +
  891 +/* 添加到购物车按钮 */
  892 +.add-to-cart-btn {
  893 + width: 100%;
  894 + padding: 10px;
  895 + background: var(--color-button, #007bff);
  896 + color: white;
  897 + border: none;
  898 + border-radius: 4px;
  899 + font-size: 14px;
  900 + font-weight: 500;
  901 + cursor: pointer;
  902 + transition: background 0.2s;
  903 +}
  904 +
  905 +.add-to-cart-btn:hover {
  906 + background: var(--color-button-hover, #0056b3);
  907 +}
  908 +
  909 +/* 加载状态 */
  910 +.loading {
  911 + text-align: center;
  912 + padding: 40px;
  913 + color: var(--color-text-secondary, #999);
  914 +}
  915 +
  916 +/* 无结果 */
  917 +.no-results {
  918 + text-align: center;
  919 + padding: 40px;
  920 + color: var(--color-text-secondary, #999);
  921 +}
  922 +
  923 +/* 错误状态 */
  924 +.error {
  925 + text-align: center;
  926 + padding: 40px;
  927 + color: var(--color-error, #e74c3c);
  928 +}
  929 +
  930 +/* 响应式设计 */
  931 +@media (max-width: 749px) {
  932 + .recommend-container {
  933 + padding: 10px;
  934 + }
  935 +
  936 + .recommend-products {
  937 + grid-template-columns: repeat(2, 1fr);
  938 + gap: 10px;
  939 + }
  940 +
  941 + .recommend-container h2 {
  942 + font-size: 20px;
  943 + }
  944 +}
  945 +
  946 +@media (min-width: 750px) and (max-width: 989px) {
  947 + .recommend-products {
  948 + grid-template-columns: repeat(3, 1fr);
  949 + }
  950 +}
  951 +
  952 +@media (min-width: 990px) {
  953 + .recommend-products {
  954 + grid-template-columns: repeat(4, 1fr);
  955 + }
  956 +}
  957 +```
  958 +
  959 +### 步骤 4.3:创建多语言文件
  960 +
  961 +#### 4.3.1 extensions/recommend/locales/zh-CN.json
  962 +
  963 +```json
  964 +{
  965 + "recommend": {
  966 + "title": "为您推荐",
  967 + "loading": "加载中,请稍候...",
  968 + "no_results": "暂无推荐商品",
  969 + "error": "加载失败,请重试",
  970 + "add_to_cart": "加入购物车",
  971 + "view_details": "查看详情"
  972 + }
  973 +}
  974 +```
  975 +
  976 +#### 4.3.2 extensions/recommend/locales/en-US.json
  977 +
  978 +```json
  979 +{
  980 + "recommend": {
  981 + "title": "Recommended for You",
  982 + "loading": "Loading, please wait...",
  983 + "no_results": "No recommended products",
  984 + "error": "Failed to load, please try again",
  985 + "add_to_cart": "Add to Cart",
  986 + "view_details": "View Details"
  987 + }
  988 +}
  989 +```
  990 +
  991 +#### 4.3.3 支持的语言列表
  992 +
  993 +店匠支持 16 种语言,建议至少提供以下语言文件:
  994 +
  995 +| 语言代码 | 语言名称 | 文件路径 |
  996 +|---------|---------|---------|
  997 +| `zh-CN` | 中文(简体) | `locales/zh-CN.json` |
  998 +| `zh-TW` | 中文(繁体) | `locales/zh-TW.json` |
  999 +| `en-US` | 英语 | `locales/en-US.json` |
  1000 +| `ja-JP` | 日语 | `locales/ja-JP.json` |
  1001 +| `ko-KR` | 韩语 | `locales/ko-KR.json` |
  1002 +
  1003 +**完整语言列表**:参考 [多语言支持文档](#多语言支持)
  1004 +
  1005 +### 步骤 4.4:Lessjs 集成(可选)
  1006 +
  1007 +Lessjs 是店匠官方提供的前端组件库,基于 Web Components。如需使用,参考:
  1008 +
  1009 +- [Lessjs 官方文档](https://lessjs.shoplazza.com/latest/docs/introduction/)
  1010 +- [Lessjs 自定义组件文档](https://lessjs.shoplazza.com/latest/docs/custom-component/)
  1011 +
  1012 +**注意**:对于简单的推荐组件,可以不使用 Lessjs,直接使用原生 JavaScript 和 CSS。
  1013 +
  1014 +---
  1015 +
  1016 +## 阶段 5:测试与部署
  1017 +
  1018 +### 步骤 5.1:本地测试
  1019 +
  1020 +#### 5.1.1 检查项目结构
  1021 +
  1022 +```bash
  1023 +cd /home/tw/saas/shoplazza-recommend-app
  1024 +
  1025 +# 检查文件结构
  1026 +tree -L 3
  1027 +
  1028 +# 应该看到:
  1029 +# shoplazza-recommend-app/
  1030 +# ├── package.json
  1031 +# ├── shoplazza.app.toml
  1032 +# └── extensions/
  1033 +# └── recommend/
  1034 +# ├── shoplazza.extension.toml
  1035 +# ├── blocks/
  1036 +# ├── assets/
  1037 +# └── locales/
  1038 +```
  1039 +
  1040 +#### 5.1.2 验证配置
  1041 +
  1042 +```bash
  1043 +# 查看应用信息
  1044 +npm run info
  1045 +
  1046 +# 应该显示:
  1047 +# - App Name: 智能推荐1.0
  1048 +# - Client ID: x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY
  1049 +# - Extension ID: 580182366958388163
  1050 +```
  1051 +
  1052 +### 步骤 5.2:部署到店匠
  1053 +
  1054 +#### 5.2.1 登录 Shoplazza CLI
  1055 +
  1056 +```bash
  1057 +# 登录(如果未登录)
  1058 +shoplazza login
  1059 +
  1060 +# 输入 Partner 账号和密码
  1061 +```
  1062 +
  1063 +#### 5.2.2 部署应用
  1064 +
  1065 +```bash
  1066 +# 部署应用
  1067 +npm run deploy
  1068 +
  1069 +# 或者
  1070 +shoplazza app deploy
  1071 +```
  1072 +
  1073 +**部署过程**:
  1074 +1. CLI 会验证项目配置
  1075 +2. 上传扩展文件到店匠服务器
  1076 +3. 返回部署结果
  1077 +
  1078 +### 步骤 5.3:在测试店铺中安装
  1079 +
  1080 +#### 5.3.1 访问应用市场
  1081 +
  1082 +1. 登录测试店铺后台
  1083 +2. 进入 **应用市场** 或 **Apps**
  1084 +3. 搜索您的应用名称(如"智能推荐1.0")
  1085 +4. 点击 **安装** 或 **Install**
  1086 +
  1087 +#### 5.3.2 OAuth 授权流程
  1088 +
  1089 +1. 点击安装后,会跳转到您的后端服务(APP URL)
  1090 +2. 后端验证 HMAC 和 shop 参数
  1091 +3. 重定向到店匠授权页面
  1092 +4. 商家点击"授权"
  1093 +5. 店匠回调您的后端(Redirect URL)
  1094 +6. 后端用 code 换取 access_token
  1095 +7. 显示安装成功页面
  1096 +
  1097 +#### 5.3.3 在主题中添加扩展
  1098 +
  1099 +1. 进入店铺后台:**在线商店** > **主题** > **自定义**
  1100 +2. 在页面中添加 **App 扩展**
  1101 +3. 选择 **智能推荐** 扩展
  1102 +4. 配置扩展设置(如商品数量)
  1103 +5. 保存并发布
  1104 +
  1105 +### 步骤 5.4:验证功能
  1106 +
  1107 +#### 5.4.1 前端验证
  1108 +
  1109 +- [ ] 扩展正确显示在主题编辑器中
  1110 +- [ ] 商品列表正确加载
  1111 +- [ ] 多语言切换正常
  1112 +- [ ] 响应式布局在不同设备上正常
  1113 +- [ ] 图片加载正常(包括错误处理)
  1114 +
  1115 +#### 5.4.2 后端验证
  1116 +
  1117 +- [ ] OAuth 授权流程正常
  1118 +- [ ] HMAC 验证正常
  1119 +- [ ] Token 正确保存到数据库
  1120 +- [ ] API 调用正常(如获取商品列表)
  1121 +
  1122 +#### 5.4.3 浏览器控制台检查
  1123 +
  1124 +打开浏览器开发者工具(F12),检查:
  1125 +
  1126 +- **Console**:无 JavaScript 错误
  1127 +- **Network**:API 请求正常(状态码 200)
  1128 +- **Elements**:HTML 结构正确
  1129 +
  1130 +---
  1131 +
  1132 +## 常见问题与最佳实践
  1133 +
  1134 +### 常见问题
  1135 +
  1136 +#### Q1: HMAC 验证失败
  1137 +
  1138 +**原因**:
  1139 +- Client Secret 配置错误
  1140 +- 参数排序不正确
  1141 +- HMAC 计算方式错误
  1142 +
  1143 +**解决方案**:
  1144 +1. 确认 `CLIENT_SECRET` 与 Partner 后台一致
  1145 +2. 确保参数按字典顺序排序
  1146 +3. 使用 `HMAC-SHA256` 算法
  1147 +
  1148 +#### Q2: 多语言不生效
  1149 +
  1150 +**原因**:
  1151 +- 语言文件路径错误
  1152 +- 翻译键名不匹配
  1153 +- Liquid 模板语法错误
  1154 +
  1155 +**解决方案**:
  1156 +1. 确认 `locales/{locale}.json` 文件存在
  1157 +2. 检查翻译键名是否与模板中的 `{{ 'key' | t }}` 一致
  1158 +3. 使用 `{{ shop.locale }}` 获取当前语言
  1159 +
  1160 +#### Q3: 扩展在主题编辑器中不显示
  1161 +
  1162 +**原因**:
  1163 +- Extension ID 配置错误
  1164 +- 部署未成功
  1165 +- Schema 配置错误
  1166 +
  1167 +**解决方案**:
  1168 +1. 确认 `shoplazza.extension.toml` 中的 `id` 正确
  1169 +2. 重新部署应用:`npm run deploy`
  1170 +3. 检查 `{% schema %}` 配置是否正确
  1171 +
  1172 +#### Q4: API 调用返回 401 未授权
  1173 +
  1174 +**原因**:
  1175 +- Access Token 过期
  1176 +- Token 未正确传递
  1177 +- API 版本不匹配
  1178 +
  1179 +**解决方案**:
  1180 +1. 使用 `refresh_token` 刷新 Access Token
  1181 +2. 确认请求头包含 `Access-Token: {access_token}`
  1182 +3. 检查 API 版本(如 `/openapi/2022-01/`)
  1183 +
  1184 +### 最佳实践
  1185 +
  1186 +#### 1. 安全性
  1187 +
  1188 +- ✅ **始终验证 HMAC**:所有来自店匠的请求都必须验证 HMAC
  1189 +- ✅ **验证 shop 参数**:确保 shop 以 `.myshoplaza.com` 结尾
  1190 +- ✅ **使用 HTTPS**:所有 API 调用必须使用 HTTPS
  1191 +- ✅ **保护 Client Secret**:不要将 Client Secret 提交到代码仓库
  1192 +
  1193 +#### 2. 性能优化
  1194 +
  1195 +- ✅ **延迟加载**:非关键资源使用 `defer` 或 `async`
  1196 +- ✅ **图片优化**:使用响应式图片和 CDN
  1197 +- ✅ **缓存策略**:合理使用浏览器缓存和 CDN 缓存
  1198 +- ✅ **代码分割**:将大型 JavaScript 拆分为多个模块
  1199 +
  1200 +#### 3. 用户体验
  1201 +
  1202 +- ✅ **加载状态**:显示加载中、错误、无结果等状态
  1203 +- ✅ **错误处理**:友好的错误提示和重试机制
  1204 +- ✅ **响应式设计**:适配移动端、平板、桌面端
  1205 +- ✅ **多语言支持**:至少支持中文和英语
  1206 +
  1207 +#### 4. 代码质量
  1208 +
  1209 +- ✅ **代码注释**:关键逻辑添加注释
  1210 +- ✅ **错误日志**:记录错误日志便于调试
  1211 +- ✅ **代码规范**:遵循项目代码规范
  1212 +- ✅ **版本控制**:使用 Git 进行版本管理
  1213 +
  1214 +---
  1215 +
  1216 +## 参考资源
  1217 +
  1218 +### 官方文档
  1219 +
  1220 +- [Shoplazza 开发者文档](https://www.shoplazza.dev/)
  1221 +- [Shoplazza CLI 文档](https://www.shoplazza.dev/docs/overview-3)
  1222 +- [Lessjs 官方文档](https://lessjs.shoplazza.com/latest/docs/introduction/)
  1223 +- [Liquid 模板语言](https://shopify.github.io/liquid/)
  1224 +
  1225 +### 社区支持
  1226 +
  1227 +- [Shoplazza 开发者社区](https://community.shoplazza.com/)
  1228 +- [GitHub Issues](https://github.com/your-org/shoplazza-recommend-app/issues)
  1229 +
  1230 +---
  1231 +
  1232 +## 附录
  1233 +
  1234 +### A. 完整项目结构
  1235 +
  1236 +```
  1237 +shoplazza-recommend-app/
  1238 +├── package.json # 项目配置
  1239 +├── shoplazza.app.toml # APP 配置(Client ID)
  1240 +├── README.md # 项目说明
  1241 +├── .gitignore # Git 忽略文件
  1242 +├── SHOPLAZZA_APP_DEVELOPMENT_GUIDE.md # 本指南
  1243 +└── extensions/
  1244 + └── recommend/ # 推荐扩展
  1245 + ├── shoplazza.extension.toml # Extension 配置
  1246 + ├── package.json # Extension 配置
  1247 + ├── blocks/ # 组件块
  1248 + │ └── recommend.liquid # 主组件
  1249 + ├── assets/ # 静态资源
  1250 + │ ├── recommend.css # 样式文件
  1251 + │ └── assets-manifest.json # 资源清单
  1252 + ├── snippets/ # 代码片段
  1253 + │ └── recommend.liquid # 可复用片段
  1254 + └── locales/ # 多语言文件
  1255 + ├── zh-CN.json # 简体中文
  1256 + ├── zh-TW.json # 繁体中文
  1257 + ├── en-US.json # 英语
  1258 + ├── ja-JP.json # 日语
  1259 + └── ko-KR.json # 韩语
  1260 +```
  1261 +
  1262 +### B. 配置文件模板
  1263 +
  1264 +#### B.1 shoplazza.app.toml
  1265 +
  1266 +```toml
  1267 +client_id = "YOUR_CLIENT_ID"
  1268 +scopes = "read_shop read_product read_customer"
  1269 +```
  1270 +
  1271 +#### B.2 shoplazza.extension.toml
  1272 +
  1273 +```toml
  1274 +id = "YOUR_EXTENSION_ID"
  1275 +name = "recommend"
  1276 +type = "theme"
  1277 +```
  1278 +
  1279 +### C. OAuth 流程时序图
  1280 +
  1281 +```
  1282 +商家 → 店匠平台 → 后端服务 → 店匠授权页 → 商家授权 → 后端服务 → 数据库
  1283 + ↓ ↓ ↓ ↓ ↓ ↓ ↓
  1284 +安装 跳转APP URL 验证HMAC 重定向授权 点击授权 换取Token 保存配置
  1285 +```
  1286 +
  1287 +---
... ...
docs/店匠相关资料/SHOPLAZZA_I18N_AND_FRONTEND_GUIDE.md 0 → 100644
... ... @@ -0,0 +1,648 @@
  1 +# Shoplazza 应用开发指南:多语言支持与前端优化
  2 +
  3 +## 目录
  4 +
  5 +1. [多语言支持](#多语言支持)
  6 +2. [前端优化与 Lessjs 集成](#前端优化与-lessjs-集成)
  7 +3. [与店匠主题的适配](#与店匠主题的适配)
  8 +
  9 +---
  10 +
  11 +## 多语言支持
  12 +
  13 +### 1. 概述
  14 +
  15 +Shoplazza 应用支持 16 种语言,通过 Liquid 模板的 `t` 过滤器和 `shop.locale` 对象实现自动语言适配。
  16 +
  17 +### 2. 支持的语言列表
  18 +
  19 +根据 [Lessjs 文档](https://lessjs.shoplazza.com/latest/docs/introduction/),Shoplazza 支持以下语言:
  20 +
  21 +| 语言代码 | 语言名称 | 示例文本 |
  22 +|---------|---------|---------|
  23 +| `ar-SA` | 阿拉伯语(沙特阿拉伯) | أضف إلى السلة |
  24 +| `de-DE` | 德语 | In den Warenkorb legen |
  25 +| `en-US` | 英语 | Add to cart |
  26 +| `es-ES` | 西班牙语 | Añadir a la cesta |
  27 +| `fr-FR` | 法语 | Ajouter au panier |
  28 +| `id-ID` | 印度尼西亚语 | Masukkan ke keranjang |
  29 +| `it-IT` | 意大利语 | Aggiungi al carrello |
  30 +| `ja-JP` | 日语 | カートに追加 |
  31 +| `ko-KR` | 韩语 | 카트에 추가하십시오 |
  32 +| `nl-NL` | 荷兰语 | Voeg toe aan winkelkar |
  33 +| `pl-PL` | 波兰语 | Dodaj do koszyka |
  34 +| `pt-PT` | 葡萄牙语 | Adicionar ao carrinho |
  35 +| `ru-RU` | 俄语 | Добавить в корзину |
  36 +| `th-TH` | 泰语 | เพิ่มลงในรถเข็น |
  37 +| `zh-CN` | 中文(简体中文) | 加入购物车 |
  38 +| `zh-TW` | 中文(繁体中文) | 加入購物車 |
  39 +
  40 +### 3. 实现方式
  41 +
  42 +#### 3.1 目录结构
  43 +
  44 +```
  45 +extensions/your-extension/
  46 +├── blocks/
  47 +│ └── your-block.liquid
  48 +├── assets/
  49 +│ └── your-block.css
  50 +└── locales/
  51 + ├── zh-CN.json
  52 + ├── en-US.json
  53 + ├── ko-KR.json
  54 + ├── ja-JP.json
  55 + └── ... (其他语言文件)
  56 +```
  57 +
  58 +#### 3.2 语言文件格式
  59 +
  60 +每个语言文件(如 `locales/zh-CN.json`)应包含完整的翻译键值对:
  61 +
  62 +```json
  63 +{
  64 + "search": {
  65 + "placeholder": "搜索商品...",
  66 + "loading": "搜索中,请稍候...",
  67 + "no_results": "未找到相关商品",
  68 + "error": "搜索失败,请重试",
  69 + "results_count": "找到 {count} 个结果",
  70 + "sort_label": "排序:",
  71 + "sort_default": "默认排序",
  72 + "sort_price_asc": "价格:低到高",
  73 + "sort_price_desc": "价格:高到低",
  74 + "sort_newest": "最新上架",
  75 + "facet_category": "类目",
  76 + "facet_brand": "品牌",
  77 + "facet_tag": "标签",
  78 + "pagination_prev": "上一页",
  79 + "pagination_next": "下一页",
  80 + "out_of_stock": "缺货",
  81 + "unknown_product": "未知商品",
  82 + "enter_keyword": "请输入搜索关键词",
  83 + "searching": "搜索中..."
  84 + }
  85 +}
  86 +```
  87 +
  88 +#### 3.3 Liquid 模板中的使用
  89 +
  90 +##### 3.3.1 获取店铺语言
  91 +
  92 +```liquid
  93 +{% comment %} 获取店铺当前语言 {% endcomment %}
  94 +{{ shop.locale }}
  95 +```
  96 +
  97 +##### 3.3.2 使用翻译过滤器
  98 +
  99 +在 HTML 属性中使用:
  100 +
  101 +```liquid
  102 +<input
  103 + type="text"
  104 + placeholder="{{ 'search.placeholder' | t }}"
  105 + aria-label="{{ 'search.placeholder' | t }}"
  106 +/>
  107 +```
  108 +
  109 +在 HTML 内容中使用:
  110 +
  111 +```liquid
  112 +<label>{{ 'search.sort_label' | t }}</label>
  113 +```
  114 +
  115 +在 `<option>` 标签中使用(需要配合 JavaScript 确保正确显示):
  116 +
  117 +```liquid
  118 +<select id="sort-select">
  119 + <option value="" data-translation-key="sortDefault">
  120 + {{ 'search.sort_default' | t }}
  121 + </option>
  122 + <option value="min_price:asc" data-translation-key="sortPriceAsc">
  123 + {{ 'search.sort_price_asc' | t }}
  124 + </option>
  125 +</select>
  126 +```
  127 +
  128 +#### 3.4 JavaScript 中的多语言支持
  129 +
  130 +##### 3.4.1 将翻译传递给 JavaScript
  131 +
  132 +在 `<script>` 标签中,通过 Liquid 将翻译文本传递给 JavaScript:
  133 +
  134 +```javascript
  135 +<script>
  136 +(function() {
  137 + // 获取店铺语言
  138 + const shopLocale = "{{ shop.locale }}" || "zh-CN";
  139 +
  140 + // 翻译文本(通过 Liquid 的 t 过滤器获取)
  141 + const translations = {
  142 + placeholder: "{{ 'search.placeholder' | t }}",
  143 + loading: "{{ 'search.loading' | t }}",
  144 + noResults: "{{ 'search.no_results' | t }}",
  145 + error: "{{ 'search.error' | t }}",
  146 + resultsCount: "{{ 'search.results_count' | t }}",
  147 + // ... 更多翻译键
  148 + };
  149 +
  150 + // 工具函数:替换占位符
  151 + function translate(key, params) {
  152 + let text = translations[key] || key;
  153 + if (params) {
  154 + Object.keys(params).forEach(param => {
  155 + text = text.replace(`{${param}}`, params[param]);
  156 + });
  157 + }
  158 + return text;
  159 + }
  160 +
  161 + // 使用翻译
  162 + function showResults(total) {
  163 + const message = translate('resultsCount', { count: total });
  164 + document.getElementById('status').textContent = message;
  165 + }
  166 +})();
  167 +</script>
  168 +```
  169 +
  170 +##### 3.4.2 处理动态内容翻译
  171 +
  172 +对于动态生成的内容(如排序下拉框),需要确保翻译正确显示:
  173 +
  174 +```javascript
  175 +// 初始化排序下拉框文本(确保翻译正确显示)
  176 +function initSortSelect() {
  177 + const sortSelect = document.getElementById('sort-select');
  178 + if (sortSelect) {
  179 + // 遍历所有选项,使用 JavaScript 设置文本
  180 + Array.from(sortSelect.options).forEach((option, index) => {
  181 + const translationKey = option.getAttribute('data-translation-key');
  182 + if (translationKey && translations[translationKey]) {
  183 + option.text = translations[translationKey];
  184 + }
  185 + });
  186 + }
  187 +}
  188 +
  189 +// 在 DOM 加载完成后初始化
  190 +document.addEventListener('DOMContentLoaded', function() {
  191 + initSortSelect();
  192 +});
  193 +```
  194 +
  195 +##### 3.4.3 翻译回退机制(可选)
  196 +
  197 +如果 Liquid 翻译失败,可以实现 JavaScript 回退机制:
  198 +
  199 +```javascript
  200 +function checkAndLoadTranslations() {
  201 + const firstTranslation = translations.placeholder;
  202 + const isLikelyEnglish = firstTranslation === 'Search products...' ||
  203 + firstTranslation === 'search.placeholder' ||
  204 + firstTranslation === '';
  205 +
  206 + // 如果检测到可能是英语,且店铺语言不是英语,尝试加载正确的翻译
  207 + if (isLikelyEnglish && shopLocale && shopLocale !== 'en-US' && shopLocale !== 'en') {
  208 + const localeFile = `/extensions/your-extension/locales/${shopLocale}.json`;
  209 + fetch(localeFile)
  210 + .then(response => response.json())
  211 + .then(data => {
  212 + if (data && data.search) {
  213 + // 更新翻译对象
  214 + translations = {
  215 + ...translations,
  216 + placeholder: data.search.placeholder || translations.placeholder,
  217 + loading: data.search.loading || translations.loading,
  218 + // ... 更多翻译
  219 + };
  220 +
  221 + // 重新初始化UI
  222 + initSortSelect();
  223 + }
  224 + })
  225 + .catch(error => {
  226 + console.warn('Failed to load translation file:', error);
  227 + });
  228 + }
  229 +}
  230 +```
  231 +
  232 +### 4. 最佳实践
  233 +
  234 +1. **完整性**:确保所有语言文件包含相同的翻译键,避免缺失导致显示键名
  235 +2. **占位符**:使用 `{count}` 等占位符支持动态内容,如 `"找到 {count} 个结果"`
  236 +3. **命名规范**:使用有意义的键名,如 `search.placeholder` 而不是 `text1`
  237 +4. **测试**:在不同语言环境下测试应用,确保所有文本正确显示
  238 +5. **回退机制**:为关键翻译提供默认值,避免显示空白或键名
  239 +
  240 +---
  241 +
  242 +## 前端优化与 Lessjs 集成
  243 +
  244 +### 1. Lessjs 简介
  245 +
  246 +**Lessjs** 是 Shoplazza(店匠)官方提供的前端组件库,基于 **Web Components** 开发。它提供了:
  247 +
  248 +- 高性能页面构建能力
  249 +- 现成的组件(轮播图、视频、瀑布流等)
  250 +- 自定义组件支持
  251 +- 与 Shoplazza 主题的深度集成
  252 +
  253 +**官方文档**:
  254 +- [Lessjs 简介](https://lessjs.shoplazza.com/latest/docs/introduction/)
  255 +- [自定义组件](https://lessjs.shoplazza.com/latest/docs/custom-component/)
  256 +
  257 +### 2. 引入 Lessjs
  258 +
  259 +#### 2.1 通过 CDN 引入
  260 +
  261 +在 Liquid 模板的 `<head>` 标签中引入 Lessjs:
  262 +
  263 +```liquid
  264 +<head>
  265 + <meta charset="UTF-8">
  266 + <title>Your App</title>
  267 +
  268 + {% comment %} 引入 Lessjs {% endcomment %}
  269 + <script async crossorigin="anonymous" src="https://static.staticdj.com/cuttlefish/v1/spz.min.js"></script>
  270 +
  271 + {% comment %} Lessjs 加载前的样式(必须){% endcomment %}
  272 + <style>
  273 + body {
  274 + -webkit-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
  275 + -moz-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
  276 + -ms-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
  277 + animation: -spz-start 8s steps(1,end) 0s 1 normal both;
  278 + }
  279 + @-webkit-keyframes -spz-start {
  280 + from { visibility: hidden; }
  281 + to { visibility: visible; }
  282 + }
  283 + @-moz-keyframes -spz-start {
  284 + from { visibility: hidden; }
  285 + to { visibility: visible; }
  286 + }
  287 + @-ms-keyframes -spz-start {
  288 + from { visibility: hidden; }
  289 + to { visibility: visible; }
  290 + }
  291 + @-o-keyframes -spz-start {
  292 + from { visibility: hidden; }
  293 + to { visibility: visible; }
  294 + }
  295 + @keyframes -spz-start {
  296 + from { visibility: hidden; }
  297 + to { visibility: visible; }
  298 + }
  299 + </style>
  300 + <noscript>
  301 + <style>
  302 + body {
  303 + -webkit-animation: none;
  304 + -moz-animation: none;
  305 + -ms-animation: none;
  306 + animation: none;
  307 + }
  308 + </style>
  309 + </noscript>
  310 +</head>
  311 +```
  312 +
  313 +**重要要求**:
  314 +1. 脚本必须带有 `async` 属性
  315 +2. 必须包含上述 CSS 样式,用于在 Lessjs 加载前隐藏页面内容
  316 +3. 必须包含 `<noscript>` 标签的样式回退
  317 +
  318 +#### 2.2 HTML lang 属性
  319 +
  320 +确保 HTML 的 `lang` 属性设置为支持的语言代码之一(见[支持的语言列表](#2-支持的语言列表)):
  321 +
  322 +```liquid
  323 +<html lang="{{ shop.locale | default: 'zh-CN' }}">
  324 +```
  325 +
  326 +### 3. 创建自定义 Lessjs 组件
  327 +
  328 +#### 3.1 基础结构
  329 +
  330 +Lessjs 自定义组件基于 ES6 class,继承 `SPZ.BaseElement`:
  331 +
  332 +```liquid
  333 +{% comment %} 使用自定义组件 {% endcomment %}
  334 +<spz-custom-ai-search layout="container"></spz-custom-ai-search>
  335 +
  336 +{% comment %} 定义自定义组件 {% endcomment %}
  337 +<spz-script layout="logic" type="application/javascript">
  338 + class SpzCustomAiSearch extends SPZ.BaseElement {
  339 + constructor(element) {
  340 + super(element);
  341 + // 初始化变量
  342 + this.searchQuery = '';
  343 + this.currentPage = 1;
  344 + }
  345 +
  346 + isLayoutSupported(layout) {
  347 + // 支持的布局类型
  348 + return layout == SPZCore.Layout.CONTAINER;
  349 + }
  350 +
  351 + buildCallback() {
  352 + // 构建回调:组件开始构建时触发,用于初始化
  353 + console.log('Component building...');
  354 + }
  355 +
  356 + mountCallback() {
  357 + // 挂载回调:组件挂载完成后触发,用于数据请求
  358 + console.log('Component mounted, fetching data...');
  359 + this.performSearch();
  360 + }
  361 +
  362 + unmountCallback() {
  363 + // 卸载回调:组件卸载前触发,用于清理
  364 + console.log('Component unmounting, cleaning up...');
  365 + // 移除事件监听器等
  366 + }
  367 +
  368 + layoutCallback() {
  369 + // 布局回调:元素及子节点完全挂载完成,用于 DOM 操作
  370 + console.log('Layout complete, ready for DOM operations');
  371 + }
  372 + }
  373 +
  374 + // 注册自定义组件
  375 + SPZ.defineElement('spz-custom-ai-search', SpzCustomAiSearch);
  376 +</spz-script>
  377 +```
  378 +
  379 +#### 3.2 命名规范
  380 +
  381 +- **组件名称**:必须以 `spz-custom-` 开头,如 `spz-custom-ai-search`
  382 +- **私有变量/函数**:以 `_` 结尾,如 `this.searchQuery_`
  383 +- **公共 API 方法**:不以 `_` 结尾,如 `this.performSearch()`
  384 +
  385 +#### 3.3 生命周期函数
  386 +
  387 +| 函数名 | 触发时机 | 用途 |
  388 +|--------|---------|------|
  389 +| `constructor` | 组件创建时 | 初始化变量,必须调用 `super(element)` |
  390 +| `isLayoutSupported` | 布局检查时 | 返回组件支持的布局类型 |
  391 +| `buildCallback` | 组件开始构建时 | 初始化工作 |
  392 +| `mountCallback` | 组件挂载完成后 | 数据请求、API 调用 |
  393 +| `unmountCallback` | 组件卸载前 | 清理事件监听、重置变量 |
  394 +| `layoutCallback` | DOM 完全挂载后 | DOM 操作、事件绑定 |
  395 +
  396 +#### 3.4 this 对象
  397 +
  398 +自定义组件中的 `this` 对象提供以下属性和方法:
  399 +
  400 +| 属性/方法 | 说明 |
  401 +|----------|------|
  402 +| `this.element` | 当前组件 DOM 元素 |
  403 +| `this.win` | window 对象 |
  404 +| `this.getViewport()` | 获取 Viewport 服务(参考 [SPZServices 文档](https://lessjs.shoplazza.com/latest/docs/spz-services/)) |
  405 +
  406 +#### 3.5 完整示例:AI 搜索组件
  407 +
  408 +```liquid
  409 +{% use "ai-search.css" %}
  410 +
  411 +<div id="ai-search" class="ai-search-container">
  412 + <spz-custom-ai-search layout="container">
  413 + <!-- 搜索框 -->
  414 + <form class="search-form" id="search-form">
  415 + <div class="search-input-wrapper">
  416 + <input
  417 + type="text"
  418 + id="search-input"
  419 + class="search-input"
  420 + placeholder="{{ 'search.placeholder' | t }}"
  421 + autocomplete="off"
  422 + />
  423 + <button type="submit" class="search-button" id="search-button">
  424 + <svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
  425 + <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
  426 + </svg>
  427 + </button>
  428 + </div>
  429 + </form>
  430 +
  431 + <!-- 搜索结果容器 -->
  432 + <div class="search-results-container" id="search-results-container"></div>
  433 + </spz-custom-ai-search>
  434 +</div>
  435 +
  436 +<spz-script layout="logic" type="application/javascript">
  437 + class SpzCustomAiSearch extends SPZ.BaseElement {
  438 + constructor(element) {
  439 + super(element);
  440 + this.apiEndpoint = 'https://saas-ai-api.essa.top/app-api/app/shoplazza/search/';
  441 + this.state = {
  442 + query: '',
  443 + currentPage: 1,
  444 + pageSize: 20
  445 + };
  446 + }
  447 +
  448 + isLayoutSupported(layout) {
  449 + return layout == SPZCore.Layout.CONTAINER;
  450 + }
  451 +
  452 + buildCallback() {
  453 + // 初始化翻译
  454 + this.shopLocale = "{{ shop.locale }}" || "zh-CN";
  455 + this.translations = {
  456 + placeholder: "{{ 'search.placeholder' | t }}",
  457 + loading: "{{ 'search.loading' | t }}",
  458 + noResults: "{{ 'search.no_results' | t }}",
  459 + // ... 更多翻译
  460 + };
  461 + }
  462 +
  463 + mountCallback() {
  464 + // 绑定搜索表单事件
  465 + const form = this.element.querySelector('#search-form');
  466 + if (form) {
  467 + form.addEventListener('submit', (e) => this.handleSearch(e));
  468 + }
  469 + }
  470 +
  471 + async handleSearch(event) {
  472 + event.preventDefault();
  473 +
  474 + const searchInput = this.element.querySelector('#search-input');
  475 + const query = searchInput.value.trim();
  476 +
  477 + if (!query) {
  478 + alert(this.translations.enterKeyword);
  479 + return;
  480 + }
  481 +
  482 + this.state.query = query;
  483 + this.state.currentPage = 1;
  484 +
  485 + await this.performSearch();
  486 + }
  487 +
  488 + async performSearch() {
  489 + const resultsContainer = this.element.querySelector('#search-results-container');
  490 + resultsContainer.innerHTML = `<div class="loading">${this.translations.loading}</div>`;
  491 +
  492 + try {
  493 + const response = await fetch(this.apiEndpoint, {
  494 + method: 'POST',
  495 + headers: { 'Content-Type': 'application/json' },
  496 + body: JSON.stringify({
  497 + query: this.state.query,
  498 + size: this.state.pageSize,
  499 + from: (this.state.currentPage - 1) * this.state.pageSize
  500 + })
  501 + });
  502 +
  503 + const data = await response.json();
  504 + this.renderResults(data);
  505 + } catch (error) {
  506 + resultsContainer.innerHTML = `<div class="error">${this.translations.error}</div>`;
  507 + }
  508 + }
  509 +
  510 + renderResults(data) {
  511 + const resultsContainer = this.element.querySelector('#search-results-container');
  512 + // 渲染搜索结果...
  513 + }
  514 +
  515 + unmountCallback() {
  516 + // 清理工作
  517 + }
  518 + }
  519 +
  520 + SPZ.defineElement('spz-custom-ai-search', SpzCustomAiSearch);
  521 +</spz-script>
  522 +```
  523 +
  524 +### 4. 使用 Lessjs 的优势
  525 +
  526 +1. **性能优化**:Lessjs 基于 Web Components,提供更好的性能
  527 +2. **主题集成**:与 Shoplazza 主题深度集成,样式更统一
  528 +3. **组件复用**:可以使用 Lessjs 提供的现成组件(轮播图、视频等)
  529 +4. **标准化**:遵循 Shoplazza 的开发规范,减少兼容性问题
  530 +
  531 +---
  532 +
  533 +## 与店匠主题的适配
  534 +
  535 +### 1. 样式适配
  536 +
  537 +#### 1.1 使用主题变量
  538 +
  539 +尽可能使用 Shoplazza 主题提供的 CSS 变量:
  540 +
  541 +```css
  542 +/* 使用主题颜色变量 */
  543 +.ai-search-container {
  544 + color: var(--color-text, #333);
  545 + background: var(--color-background, #fff);
  546 + border-color: var(--color-border, #e0e0e0);
  547 +}
  548 +
  549 +/* 使用主题字体变量 */
  550 +.ai-search-container {
  551 + font-family: var(--font-body-family, sans-serif);
  552 + font-size: var(--font-body-size, 14px);
  553 +}
  554 +```
  555 +
  556 +#### 1.2 响应式设计
  557 +
  558 +遵循 Shoplazza 的响应式断点:
  559 +
  560 +```css
  561 +/* 移动端 */
  562 +@media (max-width: 749px) {
  563 + .ai-search-container {
  564 + padding: 10px;
  565 + }
  566 +}
  567 +
  568 +/* 平板 */
  569 +@media (min-width: 750px) and (max-width: 989px) {
  570 + .ai-search-container {
  571 + padding: 20px;
  572 + }
  573 +}
  574 +
  575 +/* 桌面端 */
  576 +@media (min-width: 990px) {
  577 + .ai-search-container {
  578 + padding: 30px;
  579 + }
  580 +}
  581 +```
  582 +
  583 +### 2. 功能适配
  584 +
  585 +#### 2.1 使用 Shoplazza 全局对象
  586 +
  587 +在 JavaScript 中,可以使用 Shoplazza 提供的全局对象:
  588 +
  589 +```javascript
  590 +// 访问店铺信息
  591 +const shopDomain = window.Shopify?.shop || "{{ shop.domain }}";
  592 +const shopLocale = window.Shopify?.locale || "{{ shop.locale }}";
  593 +
  594 +// 使用 Shoplazza 的工具函数(如果可用)
  595 +if (window.Shoplazza?.utils) {
  596 + // 使用工具函数
  597 +}
  598 +```
  599 +
  600 +#### 2.2 事件处理
  601 +
  602 +遵循 Shoplazza 的事件处理规范:
  603 +
  604 +```javascript
  605 +// 使用事件委托,提高性能
  606 +document.addEventListener('click', (e) => {
  607 + if (e.target.closest('.search-button')) {
  608 + handleSearch(e);
  609 + }
  610 +});
  611 +
  612 +// 避免直接操作 DOM,使用 Lessjs 组件生命周期
  613 +```
  614 +
  615 +### 3. 性能优化建议
  616 +
  617 +1. **延迟加载**:非关键资源使用 `defer` 或 `async`
  618 +2. **图片优化**:使用 Shoplazza 的图片 CDN 和响应式图片
  619 +3. **代码分割**:将大型 JavaScript 拆分为多个模块
  620 +4. **缓存策略**:合理使用浏览器缓存和 CDN 缓存
  621 +
  622 +### 4. 测试清单
  623 +
  624 +- [ ] 在不同语言环境下测试多语言显示
  625 +- [ ] 在不同设备上测试响应式布局
  626 +- [ ] 测试与不同 Shoplazza 主题的兼容性
  627 +- [ ] 测试 Lessjs 组件的加载和渲染
  628 +- [ ] 测试 API 调用的错误处理
  629 +- [ ] 测试页面加载性能
  630 +
  631 +---
  632 +
  633 +## 总结
  634 +
  635 +1. **多语言支持**:使用 Liquid 的 `t` 过滤器和 `shop.locale`,确保所有语言文件完整
  636 +2. **Lessjs 集成**:使用 Lessjs 自定义组件提升性能和主题适配
  637 +3. **主题适配**:使用主题变量和响应式设计,确保与店匠主题良好集成
  638 +
  639 +遵循以上指南,可以开发出高质量、高性能、多语言支持的 Shoplazza 应用。
  640 +
  641 +---
  642 +
  643 +## 参考资源
  644 +
  645 +- [Lessjs 官方文档](https://lessjs.shoplazza.com/latest/docs/introduction/)
  646 +- [Lessjs 自定义组件文档](https://lessjs.shoplazza.com/latest/docs/custom-component/)
  647 +- [Shoplazza 开发者文档](https://www.shoplazza.dev/)
  648 +
... ...
docs/索引字段说明.md renamed to docs/索引字段说明v1.md
... ... @@ -158,7 +158,7 @@ sku全部字段
158 158 商品信息:品名、颜色、尺码,基本上就这三个信息
159 159  
160 160  
161   -### 分类
  161 +### 分类prefixQuery
162 162 最多三级分类,商品可以指向任何级别的分类
163 163 Field Type
164 164 category varchar(255)
... ...
docs/索引字段说明v2-plan.md 0 → 100644
... ... @@ -0,0 +1,168 @@
  1 +# 索引重构v2方案
  2 +
  3 +## 概述
  4 +
  5 +根据 `索引字段说明v2.md` 的要求重构Elasticsearch索引映射结构和MySQL到ES的数据导入脚本。
  6 +
  7 +## 主要变更点
  8 +
  9 +### 1. 索引映射结构重构
  10 +
  11 +#### 1.1 多语言文本字段
  12 +
  13 +- 为文本字段添加中英文双字段支持(title_zh/title_en, brief_zh/brief_en, description_zh/description_en, vendor_zh/vendor_en)
  14 +- 中文字段使用 `index_ansj`/`query_ansj` 分析器(对应文档中的hanlp_index/hanlp_standard)
  15 +- 英文字段使用 `english` 分析器
  16 +- **暂时只填充中文字段,英文字段设为空**(不需要语言检测,每个tenant的语言预先知道)
  17 +
  18 +#### 1.2 分类字段多层级支持
  19 +
  20 +类别数据源:
  21 +在spu表中:
  22 +Field Type
  23 +category varchar(255)
  24 +category_id bigint(20)
  25 +category_google_id bigint(20)
  26 +category_level int(11)
  27 +category_path varchar(500)
  28 +
  29 +mapping:
  30 +
  31 + "category_path_zh": { // 提供模糊查询功能,辅助相关性计算
  32 + "type": "text",
  33 + "analyzer": "hanlp_index",
  34 + "search_analyzer": "hanlp_standard"
  35 + },
  36 + "category_path_en": { // 提供模糊查询功能,辅助相关性计算
  37 + "type": "text",
  38 + "analyzer": "english",
  39 + "search_analyzer": "english"
  40 + },
  41 + "category_name_zh": { // 提供模糊查询功能,辅助相关性计算
  42 + "type": "text",
  43 + "analyzer": "hanlp_index",
  44 + "search_analyzer": "hanlp_standard"
  45 + },
  46 + "category_name_en": { // 提供模糊查询功能,辅助相关性计算
  47 + "type": "text",
  48 + "analyzer": "english",
  49 + "search_analyzer": "english"
  50 + },
  51 +
  52 + "category_id": {
  53 + "type": "keyword"
  54 + },
  55 + "category_name": {
  56 + "type": "keyword"
  57 + },
  58 + "category_level": {
  59 + "type": "integer"
  60 + },
  61 + "category1_name": { // 不同层级下 可能有同名的情况,因此提供一二三级分开的查询方式
  62 + "type": "keyword"
  63 + },
  64 + "category2_name": {
  65 + "type": "keyword"
  66 + },
  67 + "category3_name": {
  68 + "type": "keyword"
  69 + },
  70 +
  71 +
  72 +#### 1.3 SKU字段展开
  73 +
  74 +- 添加 `sku_prices` (float数组) - 所有SKU价格列表
  75 +- 添加 `sku_weights` (long数组) - 重量数值列表(转换为整数克)
  76 +- 添加 `sku_weight_units` (keyword数组) - 重量+单位字符串列表
  77 +- 添加 `total_inventory` (long) - SKU库存总和
  78 +- 保留 `min_price`, `max_price` (float)
  79 +
  80 +#### 1.4 选项字段处理
  81 +
  82 +- 添加 `option1_name`, `option2_name`, `option3_name` (keyword) - SPU级别的选项名称定义
  83 +- 修改SKU嵌套结构:将 `options` 对象改为 `option1_value`, `option2_value`, `option3_value` (keyword)
  84 +- 添加 `specifications` (nested, index=false) - 动态属性,仅用于返回
  85 +
  86 +#### 1.5 标签字段
  87 +
  88 +- `tags` 改为 keyword 数组类型(分割逗号分隔的字符串)
  89 +
  90 +#### 1.6 SKU嵌套结构更新
  91 +
  92 +- 添加 `sku_code` (keyword) 字段
  93 +- 添加 `weight` (float), `weight_unit` (keyword)
  94 +- 将 `options` 对象改为 `option1_value`, `option2_value`, `option3_value`
  95 +- 添加 `image_src` (keyword, index=false)
  96 +
  97 +#### 1.7 删除SEO字段
  98 +
  99 +- **完全删除** `seo_title`, `seo_description`, `seo_keywords` 字段
  100 +- 从索引映射中移除
  101 +- 从数据转换脚本中移除相关处理
  102 +
  103 +### 2. 数据转换脚本重构
  104 +
  105 +#### 2.1 多语言文本处理
  106 +
  107 +- **简化处理**:暂时只填充中文字段(title_zh, brief_zh, description_zh, vendor_zh)
  108 +- 英文字段(title_en, brief_en, description_en, vendor_en)设为空或None
  109 +- 不需要语言检测逻辑
  110 +
  111 +#### 2.2 分类路径解析
  112 +
  113 +- 从 `category_path` 字段按 "/" 分割提取分类层级
  114 +- 分割结果赋值给 `category1_name`, `category2_name`, `category3_name`
  115 +- 生成 `category_path_zh`(暂时填充,`category_path_en` 设为空)
  116 +
  117 +#### 2.3 SKU字段展开计算
  118 +
  119 +- 提取所有SKU的价格,生成 `sku_prices` 数组
  120 +- 提取所有SKU的重量,转换为克(乘以1000),生成 `sku_weights` 数组
  121 +- 生成 `sku_weight_units` 数组(格式:"重量值单位")
  122 +- 计算所有SKU库存总和,赋值给 `total_inventory`
  123 +- 计算 `min_price` 和 `max_price`
  124 +
  125 +#### 2.4 选项字段处理
  126 +
  127 +- SPU级别的选项名称:需要从SPU数据或SKU数据推断(如果SPU表中没有,需要查看是否有选项表)
  128 +- SKU级别的选项值:从SKU的 `option1`, `option2`, `option3` 字段提取
  129 +- 生成 `specifications` 嵌套数组(从选项名称和值对生成)
  130 +
  131 +#### 2.5 标签处理
  132 +
  133 +- `tags` 字段按逗号分割转换为数组
  134 +
  135 +#### 2.6 删除SEO字段处理
  136 +
  137 +- **完全移除** seo_title, seo_description, seo_keywords 相关代码
  138 +
  139 +## 实施步骤
  140 +
  141 +### 步骤1:修改索引映射生成器
  142 +
  143 +**文件**: `indexer/mapping_generator.py` 和相关字段配置
  144 +
  145 +- 更新 `get_es_mapping_for_field` 函数以支持多语言字段
  146 +- 添加分类字段的完整映射生成逻辑
  147 +- 添加SKU展开字段的映射
  148 +- 添加选项字段的映射
  149 +- **删除SEO字段的映射生成**
  150 +
  151 +### 步骤2:重构数据转换脚本
  152 +
  153 +**文件**: `indexer/spu_transformer.py`
  154 +
  155 +- **简化多语言处理**:只填充中文字段(title_zh, brief_zh等),英文字段设为空或None
  156 +- 实现分类路径的解析和展开
  157 +- 实现SKU字段的展开计算(价格、重量、库存)
  158 +- 实现选项字段的处理
  159 +- 更新SKU嵌套结构的生成
  160 +- 处理标签的分割
  161 +- **删除所有SEO字段相关代码**
  162 +
  163 +### 步骤3:更新配置文件
  164 +
  165 +**文件**: `config/config.yaml`
  166 +
  167 +- **删除SEO字段配置**:移除 seo_title, seo_description, seo_keywords 的字段定义
  168 +- 可能需要添加新字段的配置定义,或者直接在代码中生成映射(如果配置系统不支持这些复杂字段)
0 169 \ No newline at end of file
... ...
docs/索引字段说明v2-参考表结构.md 0 → 100644
... ... @@ -0,0 +1,81 @@
  1 +spu表全部字段
  2 +"Field" "Type" "Null" "Key" "Default" "Extra"
  3 +"id" "bigint(20)" "NO" "PRI" "auto_increment"
  4 +"shop_id" "bigint(20)" "NO" "MUL" ""
  5 +"shoplazza_id" "varchar(64)" "NO" "" ""
  6 +"handle" "varchar(255)" "YES" "MUL" ""
  7 +"title" "varchar(500)" "NO" "" ""
  8 +"brief" "varchar(1000)" "YES" "" ""
  9 +"description" "text" "YES" "" ""
  10 +"spu" "varchar(100)" "YES" "" ""
  11 +"vendor" "varchar(255)" "YES" "" ""
  12 +"vendor_url" "varchar(500)" "YES" "" ""
  13 +"seo_title" "varchar(500)" "YES" "" ""
  14 +"seo_description" "text" "YES" "" ""
  15 +"seo_keywords" "text" "YES" "" ""
  16 +"image_src" "varchar(500)" "YES" "" ""
  17 +"image_width" "int(11)" "YES" "" ""
  18 +"image_height" "int(11)" "YES" "" ""
  19 +"image_path" "varchar(255)" "YES" "" ""
  20 +"image_alt" "varchar(500)" "YES" "" ""
  21 +"inventory_policy" "varchar(50)" "YES" "" ""
  22 +"inventory_quantity" "int(11)" "YES" "" "0" ""
  23 +"inventory_tracking" "tinyint(1)" "YES" "" "0" ""
  24 +"published" "tinyint(1)" "YES" "" "0" ""
  25 +"published_at" "datetime" "YES" "MUL" ""
  26 +"requires_shipping" "tinyint(1)" "YES" "" "1" ""
  27 +"taxable" "tinyint(1)" "YES" "" "0" ""
  28 +"fake_sales" "int(11)" "YES" "" "0" ""
  29 +"display_fake_sales" "tinyint(1)" "YES" "" "0" ""
  30 +"mixed_wholesale" "tinyint(1)" "YES" "" "0" ""
  31 +"need_variant_image" "tinyint(1)" "YES" "" "0" ""
  32 +"has_only_default_variant" "tinyint(1)" "YES" "" "0" ""
  33 +"tags" "text" "YES" "" ""
  34 +"note" "text" "YES" "" ""
  35 +"category" "varchar(255)" "YES" "" ""
  36 +"category_id" "bigint(20)" "YES" "" ""
  37 +"category_google_id" "bigint(20)" "YES" "" ""
  38 +"category_level" "int(11)" "YES" "" ""
  39 +"category_path" "varchar(500)" "YES" "" ""
  40 +"shoplazza_created_at" "datetime" "YES" "" ""
  41 +"shoplazza_updated_at" "datetime" "YES" "MUL" ""
  42 +"tenant_id" "bigint(20)" "NO" "MUL" ""
  43 +"creator" "varchar(64)" "YES" "" "" ""
  44 +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" ""
  45 +"updater" "varchar(64)" "YES" "" "" ""
  46 +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP"
  47 +"deleted" "bit(1)" "NO" "" "b'0'" ""
  48 +
  49 +sku全部字段
  50 +"Field" "Type" "Null" "Key" "Default" "Extra"
  51 +"id" "bigint(20)" "NO" "PRI" "auto_increment"
  52 +"spu_id" "bigint(20)" "NO" "MUL" ""
  53 +"shop_id" "bigint(20)" "NO" "MUL" ""
  54 +"shoplazza_id" "varchar(64)" "NO" "" ""
  55 +"shoplazza_product_id" "varchar(64)" "NO" "MUL" ""
  56 +"shoplazza_image_id" "varchar(64)" "YES" "" ""
  57 +"title" "varchar(500)" "YES" "" ""
  58 +"sku" "varchar(100)" "YES" "MUL" ""
  59 +"barcode" "varchar(100)" "YES" "" ""
  60 +"position" "int(11)" "YES" "" "0" ""
  61 +"price" "decimal(10,2)" "YES" "" ""
  62 +"compare_at_price" "decimal(10,2)" "YES" "" ""
  63 +"cost_price" "decimal(10,2)" "YES" "" ""
  64 +"option1" "varchar(255)" "YES" "" ""
  65 +"option2" "varchar(255)" "YES" "" ""
  66 +"option3" "varchar(255)" "YES" "" ""
  67 +"inventory_quantity" "int(11)" "YES" "" "0" ""
  68 +"weight" "decimal(10,2)" "YES" "" ""
  69 +"weight_unit" "varchar(10)" "YES" "" ""
  70 +"image_src" "varchar(500)" "YES" "" ""
  71 +"wholesale_price" "json" "YES" "" ""
  72 +"note" "text" "YES" "" ""
  73 +"extend" "json" "YES" "" ""
  74 +"shoplazza_created_at" "datetime" "YES" "" ""
  75 +"shoplazza_updated_at" "datetime" "YES" "" ""
  76 +"tenant_id" "bigint(20)" "NO" "MUL" ""
  77 +"creator" "varchar(64)" "YES" "" "" ""
  78 +"create_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" ""
  79 +"updater" "varchar(64)" "YES" "" "" ""
  80 +"update_time" "datetime" "NO" "" "CURRENT_TIMESTAMP" "on update CURRENT_TIMESTAMP"
  81 +"deleted" "bit(1)" "NO" "" "b'0'" ""
... ...
docs/索引字段说明v2.md 0 → 100644
... ... @@ -0,0 +1,358 @@
  1 +SPU-SKU索引方案选型
  2 +1. spu为单位。SKU字段展开作为SPU属性
  3 +1.1 索引方案
  4 +除了title, brielf description seo相关 cate tags vendor所有影响相关性的字段都在spu。 sku只有款式、价格、重量、库存等相关属性。所以,可以以spu为单位建立索引。
  5 +sku中需要参与搜索的属性(比如价格、库存)展开到spu。
  6 +sku的所有需要返回的字段作为nested字段,仅用于返回。
  7 +灌入数据准备
  8 +def build_product_document(product, skus):
  9 + # 提取价格列表(转换为float,保留两位小数)
  10 + price_list = [float(sku.price) for sku in skus if sku.price is not None]
  11 +
  12 + # 提取重量信息(重量转为int,单位统一为克;重量+单位拼接为字符串)
  13 + weight_list = [int(float(sku.weight) * 1000) for sku in skus if sku.weight is not None] # 转为整数克
  14 + weight_with_unit_list = [f"{sku.weight}{sku.weight_unit}" for sku in skus if sku.weight and sku.weight_unit]
  15 +
  16 + # 计算库存总和
  17 + total_stock = sum([sku.inventory_quantity for sku in skus if sku.inventory_quantity is not None])
  18 +
  19 + # 计算价格区间
  20 + min_price = min(price_list) if price_list else 0.0
  21 + max_price = max(price_list) if price_list else 0.0
  22 +
  23 + return {
  24 + "spu_id": str(product.id),
  25 + "title": product.title,
  26 +
  27 + # SPU级别的选项名称定义(如:颜色、尺码、材质)
  28 + "option1_name": getattr(product, 'option1', None),
  29 + "option2_name": getattr(product, 'option2', None),
  30 + "option3_name": getattr(product, 'option3', None),
  31 +
  32 + # SKU搜索字段(展开)
  33 + # 价格(int)、重量(int)、重量单位拼接重量(keyword),都以list形式灌入
  34 + "sku_prices": price_list, # 所有SKU价格列表,用于范围聚合
  35 + "sku_weights": weight_list, # 重量数值列表(转换为整数克)
  36 + "sku_weight_units": weight_with_unit_list, # 重量+单位字符串列表
  37 +
  38 + # 库存总和 将SKU的库存加起来作为一个值灌入
  39 + "total_inventory": total_stock, # SKU库存总和
  40 +
  41 + # 售价,灌入3个字段:SKU价格列表、最高价、最低价
  42 + "min_price": min_price, # 最低售价
  43 + "max_price": max_price, # 最高售价
  44 + "price_range": { # 价格区间对象,便于范围查询
  45 + "gte": min_price,
  46 + "lte": max_price
  47 + },
  48 +
  49 + # SKU详细信息(nested结构,仅用于返回)
  50 + "skus": [
  51 + {
  52 + "sku_id": str(sku.id),
  53 + "price": float(sku.price) if sku.price else 0.0,
  54 + "compare_at_price": float(sku.compare_at_price) if sku.compare_at_price else None,
  55 + "sku_code": sku.sku,
  56 + "stock": sku.inventory_quantity,
  57 + "weight": float(sku.weight) if sku.weight else None,
  58 + "weight_unit": sku.weight_unit,
  59 +
  60 + # SKU级别的选项值(对应SPU的选项名称)
  61 + "option1_value": sku.option1,
  62 + "option2_value": sku.option2,
  63 + "option3_value": sku.option3,
  64 +
  65 + "image_src": sku.image_src
  66 + }
  67 + for sku in skus
  68 + ],
  69 +
  70 + # 其他SPU级别字段(根据索引文档补充)
  71 + "tenant_id": str(product.tenant_id),
  72 + "brief": product.brief,
  73 + "description": product.description,
  74 + "vendor": product.vendor,
  75 + "category": product.category,
  76 + "tags": product.tags.split(',') if product.tags else [],
  77 + "seo_title": product.seo_title,
  78 + "seo_description": product.seo_description,
  79 + "seo_keywords": product.seo_keywords.split(',') if product.seo_keywords else [],
  80 + "image_url": product.image_src,
  81 + "create_time": product.create_time.isoformat() if product.create_time else None,
  82 + "update_time": product.update_time.isoformat() if product.update_time else None
  83 + }
  84 + 索引定义
  85 +{
  86 + "mappings": {
  87 + "properties": {
  88 + "tenant_id": {
  89 + "type": "keyword"
  90 + },
  91 + "spu_id": {
  92 + "type": "keyword"
  93 + },
  94 + // 文本相关性相关字段
  95 + "title_zh": {
  96 + "type": "text",
  97 + "analyzer": "hanlp_index",
  98 + "search_analyzer": "hanlp_standard"
  99 + },
  100 + "brief_zh": {
  101 + "type": "text",
  102 + "analyzer": "hanlp_index",
  103 + "search_analyzer": "hanlp_standard"
  104 + },
  105 + "description_zh": {
  106 + "type": "text",
  107 + "analyzer": "hanlp_index",
  108 + "search_analyzer": "hanlp_standard"
  109 + },
  110 + "vendor_zh": {
  111 + "type": "text",
  112 + "analyzer": "hanlp_index",
  113 + "search_analyzer": "hanlp_standard",
  114 + "fields": {
  115 + "keyword": {
  116 + "type": "keyword",
  117 + "normalizer": "lowercase"
  118 + }
  119 + }
  120 + },
  121 +
  122 + "title_en": {
  123 + "type": "text",
  124 + "analyzer": "english",
  125 + "search_analyzer": "english",
  126 + },
  127 + "brief_en": {
  128 + "type": "text",
  129 + "analyzer": "english",
  130 + "search_analyzer": "english",
  131 +
  132 + },
  133 + "description_en": {
  134 + "type": "text",
  135 + "analyzer": "english",
  136 + "search_analyzer": "english",
  137 + },
  138 + "vendor_en": {
  139 + "type": "text",
  140 + "analyzer": "english",
  141 + "search_analyzer": "english",
  142 + "fields": {
  143 + "keyword": {
  144 + "type": "keyword",
  145 + "normalizer": "lowercase"
  146 + }
  147 + }
  148 + },
  149 +
  150 + "tags": {
  151 + "type": "keyword",
  152 + },
  153 +
  154 +
  155 + "min_price": {
  156 + "type": "float"
  157 + },
  158 + "max_price": {
  159 + "type": "float"
  160 + },
  161 + "compare_at_price": {
  162 + "type": "float"
  163 + },
  164 + "sku_prices": {
  165 + "type": "float"
  166 + },
  167 + "sku_weights": {
  168 + "type": "long"
  169 + },
  170 + "sku_weight_units": {
  171 + "type": "keyword"
  172 + },
  173 + "total_inventory": {
  174 + "type": "long"
  175 + },
  176 +
  177 + "image_url": {
  178 + "type": "keyword",
  179 + "index": false
  180 + },
  181 +
  182 + "title_embedding": {
  183 + "type": "dense_vector",
  184 + "dims": 1024,
  185 + "index": true,
  186 + "similarity": "dot_product"
  187 + },
  188 +
  189 + "create_time": {
  190 + "type": "date"
  191 + },
  192 + "update_time": {
  193 + "type": "date"
  194 + },
  195 +
  196 + "option1_name": {
  197 + "type": "keyword"
  198 + },
  199 + "option2_name": {
  200 + "type": "keyword"
  201 + },
  202 + "option3_name": {
  203 + "type": "keyword"
  204 + },
  205 +
  206 + "skus": {
  207 + "type": "nested",
  208 + "properties": {
  209 + "sku_id": {
  210 + "type": "keyword"
  211 + },
  212 + "price": {
  213 + "type": "float"
  214 + },
  215 + "compare_at_price": {
  216 + "type": "float"
  217 + },
  218 + "sku_code": {
  219 + "type": "keyword"
  220 + },
  221 + "stock": {
  222 + "type": "long"
  223 + },
  224 + "weight": {
  225 + "type": "float"
  226 + },
  227 + "weight_unit": {
  228 + "type": "keyword"
  229 + },
  230 + "option1_value": {
  231 + "type": "keyword"
  232 + },
  233 + "option2_value": {
  234 + "type": "keyword"
  235 + },
  236 + "option3_value": {
  237 + "type": "keyword"
  238 + },
  239 + "image_src": {
  240 + "type": "keyword",
  241 + "index": false
  242 + }
  243 + }
  244 + }
  245 + }
  246 + }
  247 +}
  248 +1.2 查询方案
  249 +对数组字段使用 dis_max,只取最高分,避免累加。
  250 +其他重点字段
  251 +1. Sku title
  252 +2. category
  253 +2.1 Mysql
  254 +在spu表中:
  255 +Field Type
  256 +category varchar(255)
  257 +category_id bigint(20)
  258 +category_google_id bigint(20)
  259 +category_level int(11)
  260 +category_path varchar(500)
  261 +2.2 ES索引
  262 +2.2.1 输入数据
  263 + 设计 1,2,3级分类 三个字段,的 category (原始文本)
  264 +2.2.2 索引方法
  265 + 设计要求:
  266 + 1. 支持facet(精确过滤、keyword聚合),并且性能需要足够高。
  267 + 2. 支持普通搜索模糊匹配(用户原始query可能包括分类词)。
  268 + 3. 模糊匹配要考虑多语言
  269 +方案:采用方案2
  270 + 1. categoryPath索引 + Prefix 查询(categoryPath.keyword: "服装/男装")(如果满足条件的key太多的则性能较差,比如 查询的是一级类目,类目树叶子节点太多时性能较差)
  271 + 2. categoryPath支撑模糊查询 和 多级cate keyword索引支撑精确查询。 索引阶段冗余,查询性能高。
  272 + "category_path_zh": { // 提供模糊查询功能,辅助相关性计算
  273 + "type": "text",
  274 + "analyzer": "hanlp_index",
  275 + "search_analyzer": "hanlp_standard"
  276 + },
  277 + "category_path_en": { // 提供模糊查询功能,辅助相关性计算
  278 + "type": "text",
  279 + "analyzer": "english",
  280 + "search_analyzer": "english"
  281 + },
  282 + "category_path": { // 用于多层级的筛选、精确匹配
  283 + "type": "keyword",
  284 + "normalizer": "lowercase"
  285 + },
  286 + "category_id": {
  287 + "type": "keyword"
  288 + },
  289 + "category_name": {
  290 + "type": "keyword"
  291 + },
  292 + "category_level": {
  293 + "type": "integer"
  294 + },
  295 + "category1_name": { // 不同层级下 可能有同名的情况,因此提供一二三级分开的查询方式
  296 + "type": "keyword"
  297 + },
  298 + "category2_name": {
  299 + "type": "keyword"
  300 + },
  301 + "category3_name": {
  302 + "type": "keyword"
  303 + },
  304 +
  305 +3. tags
  306 +3.1 数据源
  307 +多值
  308 +标签
  309 +最多输入250个标签,每个不得超过500字符,多个标签请用「英文逗号」隔开
  310 +新品,热卖,爆款
  311 +耳机,头戴式,爆款
  312 +
  313 +分割后 list形式灌入
  314 +3.2 Mysql
  315 +3.3 ES索引
  316 +3.3.1 输入数据
  317 +3.3.2 索引方法
  318 +4. 供应商
  319 +4.1 数据源
  320 +4.2 Mysql
  321 +4.3 ES索引
  322 +4.3.1 输入数据
  323 +4.3.2 索引方法
  324 +5. 款式/选项值(options)
  325 +5.1 数据源
  326 +以下区域字段,商品属性为M(商品主体)的行需填写款式名称,商品属性为P(子款式)的行需填写款式值信息,商品属性为S(单一款式商品)的行无需填写
  327 +款式1 款式2 款式3
  328 +最多255字符 最多255字符 最多255字符
  329 +SIZE COLOR
  330 +S red
  331 +...
  332 +5.2 Mysql
  333 +1. API 在 SPU 的维度直接返回3个属性定义,存储在 shoplazza_product_option 中:
  334 +1. API在 SKU的维度直接返回3个属性值,存储在 shoplazza_product_sku 表的 option 相关的字段中:
  335 +5.3 ES索引
  336 +5.3.1
  337 + 3nested,支持超过3个属性(动态)。只用作返回,不能查询。节省索引空间
  338 + "specifications": {
  339 + "type": "nested",
  340 + "properties": {
  341 + "name": { "type": "keyword","index": false },
  342 + "value": { "type": "keyword","index": false }
  343 + }
  344 + },
  345 +
  346 +6. SEO相关字段
  347 +6.1 数据源
  348 +SEO标题 SEO描述 SEO URL Handle SEO URL 重定向 SEO关键词
  349 +最多5000字符 最多5000字符 "最多支持输入255字符
  350 + (SEO URL handle只对SEO URL的「URL参数」部分进行更改,即“products/”后的内容,如:products/「URL参数」
  351 + )" "创建URL重定向,访问修改前链接可跳转到修改后的新链接页面
  352 +「Y」:TRUE
  353 +「N」:FALSE " 多个关键词请用「英文逗号」隔开
  354 +
  355 +6.2 Mysql
  356 +6.3 ES索引
  357 +6.3.1 输入数据
  358 +6.3.2 索引方法
0 359 \ No newline at end of file
... ...