店匠(Shoplazza)APP 开发完整指南
参考
https://xp0y6kmku6.feishu.cn/wiki/UsFiwDyGgiDWDwkAe5jcKHbSnHd
目录
概述
本指南将帮助您从零开始创建一个完整的店匠(Shoplazza)Public App,包括:
- 前端:使用 Liquid 模板和 Lessjs 组件库开发主题扩展
- 后端:实现 OAuth 2.0 授权流程、HMAC 验证、API 调用
- 多语言支持:支持 16 种语言的国际化
- 主题集成:与店匠主题深度集成
前置要求
- Node.js 14.14.0 或更高版本
- Shoplazza 合作伙伴账户
- Shoplazza CLI(用于部署)
- 后端服务(Java/Spring Boot 或 Node.js/Express)
- 测试店铺(用于开发测试)
阶段 1:在店匠 Partner 后台创建新 APP
步骤 1.1:登录 Partner 后台
- 访问:https://partners.shoplazza.com/
- 使用合作伙伴账号登录
- 进入管理页面:https://partners.shoplazza.com/{partner_id}
步骤 1.2:创建新 APP
- 左侧导航:Apps
- 点击:Create App 或 创建应用
- 填写信息:
- App Name:智能推荐1.0(或自定义名称)
- App Type:Public App(公共应用)
- Category:选择合适分类(如 Product Recommendations)
- 点击: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.comhmac:HMAC 签名(用于验证请求来源)install_from:安装来源,例如app_storestore_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 安全检查(必须)
在继续之前,必须执行以下安全检查:
验证 HMAC 签名
- 从查询字符串中移除
hmac参数 - 按字典顺序排序剩余参数
- 使用
CLIENT_SECRET计算 HMAC-SHA256 - 与请求中的
hmac值比较
- 从查询字符串中移除
验证 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
部署过程:
- CLI 会验证项目配置
- 上传扩展文件到店匠服务器
- 返回部署结果
步骤 5.3:在测试店铺中安装
5.3.1 访问应用市场
- 登录测试店铺后台
- 进入 应用市场 或 Apps
- 搜索您的应用名称(如"智能推荐1.0")
- 点击 安装 或 Install
5.3.2 OAuth 授权流程
- 点击安装后,会跳转到您的后端服务(APP URL)
- 后端验证 HMAC 和 shop 参数
- 重定向到店匠授权页面
- 商家点击"授权"
- 店匠回调您的后端(Redirect URL)
- 后端用 code 换取 access_token
- 显示安装成功页面
5.3.3 在主题中添加扩展
- 进入店铺后台:在线商店 > 主题 > 自定义
- 在页面中添加 App 扩展
- 选择 智能推荐 扩展
- 配置扩展设置(如商品数量)
- 保存并发布
步骤 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 计算方式错误
解决方案:
- 确认
CLIENT_SECRET与 Partner 后台一致 - 确保参数按字典顺序排序
- 使用
HMAC-SHA256算法
Q2: 多语言不生效
原因:
- 语言文件路径错误
- 翻译键名不匹配
- Liquid 模板语法错误
解决方案:
- 确认
locales/{locale}.json文件存在 - 检查翻译键名是否与模板中的
{{ 'key' | t }}一致 - 使用
{{ shop.locale }}获取当前语言
Q3: 扩展在主题编辑器中不显示
原因:
- Extension ID 配置错误
- 部署未成功
- Schema 配置错误
解决方案:
- 确认
shoplazza.extension.toml中的id正确 - 重新部署应用:
npm run deploy - 检查
{% schema %}配置是否正确
Q4: API 调用返回 401 未授权
原因:
- Access Token 过期
- Token 未正确传递
- API 版本不匹配
解决方案:
- 使用
refresh_token刷新 Access Token - 确认请求头包含
Access-Token: {access_token} - 检查 API 版本(如
/openapi/2022-01/)
最佳实践
1. 安全性
- ✅ 始终验证 HMAC:所有来自店匠的请求都必须验证 HMAC
- ✅ 验证 shop 参数:确保 shop 以
.myshoplaza.com结尾 - ✅ 使用 HTTPS:所有 API 调用必须使用 HTTPS
- ✅ 保护 Client Secret:不要将 Client Secret 提交到代码仓库
2. 性能优化
- ✅ 延迟加载:非关键资源使用
defer或async - ✅ 图片优化:使用响应式图片和 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 保存配置