店匠app安装-前端开发部署-全流程-笔记.md 32 KB

店匠(Shoplazza)APP 开发完整指南

参考

https://xp0y6kmku6.feishu.cn/wiki/UsFiwDyGgiDWDwkAe5jcKHbSnHd

目录

  1. 概述
  2. 阶段 1:在店匠 Partner 后台创建新 APP
  3. 阶段 2:创建前端项目
  4. 阶段 3:开发后端服务
  5. 阶段 4:前端开发详解
  6. 阶段 5:测试与部署
  7. 常见问题与最佳实践

概述

本指南将帮助您从零开始创建一个完整的店匠(Shoplazza)Public App,包括:

  • 前端:使用 Liquid 模板和 Lessjs 组件库开发主题扩展
  • 后端:实现 OAuth 2.0 授权流程、HMAC 验证、API 调用
  • 多语言支持:支持 16 种语言的国际化
  • 主题集成:与店匠主题深度集成

前置要求


阶段 1:在店匠 Partner 后台创建新 APP

步骤 1.1:登录 Partner 后台

  1. 访问:https://partners.shoplazza.com/
  2. 使用合作伙伴账号登录
  3. 进入管理页面:https://partners.shoplazza.com/{partner_id}

步骤 1.2:创建新 APP

  1. 左侧导航:Apps
  2. 点击:Create App创建应用
  3. 填写信息:
    • App Name:智能推荐1.0(或自定义名称)
    • App TypePublic App(公共应用)
    • Category:选择合适分类(如 Product Recommendations)
  4. 点击:Create创建

步骤 1.3:记录关键信息

系统会生成 Client credentials(APP 的公共标识符,OAuth 流程以及与 Shoplazza API 互动都会需要):

  • Client ID:例如 x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY
  • Client Secret:例如 LF71s5gqtPws2i08C9y43uIYXpF_c91ZnqRmS1z8Ekk

⚠️ 重要:请妥善保存这些凭证,后续无法再次查看 Client Secret。

步骤 1.4:配置 APP 信息

在 APP 详情页面配置以下信息:

1.4.1 App URL(安装入口)

当商家从应用市场点击"安装"时,店匠会跳转到此 URL:

https://saas-ai-api.essa.top/recommend/oauth/install

请求参数

  • shop:店铺域名,例如 47167113-1.myshoplaza.com
  • hmac:HMAC 签名(用于验证请求来源)
  • install_from:安装来源,例如 app_store
  • store_id:店铺 ID

完整示例

https://saas-ai-api.essa.top/recommend/oauth/install?
  hmac=c4caf9b08bdeff7531bb12712ffea860264ec24f5fd953832505c5024d19edca&
  install_from=app_store&
  shop=rwerwre.myshoplaza.com&
  store_id=1339409

1.4.2 Redirect URL(OAuth 回调地址)

商家授权后,店匠会重定向到此 URL:

https://saas-ai-api.essa.top/recommend/oauth/callback

请求参数

  • code:授权码(只能使用一次)
  • hmac:HMAC 签名
  • shop:店铺域名
  • state:防 CSRF 攻击的随机值(可选)

1.4.3 Scopes(权限范围)

根据应用功能需求选择权限:

Scope 说明 是否必需
read_shop 读取店铺信息 ✅ 推荐
read_product 读取商品信息 ✅ 推荐
read_customer 读取客户信息 可选
read_order 读取订单信息 可选
read_app_proxy 读取应用代理 可选
write_cart_transform 修改购物车 可选

推荐配置(智能推荐 APP):

read_shop read_product read_customer

步骤 1.5:记录 Extension ID

创建主题扩展后,系统会生成 Extension ID,例如:580182366958388163

此 ID 需要在 shoplazza.extension.toml 中配置。


阶段 2:创建前端项目

步骤 2.1:创建项目目录

# 进入父目录
cd /home/tw/saas

# 创建新项目目录
mkdir shoplazza-recommend-app
cd shoplazza-recommend-app

步骤 2.2:初始化项目

方式 1:使用 Shoplazza CLI(推荐)

# 安装 Shoplazza CLI(如果未安装)
npm i -g shoplazza-cli

# 生成项目结构
shoplazza app generate

方式 2:手动创建

按照以下结构手动创建文件:

shoplazza-recommend-app/
├── package.json
├── shoplazza.app.toml
├── README.md
├── .gitignore
└── extensions/
    └── recommend/
        ├── shoplazza.extension.toml
        ├── package.json
        ├── blocks/
        │   └── recommend.liquid
        ├── assets/
        │   ├── recommend.css
        │   └── assets-manifest.json
        ├── snippets/
        │   └── recommend.liquid
        └── locales/
            ├── zh-CN.json
            └── en-US.json

步骤 2.3:创建根目录配置文件

2.3.1 package.json

{
  "name": "智能推荐1.0",
  "version": "1.0.0",
  "scripts": {
    "shoplazza": "shoplazza",
    "info": "shoplazza app info",
    "generate": "shoplazza app generate",
    "deploy": "shoplazza app deploy"
  },
  "author": "",
  "license": "ISC",
  "private": true,
  "workspaces": [
    "extensions/*"
  ]
}

2.3.2 shoplazza.app.toml

使用步骤 1.3 中获取的 Client ID:

# 使用步骤 1.3 中获取的 Client ID
client_id = "x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY"

# 权限范围(与 Partner 后台配置保持一致)
scopes = "read_shop read_product read_customer"

2.3.3 .gitignore

# Dependencies
node_modules/
package-lock.json
yarn.lock

# Build outputs
app-deploy/
dist/
build/

# Environment
.env
.env.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

2.3.4 README.md

# 智能推荐 1.0

店匠(Shoplazza)智能推荐 APP 前端项目。

## 快速开始

### 安装依赖

```bash
npm install

部署应用

npm run deploy

查看应用信息

npm run info

项目结构

  • extensions/recommend/:推荐扩展
    • blocks/recommend.liquid:主组件
    • assets/recommend.css:样式文件
    • locales/:多语言文件 ```

步骤 2.4:创建 Extension 配置文件

2.4.1 extensions/recommend/shoplazza.extension.toml

# Extension ID(从 Partner 后台获取,或部署后系统生成)
id = "580182366958388163"

# Extension 名称(与目录名保持一致)
name = "recommend"

# Extension 类型(theme 表示主题扩展)
type = "theme"

2.4.2 extensions/recommend/package.json

{
  "name": "recommend",
  "version": "1.0.0",
  "private": true
}

阶段 3:开发后端服务

步骤 3.1:OAuth 2.0 授权流程

店匠使用标准的 OAuth 2.0 授权码(Authorization Code)流程:

1. 商家在应用市场点击"安装"
   ↓
2. 店匠跳转到 APP URL(步骤 1.4.1)
   ↓
3. 后端验证 HMAC 和 shop 参数
   ↓
4. 后端重定向到店匠授权页面
   ↓
5. 商家在授权页面点击"授权"
   ↓
6. 店匠回调 Redirect URL(步骤 1.4.2),携带 code
   ↓
7. 后端验证 HMAC,用 code 换取 access_token
   ↓
8. 保存 token 到数据库,完成安装

步骤 3.2:实现 APP URL 处理(步骤 3)

3.2.1 请求示例

GET https://saas-ai-api.essa.top/recommend/oauth/install?
  hmac=c4caf9b08bdeff7531bb12712ffea860264ec24f5fd953832505c5024d19edca&
  install_from=app_store&
  shop=rwerwre.myshoplaza.com&
  store_id=1339409

3.2.2 安全检查(必须)

在继续之前,必须执行以下安全检查:

  1. 验证 HMAC 签名

    • 从查询字符串中移除 hmac 参数
    • 按字典顺序排序剩余参数
    • 使用 CLIENT_SECRET 计算 HMAC-SHA256
    • 与请求中的 hmac 值比较
  2. 验证 shop 参数

    • 必须是有效的店铺主机名
    • 必须以 .myshoplaza.com 结尾

3.2.3 Java 实现示例

@RestController
@RequestMapping("/recommend/oauth")
public class RecommendOAuthController {

    @Autowired
    private ShoplazzaOAuthService oAuthService;

    /**
     * 处理 APP 安装请求(步骤 3)
     */
    @GetMapping("/install")
    public ResponseEntity<Void> handleInstall(
            @RequestParam("shop") String shop,
            @RequestParam("hmac") String hmac,
            @RequestParam(value = "install_from", required = false) String installFrom,
            @RequestParam(value = "store_id", required = false) String storeId,
            HttpServletRequest request) {

        // 1. 验证 HMAC
        Map<String, String> params = new HashMap<>();
        params.put("shop", shop);
        params.put("hmac", hmac);
        if (installFrom != null) params.put("install_from", installFrom);
        if (storeId != null) params.put("store_id", storeId);

        if (!oAuthService.verifyHmac(params, hmac)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }

        // 2. 验证 shop 参数
        if (!shop.endsWith(".myshoplaza.com")) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }

        // 3. 生成授权 URL 并重定向(步骤 4)
        String authUrl = oAuthService.buildAuthorizationRedirect(shop);
        return ResponseEntity.status(HttpStatus.FOUND)
                .header(HttpHeaders.LOCATION, authUrl)
                .build();
    }
}

3.2.4 Node.js 实现示例

const express = require("express");
const crypto = require("crypto");
const app = express();

const CLIENT_ID = "x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY";
const CLIENT_SECRET = "LF71s5gqtPws2i08C9y43uIYXpF_c91ZnqRmS1z8Ekk";
const REDIRECT_URI = "https://saas-ai-api.essa.top/recommend/oauth/callback";

// HMAC 验证函数
function verifyHmac(queryParams, receivedHmac) {
    const map = { ...queryParams };
    delete map["hmac"];

    const sortedKeys = Object.keys(map).sort();
    const message = sortedKeys.map(key => `${key}=${map[key]}`).join('&');

    const calculatedHmac = crypto
        .createHmac("sha256", CLIENT_SECRET)
        .update(message)
        .digest("hex");

    return crypto.timingSafeEqual(
        Buffer.from(calculatedHmac),
        Buffer.from(receivedHmac)
    );
}

// 处理安装请求
app.get("/recommend/oauth/install", (req, res) => {
    const { shop, hmac, install_from, store_id } = req.query;

    // 1. 验证 HMAC
    if (!verifyHmac(req.query, hmac)) {
        return res.status(400).send("HMAC validation failed");
    }

    // 2. 验证 shop 参数
    if (!shop || !shop.endsWith(".myshoplaza.com")) {
        return res.status(400).send("Invalid shop parameter");
    }

    // 3. 生成 state(防 CSRF)
    const state = crypto.randomBytes(16).toString("hex");

    // 4. 构建授权 URL
    const scopes = "read_shop read_product read_customer";
    const authUrl = `https://${shop}/admin/oauth/authorize?` +
        `client_id=${CLIENT_ID}&` +
        `scope=${encodeURIComponent(scopes)}&` +
        `redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
        `response_type=code&` +
        `state=${state}`;

    // 5. 重定向到授权页面
    res.redirect(authUrl);
});

步骤 3.3:实现 OAuth 回调处理(步骤 7-9)

3.3.1 请求示例

GET https://saas-ai-api.essa.top/recommend/oauth/callback?
  code=wBe-NWHzW21e94YqD4bRKBsJsE2GcZlDzP4oW9w2ddk&
  hmac=4c396fac1912057b65228f5bbd4a65255961d85a60fba1f1105ddbf27f17b58f&
  shop=rwerwre.myshoplaza.com&
  state=abc123

3.3.2 Java 实现示例

/**
 * 处理 OAuth 回调(步骤 7-9)
 */
@GetMapping("/callback")
public ResponseEntity<String> handleCallback(
        @RequestParam("code") String code,
        @RequestParam("hmac") String hmac,
        @RequestParam("shop") String shop,
        @RequestParam(value = "state", required = false) String state) {

    // 1. 验证 HMAC
    Map<String, String> params = new HashMap<>();
    params.put("code", code);
    params.put("hmac", hmac);
    params.put("shop", shop);
    if (state != null) params.put("state", state);

    if (!oAuthService.verifyHmac(params, hmac)) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body("HMAC validation failed");
    }

    // 2. 用 code 换取 access_token
    ShoplazzaTokenResponse tokenResponse = oAuthService.exchangeForToken(shop, code);

    // 3. 保存 token 到数据库
    ShopConfig shopConfig = shopConfigService.saveOrUpdate(shop, tokenResponse);

    // 4. 返回安装成功页面
    String successPage = buildSuccessPage(shopConfig);
    return ResponseEntity.ok()
            .contentType(MediaType.TEXT_HTML)
            .body(successPage);
}

/**
 * 用 code 换取 access_token
 */
public ShoplazzaTokenResponse exchangeForToken(String shop, String code) {
    String tokenUrl = "https://" + shop + "/admin/oauth/token";

    Map<String, String> requestBody = new HashMap<>();
    requestBody.put("client_id", CLIENT_ID);
    requestBody.put("client_secret", CLIENT_SECRET);
    requestBody.put("code", code);
    requestBody.put("grant_type", "authorization_code");
    requestBody.put("redirect_uri", REDIRECT_URI);

    // 发送 POST 请求
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);

    HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody, headers);
    ResponseEntity<ShoplazzaTokenResponse> response = restTemplate.postForEntity(
            tokenUrl, entity, ShoplazzaTokenResponse.class);

    return response.getBody();
}

3.3.3 Token 响应格式

{
  "token_type": "Bearer",
  "expires_at": 1550546245,
  "access_token": "eyJ0eXAiOiJKV1QiLCJh...",
  "refresh_token": "def502003d28ba08a964e...",
  "store_id": "1339409",
  "store_name": "rwerwre"
}

步骤 3.4:实现 API 调用

获取 access_token 后,可以调用店匠 API:

/**
 * 调用店匠 API 示例:获取商品列表
 */
public List<Product> getProducts(String shop, String accessToken) {
    String apiUrl = "https://" + shop + "/openapi/2022-01/products";

    HttpHeaders headers = new HttpHeaders();
    headers.set("Access-Token", accessToken);
    headers.set("Content-Type", "application/json");

    HttpEntity<?> entity = new HttpEntity<>(headers);

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<ProductListResponse> response = restTemplate.exchange(
            apiUrl,
            HttpMethod.GET,
            entity,
            ProductListResponse.class
    );

    return response.getBody().getProducts();
}

阶段 4:前端开发详解

步骤 4.1:创建 Liquid 模板

4.1.1 extensions/recommend/blocks/recommend.liquid

{% comment %}
  推荐商品组件
  使用 {% use %} 引入 CSS 文件
{% endcomment %}
{% use "recommend.css" %}

<div id="recommend" class="recommend-container">
  {% comment %} 版本标识(开发测试用){% endcomment %}
  <div class="recommend-badge">智能推荐 APP</div>

  {% comment %} 标题(使用多语言){% endcomment %}
  <h2>{{ 'recommend.title' | t }}</h2>

  {% comment %} 商品列表容器 {% endcomment %}
  <div class="recommend-products" id="recommend-products">
    <div class="loading">{{ 'recommend.loading' | t }}</div>
  </div>
</div>

<script>
(function() {
  {% comment %} 配置信息(店匠会自动注入 shop 对象){% endcomment %}
  window.RECOMMEND_CONFIG = {
    storeId: "{{ shop.domain }}",
    apiEndpoint: "https://saas-ai-api.essa.top/app-api/app/shoplazza/recommend/products",
    locale: "{{ shop.locale }}"
  };

  {% comment %} 获取店铺语言 {% endcomment %}
  const shopLocale = "{{ shop.locale }}" || "zh-CN";

  {% comment %} 翻译文本(通过 Liquid 的 t 过滤器获取){% endcomment %}
  const translations = {
    title: "{{ 'recommend.title' | t }}",
    loading: "{{ 'recommend.loading' | t }}",
    noResults: "{{ 'recommend.no_results' | t }}",
    error: "{{ 'recommend.error' | t }}",
    addToCart: "{{ 'recommend.add_to_cart' | t }}",
    viewDetails: "{{ 'recommend.view_details' | t }}"
  };

  {% comment %} 加载推荐商品 {% endcomment %}
  async function loadRecommendations() {
    const container = document.getElementById('recommend-products');

    try {
      const response = await fetch(
        `${window.RECOMMEND_CONFIG.apiEndpoint}?storeId=${window.RECOMMEND_CONFIG.storeId}`,
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json'
          }
        }
      );

      if (!response.ok) {
        throw new Error('加载失败: ' + response.status);
      }

      const data = await response.json();

      if (data.products && data.products.length > 0) {
        renderProducts(data.products);
      } else {
        container.innerHTML = `<div class="no-results">${translations.noResults}</div>`;
      }
    } catch (error) {
      console.error('加载推荐商品失败:', error);
      container.innerHTML = `<div class="error">${translations.error}</div>`;
    }
  }

  {% comment %} 渲染商品列表 {% endcomment %}
  function renderProducts(products) {
    const container = document.getElementById('recommend-products');

    const html = products.map(product => {
      const imageUrl = product.image_url || product.image || '/assets/no-image.png';
      const productUrl = product.url || product.handle || '#';
      const price = formatPrice(product.price || product.price_formatted || '0.00');

      return `
        <div class="product-item" data-product-id="${product.id}">
          <a href="${productUrl}" class="product-link">
            <div class="product-image-wrapper">
              <img 
                src="${imageUrl}" 
                alt="${escapeHtml(product.title || product.name || '未知商品')}"
                class="product-image"
                onerror="this.src='/assets/no-image.png'"
              />
            </div>
            <div class="product-info">
              <h3 class="product-title">${escapeHtml(product.title || product.name || '未知商品')}</h3>
              <div class="product-price">${price}</div>
              ${product.vendor ? `<div class="product-vendor">${escapeHtml(product.vendor)}</div>` : ''}
            </div>
          </a>
          <button class="add-to-cart-btn" onclick="addToCart('${product.id}')">
            ${translations.addToCart}
          </button>
        </div>
      `;
    }).join('');

    container.innerHTML = html;
  }

  {% comment %} 工具函数:转义 HTML {% endcomment %}
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  {% comment %} 工具函数:格式化价格 {% endcomment %}
  function formatPrice(price) {
    if (typeof price === 'number') {
      return '¥' + price.toFixed(2);
    }
    if (typeof price === 'string') {
      const num = parseFloat(price);
      if (!isNaN(num)) {
        return '¥' + num.toFixed(2);
      }
    }
    return price || '¥0.00';
  }

  {% comment %} 添加到购物车(示例){% endcomment %}
  window.addToCart = function(productId) {
    console.log('添加到购物车:', productId);
    // 实现添加到购物车逻辑
  };

  {% comment %} 初始化 {% endcomment %}
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', loadRecommendations);
  } else {
    loadRecommendations();
  }
})();
</script>

{% comment %} Schema 配置(用于主题编辑器){% endcomment %}
{% schema %} 
{
  "name": {
    "en-US": "recommend(dev)",
    "zh-CN": "智能推荐(开发)"
  },
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": {
        "en-US": "Title",
        "zh-CN": "标题"
      },
      "default": {
        "en-US": "Recommended Products",
        "zh-CN": "推荐商品"
      }
    },
    {
      "type": "range",
      "id": "product_count",
      "label": {
        "en-US": "Number of products",
        "zh-CN": "商品数量"
      },
      "min": 4,
      "max": 20,
      "step": 1,
      "default": 8
    }
  ]
}
{% endschema %}

步骤 4.2:创建样式文件

4.2.1 extensions/recommend/assets/recommend.css

/* 推荐商品容器 */
.recommend-container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: var(--font-body-family, sans-serif);
  color: var(--color-text, #333);
  background: var(--color-background, #fff);
}

/* 版本标识(开发测试用)*/
.recommend-badge {
  position: fixed;
  top: 10px;
  right: 10px;
  background: #ff6b6b;
  color: white;
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
  z-index: 9999;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

/* 标题 */
.recommend-container h2 {
  font-size: 24px;
  font-weight: 600;
  margin-bottom: 20px;
  text-align: center;
}

/* 商品网格 */
.recommend-products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

/* 商品项 */
.product-item {
  border: 1px solid var(--color-border, #e0e0e0);
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
  background: var(--color-background, #fff);
}

.product-item:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

/* 商品链接 */
.product-link {
  text-decoration: none;
  color: inherit;
  display: block;
}

/* 商品图片 */
.product-image-wrapper {
  width: 100%;
  padding-top: 100%; /* 1:1 比例 */
  position: relative;
  overflow: hidden;
  background: #f5f5f5;
}

.product-image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 商品信息 */
.product-info {
  padding: 12px;
}

.product-title {
  font-size: 14px;
  font-weight: 500;
  margin: 0 0 8px 0;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.product-price {
  font-size: 16px;
  font-weight: 600;
  color: var(--color-price, #e74c3c);
  margin-bottom: 4px;
}

.product-vendor {
  font-size: 12px;
  color: var(--color-text-secondary, #999);
}

/* 添加到购物车按钮 */
.add-to-cart-btn {
  width: 100%;
  padding: 10px;
  background: var(--color-button, #007bff);
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;
}

.add-to-cart-btn:hover {
  background: var(--color-button-hover, #0056b3);
}

/* 加载状态 */
.loading {
  text-align: center;
  padding: 40px;
  color: var(--color-text-secondary, #999);
}

/* 无结果 */
.no-results {
  text-align: center;
  padding: 40px;
  color: var(--color-text-secondary, #999);
}

/* 错误状态 */
.error {
  text-align: center;
  padding: 40px;
  color: var(--color-error, #e74c3c);
}

/* 响应式设计 */
@media (max-width: 749px) {
  .recommend-container {
    padding: 10px;
  }

  .recommend-products {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
  }

  .recommend-container h2 {
    font-size: 20px;
  }
}

@media (min-width: 750px) and (max-width: 989px) {
  .recommend-products {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 990px) {
  .recommend-products {
    grid-template-columns: repeat(4, 1fr);
  }
}

步骤 4.3:创建多语言文件

4.3.1 extensions/recommend/locales/zh-CN.json

{
  "recommend": {
    "title": "为您推荐",
    "loading": "加载中,请稍候...",
    "no_results": "暂无推荐商品",
    "error": "加载失败,请重试",
    "add_to_cart": "加入购物车",
    "view_details": "查看详情"
  }
}

4.3.2 extensions/recommend/locales/en-US.json

{
  "recommend": {
    "title": "Recommended for You",
    "loading": "Loading, please wait...",
    "no_results": "No recommended products",
    "error": "Failed to load, please try again",
    "add_to_cart": "Add to Cart",
    "view_details": "View Details"
  }
}

4.3.3 支持的语言列表

店匠支持 16 种语言,建议至少提供以下语言文件:

语言代码 语言名称 文件路径
zh-CN 中文(简体) locales/zh-CN.json
zh-TW 中文(繁体) locales/zh-TW.json
en-US 英语 locales/en-US.json
ja-JP 日语 locales/ja-JP.json
ko-KR 韩语 locales/ko-KR.json

完整语言列表:参考 多语言支持文档

步骤 4.4:Lessjs 集成(可选)

Lessjs 是店匠官方提供的前端组件库,基于 Web Components。如需使用,参考:

注意:对于简单的推荐组件,可以不使用 Lessjs,直接使用原生 JavaScript 和 CSS。


阶段 5:测试与部署

步骤 5.1:本地测试

5.1.1 检查项目结构

cd /home/tw/saas/shoplazza-recommend-app

# 检查文件结构
tree -L 3

# 应该看到:
# shoplazza-recommend-app/
# ├── package.json
# ├── shoplazza.app.toml
# └── extensions/
#     └── recommend/
#         ├── shoplazza.extension.toml
#         ├── blocks/
#         ├── assets/
#         └── locales/

5.1.2 验证配置

# 查看应用信息
npm run info

# 应该显示:
# - App Name: 智能推荐1.0
# - Client ID: x3apOQkj98Aij61BKi1TJ9q6wyfY3HrbmUZJ5FPfSbY
# - Extension ID: 580182366958388163

步骤 5.2:部署到店匠

5.2.1 登录 Shoplazza CLI

# 登录(如果未登录)
shoplazza login

# 输入 Partner 账号和密码

5.2.2 部署应用

# 部署应用
npm run deploy

# 或者
shoplazza app deploy

部署过程

  1. CLI 会验证项目配置
  2. 上传扩展文件到店匠服务器
  3. 返回部署结果

步骤 5.3:在测试店铺中安装

5.3.1 访问应用市场

  1. 登录测试店铺后台
  2. 进入 应用市场Apps
  3. 搜索您的应用名称(如"智能推荐1.0")
  4. 点击 安装Install

5.3.2 OAuth 授权流程

  1. 点击安装后,会跳转到您的后端服务(APP URL)
  2. 后端验证 HMAC 和 shop 参数
  3. 重定向到店匠授权页面
  4. 商家点击"授权"
  5. 店匠回调您的后端(Redirect URL)
  6. 后端用 code 换取 access_token
  7. 显示安装成功页面

5.3.3 在主题中添加扩展

  1. 进入店铺后台:在线商店 > 主题 > 自定义
  2. 在页面中添加 App 扩展
  3. 选择 智能推荐 扩展
  4. 配置扩展设置(如商品数量)
  5. 保存并发布

步骤 5.4:验证功能

5.4.1 前端验证

  • [ ] 扩展正确显示在主题编辑器中
  • [ ] 商品列表正确加载
  • [ ] 多语言切换正常
  • [ ] 响应式布局在不同设备上正常
  • [ ] 图片加载正常(包括错误处理)

5.4.2 后端验证

  • [ ] OAuth 授权流程正常
  • [ ] HMAC 验证正常
  • [ ] Token 正确保存到数据库
  • [ ] API 调用正常(如获取商品列表)

5.4.3 浏览器控制台检查

打开浏览器开发者工具(F12),检查:

  • Console:无 JavaScript 错误
  • Network:API 请求正常(状态码 200)
  • Elements:HTML 结构正确

常见问题与最佳实践

常见问题

Q1: HMAC 验证失败

原因

  • Client Secret 配置错误
  • 参数排序不正确
  • HMAC 计算方式错误

解决方案

  1. 确认 CLIENT_SECRET 与 Partner 后台一致
  2. 确保参数按字典顺序排序
  3. 使用 HMAC-SHA256 算法

Q2: 多语言不生效

原因

  • 语言文件路径错误
  • 翻译键名不匹配
  • Liquid 模板语法错误

解决方案

  1. 确认 locales/{locale}.json 文件存在
  2. 检查翻译键名是否与模板中的 {{ 'key' | t }} 一致
  3. 使用 {{ shop.locale }} 获取当前语言

Q3: 扩展在主题编辑器中不显示

原因

  • Extension ID 配置错误
  • 部署未成功
  • Schema 配置错误

解决方案

  1. 确认 shoplazza.extension.toml 中的 id 正确
  2. 重新部署应用:npm run deploy
  3. 检查 {% schema %} 配置是否正确

Q4: API 调用返回 401 未授权

原因

  • Access Token 过期
  • Token 未正确传递
  • API 版本不匹配

解决方案

  1. 使用 refresh_token 刷新 Access Token
  2. 确认请求头包含 Access-Token: {access_token}
  3. 检查 API 版本(如 /openapi/2022-01/

最佳实践

1. 安全性

  • 始终验证 HMAC:所有来自店匠的请求都必须验证 HMAC
  • 验证 shop 参数:确保 shop 以 .myshoplaza.com 结尾
  • 使用 HTTPS:所有 API 调用必须使用 HTTPS
  • 保护 Client Secret:不要将 Client Secret 提交到代码仓库

2. 性能优化

  • 延迟加载:非关键资源使用 deferasync
  • 图片优化:使用响应式图片和 CDN
  • 缓存策略:合理使用浏览器缓存和 CDN 缓存
  • 代码分割:将大型 JavaScript 拆分为多个模块

3. 用户体验

  • 加载状态:显示加载中、错误、无结果等状态
  • 错误处理:友好的错误提示和重试机制
  • 响应式设计:适配移动端、平板、桌面端
  • 多语言支持:至少支持中文和英语

4. 代码质量

  • 代码注释:关键逻辑添加注释
  • 错误日志:记录错误日志便于调试
  • 代码规范:遵循项目代码规范
  • 版本控制:使用 Git 进行版本管理

参考资源

官方文档

社区支持


附录

A. 完整项目结构

shoplazza-recommend-app/
├── package.json                    # 项目配置
├── shoplazza.app.toml              # APP 配置(Client ID)
├── README.md                       # 项目说明
├── .gitignore                      # Git 忽略文件
├── SHOPLAZZA_APP_DEVELOPMENT_GUIDE.md  # 本指南
└── extensions/
    └── recommend/                  # 推荐扩展
        ├── shoplazza.extension.toml # Extension 配置
        ├── package.json            # Extension 配置
        ├── blocks/                 # 组件块
        │   └── recommend.liquid    # 主组件
        ├── assets/                 # 静态资源
        │   ├── recommend.css      # 样式文件
        │   └── assets-manifest.json # 资源清单
        ├── snippets/               # 代码片段
        │   └── recommend.liquid    # 可复用片段
        └── locales/               # 多语言文件
            ├── zh-CN.json         # 简体中文
            ├── zh-TW.json         # 繁体中文
            ├── en-US.json         # 英语
            ├── ja-JP.json         # 日语
            └── ko-KR.json         # 韩语

B. 配置文件模板

B.1 shoplazza.app.toml

client_id = "YOUR_CLIENT_ID"
scopes = "read_shop read_product read_customer"

B.2 shoplazza.extension.toml

id = "YOUR_EXTENSION_ID"
name = "recommend"
type = "theme"

C. OAuth 流程时序图

商家 → 店匠平台 → 后端服务 → 店匠授权页 → 商家授权 → 后端服务 → 数据库
  ↓        ↓          ↓            ↓           ↓          ↓         ↓
安装   跳转APP URL  验证HMAC    重定向授权    点击授权   换取Token  保存配置