跳到主要内容

开发应用

本教程是「用 Shoplazza CLI 构建应用」系列的第一步:先用 shoplazza app 命令脚手架一个 Node.js + Express.js 应用并跑通 OAuth,再继续为同一工程添加 主题扩展结账扩展

在本教程中,您将构建一个应用程序,通过调用 Shoplazza OpenAPI 接口,随机生成一个商品内容。

你将学到什么

在本教程中,你将学习如何执行以下任务:

  • 如何快速开发一个示例 Shoplazza App

  • 如何实现 OAuth 授权流程获取店铺访问权限

  • 如何通过 HMAC 验证确保请求安全性

  • 如何调用 Shoplazza OpenAPI 创建商品

前置需求

  • 已在 Shoplazza Partners 创建了应用(查看如何创建应用

  • 已在 Partners 创建了测试店铺

  • Node.js 版本 ≥ 18(Node 14 已于 2023-04 EOL,建议使用 Volta / nvm 锁定版本)

  • 基础的 JavaScript 和 Express 框架知识

第一步:搭建项目基础

1.1 创建项目文件夹

mkdir shoplazza-app-demo
cd shoplazza-app-demo

1.2 初始化 npm 项目

npm init -y

这将创建一个 package.json 文件,包含项目的基本信息。

1.3 安装依赖

npm install express axios dotenv sqlite3

依赖说明:

  • express: Web 应用框架,用于构建 HTTP 服务

  • axios: HTTP 客户端,用于调用 Shoplazza OpenAPI

  • dotenv: 环境变量管理工具

  • sqlite3: 轻量级数据库,用于存储访问令牌

crypto 是 Node.js 内置模块(用于 HMAC 验证),无需也不要通过 npm 安装。npm 上同名的 crypto 包是废弃/无关包,存在 typosquatting 供应链风险。

1.4 创建基础 Express 服务

在项目根目录创建 index.js 文件:

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

app.get("/", (req, res) => {
res.send("Shoplazza App is running!");
});

app.listen(3000, () => {
console.log("Server is listening on port 3000");
});

1.5 启动服务

node index.js

访问 http://localhost:3000,你应该看到 "Shoplazza App is running!" 的提示。

第二步:配置 ngrok 实现内网穿透

由于 Shoplazza 需要通过公网 URL 访问你的应用,我们需要使用 ngrok 将本地服务暴露到公网。

2.1 下载并安装 ngrok

访问 ngrok 官网 下载对应系统的版本。

macOS 用户可以使用 Homebrew 安装:

brew install ngrok/ngrok/ngrok

或者直接下载二进制文件:

  1. 访问 https://ngrok.com/download

  2. 下载对应系统的版本

  3. 解压并将 ngrok 移动到系统 PATH 中

2.2 注册并获取 authtoken

  1. 访问 https://dashboard.ngrok.com/signup 注册账号

  2. 登录后访问 https://dashboard.ngrok.com/get-started/your-authtoken

  3. 复制你的 authtoken

  4. 在终端执行:

ngrok config add-authtoken YOUR_AUTHTOKEN

2.3 启动 ngrok

确保你的 Express 服务正在运行(端口 3000),然后在新的终端窗口中执行:

ngrok http 3000

你将看到类似以下的输出:

Session Status online
Version 3.x.x
Region United States (us)
Forwarding https://xxx.ngrok-free.dev -> http://localhost:3000

访问 https://xxx.ngrok-free.dev,你应该看到 "Shoplazza App is running!" 的提示。

第三步:配置环境变量

3.1 创建 .env 文件

项目通常会提供 .env.example 模板,直接 cp .env.example .env 后填入实际值即可。

在项目根目录创建 .env 文件:

CLIENT_ID=your_client_id_here
CLIENT_SECRET=your_client_secret_here
BASE_URL=https://xxx.ngrok-free.dev
SCOPES=write_product

SCOPES 是应用申请的权限范围,多个权限用空格分隔(例如 write_product read_order)。完整权限列表见 Shoplazza API 权限文档

3.2 获取 CLIENT_ID 和 CLIENT_SECRET

  1. 登录 Shoplazza Partners

  2. 进入你创建的应用详情页

  3. 在"应用凭证"或"App credentials"部分找到:Client ID(客户端 ID),Client Secret(客户端密钥)

  4. 将这些值复制到 .env 文件中

3.3 配置 BASE_URL

将第二步中 ngrok 生成的 HTTPS 地址填入 BASE_URL

BASE_URL=https://xxx.ngrok-free.dev

3.4 配置应用回调地址

在 Shoplazza Partners 应用设置中,设置下面两个地址:

  • App URL 设置为: https://xxx.ngrok-free.dev/auth

  • Redirect URL 设置为: https://xxx.ngrok-free.dev/auth/callback

3.5 创建 .gitignore 文件

为了保护敏感信息,创建 .gitignore 文件:

# Dependencies
node_modules/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# OS files
.DS_Store
Thumbs.db

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

# Build outputs
dist/
build/

.env
.env.*
*.db

安全提示:

  • 永远不要将 .env 文件提交到代码仓库

  • 建议在生产环境使用环境变量管理服务(如 AWS Secrets Manager、Azure Key Vault 等)

第四步:实现 OAuth 授权流程

本步骤实现 OAuth 2.0 授权码流程。完整的流程原理、HMAC 算法、安全防护与最佳实践,见 App 授权参考

4.1 创建工具函数和 HMAC 验证

在实现具体的授权路由之前,我们需要准备一些工具函数。

这些函数包括:环境变量检查中间件、HMAC 验证中间件等。HMAC 验证是 OAuth 流程中的关键安全措施,用于确保请求确实来自 Shoplazza 平台,而不是被恶意第三方伪造或篡改。

创建 utils/index.js 文件:

require("dotenv").config();
const crypto = require("crypto");

const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const BASE_URL = process.env.BASE_URL;
const SCOPES = process.env.SCOPES;
const REDIRECT_URI = `${BASE_URL}/auth/callback`;

// 校验 shop 域名(防止 SSRF):允许 *.myshoplaza.com / *.dev.myshoplaza.com / *.stg.myshoplaza.com
const SHOP_DOMAIN_REGEXP = /^[a-z0-9][a-z0-9-]*(\.dev|\.stg)?\.myshoplaza\.com$/i;
function isValidShop(shop) {
return typeof shop === "string" && SHOP_DOMAIN_REGEXP.test(shop);
}

// 检查必需的环境变量
function checkEnvMiddleware(req, res, next) {
if (!CLIENT_ID || !CLIENT_SECRET || !BASE_URL) {
const missingVars = [];
if (!CLIENT_ID) missingVars.push("CLIENT_ID");
if (!CLIENT_SECRET) missingVars.push("CLIENT_SECRET");
if (!BASE_URL) missingVars.push("BASE_URL");

return res.status(500).send(`环境变量配置缺失: ${missingVars.join(", ")}`);
}
next();
}

// 安全比对函数(防止时序攻击)
function secureCompare(a, b) {
try {
const A = Buffer.from(a, "hex");
const B = Buffer.from(b, "hex");
if (A.length !== B.length) return false;
return crypto.timingSafeEqual(A, B);
} catch (e) {
return false;
}
}

// HMAC 验证中间件
function hmacValidator(req, res, next) {
const { hmac } = req.query;
const map = Object.assign({}, req.query);
delete map["hmac"]; // 移除 hmac 参数

// 按字母顺序排序参数
const sortedKeys = Object.keys(map).sort();
const message = sortedKeys.map((key) => `${key}=${map[key]}`).join("&");

// 使用 CLIENT_SECRET 生成 HMAC
const generated_hash = crypto
.createHmac("sha256", CLIENT_SECRET)
.update(message)
.digest("hex");

if (!secureCompare(generated_hash, hmac)) {
return res.status(400).send("HMAC validation failed");
}
next();
}

// 简易的 state 临时存储(演示用,生产建议改用 Redis / session)
const stateStore = new Map();
const STATE_TTL_MS = 10 * 60 * 1000; // 10 分钟
function saveState(state, shop) {
stateStore.set(state, { shop, expireAt: Date.now() + STATE_TTL_MS });
}
function consumeState(state, shop) {
const entry = stateStore.get(state);
if (!entry) return false;
stateStore.delete(state); // 一次性使用
if (entry.expireAt < Date.now()) return false;
return entry.shop === shop;
}

module.exports = {
checkEnvMiddleware,
hmacValidator,
isValidShop,
saveState,
consumeState,
CLIENT_ID,
CLIENT_SECRET,
BASE_URL,
SCOPES,
REDIRECT_URI,
};

代码说明:

  • checkEnvMiddleware: 中间件函数,确保所有必需的环境变量都已配置

  • secureCompare: 使用时间安全的比对方式,防止时序攻击

  • hmacValidator: HMAC 验证中间件,确保请求来自 Shoplazza

  • isValidShop: 校验 shop 参数是合法的 *.myshoplaza.com 域名,防止 SSRF 攻击——攻击者可能传入 evil.com 让服务端把 token 换取请求发到第三方域。

  • saveState / consumeState: OAuth state 临时存储,发起授权时写入、回调时一次性消费并校验,防止 CSRF 攻击

  • REDIRECT_URI: 自动拼接授权回调地址

HMAC 验证算法与"为什么需要 HMAC"见 App 授权参考

4.2 实现授权入口路由

现在我们来实现授权流程的第一步:构建授权页面的 URL 并重定向用户。

当用户点击安装应用时,应用需要将用户重定向到 Shoplazza 的授权页面,在 URL 中携带应用 ID、权限范围、回调地址等参数。

在项目根目录 index.js 中添加 /auth 路由:

const crypto = require("crypto");
const {
checkEnvMiddleware,
hmacValidator,
isValidShop,
saveState,
CLIENT_ID,
SCOPES,
REDIRECT_URI,
} = require("./utils/index");

app.get("/auth", checkEnvMiddleware, hmacValidator, (req, res) => {
const { shop } = req.query;

// 校验 shop 必须是合法的 *.myshoplaza.com 域名(防止 SSRF)
if (!isValidShop(shop)) {
return res.status(400).send("Invalid shop domain");
}

const state = crypto.randomBytes(16).toString("hex"); // 随机值,防止 CSRF 攻击
saveState(state, shop); // 存储 state,回调时校验

// URL 编码参数
const scopesEncoded = encodeURIComponent(SCOPES);
const redirectUriEncoded = encodeURIComponent(REDIRECT_URI);

// 重定向用户到 Shoplazza 授权页面
res.redirect(
`https://${shop}/admin/oauth/authorize?client_id=${CLIENT_ID}&scope=${scopesEncoded}&redirect_uri=${redirectUriEncoded}&response_type=code&state=${state}`
);
});

参数说明:

  • shop: 店铺域名(如 example.myshoplaza.com),从查询参数中获取,必须校验是合法的 *.myshoplaza.com 域名

  • client_id: 应用的客户端 ID

  • scope: 请求的权限范围,从环境变量 SCOPES 读取;多个权限用空格分隔(如 write_product read_order

  • redirect_uri: 授权完成后的回调地址

  • response_type: 固定值 code,表示使用授权码模式

  • state: 随机字符串,用于防止 CSRF 攻击;必须/auth 中存储、/auth/callback 中校验(不一致即拒绝)

更多权限范围请参考 Shoplazza API 权限文档

4.3 实现授权回调路由

当用户在 Shoplazza 授权页面点击同意后,Shoplazza 会将用户重定向回我们的应用,并在 URL 中携带授权码(code)。

我们需要实现回调路由来接收这个授权码(code),然后用它换取访问令牌(access_token)。获取到的 access_token 需要妥善存储,因为后续所有的 API 调用都需要使用它来进行身份验证。

index.js 中添加 /auth/callback 路由:

const axios = require("axios");
const {
hmacValidator,
isValidShop,
consumeState,
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URI,
} = require("./utils/index");
const { saveToken } = require("./utils/sqlite");

app.get("/auth/callback", hmacValidator, async (req, res) => {
const { code, hmac, state, shop } = req.query;

if (!shop || !hmac || !code || !state) {
return res.status(400).send("Required parameters missing");
}

// 校验 shop 域名(防止 SSRF)
if (!isValidShop(shop)) {
return res.status(400).send("Invalid shop domain");
}

// 校验 state(防止 CSRF):从存储取出,与本次 shop 一致才放行
if (!consumeState(state, shop)) {
return res.status(400).send("Invalid or expired state");
}

try {
// 使用 code 获取 access_token
const { data } = await axios.post(`https://${shop}/admin/oauth/token`, {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
});

// 验证响应结构
if (!data || !data.access_token) {
console.error("Token exchange failed: missing access_token", data);
return res.status(502).send("Token exchange failed");
}

// 存储 access_token
await saveToken(shop, data.access_token);

// 跳转到首页
res.redirect(`/?shop=${shop}`);
} catch (err) {
console.error("OAuth callback error:", err.response?.data || err.message);
res.status(500).send("OAuth callback failed");
}
});

Token 接口请求参数:

  • client_id: 应用的客户端 ID

  • client_secret: 应用的客户端密钥

  • code: 授权码

  • grant_type: 固定值 authorization_code

  • redirect_uri: 必须与授权时的回调地址一致

Token 接口响应:

{
"token_type": "Bearer",
"expires_at": 1550546245,
"access_token": "{YOUR_ACCESS_TOKEN}",
"refresh_token": "{YOUR_REFRESH_TOKEN}",
"store_id": "2",
"store_name": "xxx"
}

4.4 实现 Token 存储

获取到 access_token 后,我们需要将它持久化存储。

在示例项目中,我们使用 SQLite 数据库来存储每个店铺的访问令牌,以店铺域名作为主键(生产环境建议使用 PostgreSQL、MySQL 等)。这样当店铺再次访问应用时,我们可以直接从数据库中读取令牌,无需重新授权。

创建 utils/sqlite.js 文件:

const path = require("path");
const sqlite3 = require("sqlite3").verbose();

const DB_PATH = path.resolve(__dirname, "../database/tokens.db");
const db = new sqlite3.Database(DB_PATH);

// 初始化数据库表
const initPromise = new Promise((resolve, reject) => {
db.run(
`CREATE TABLE IF NOT EXISTS shop_tokens (
shop_domain TEXT PRIMARY KEY,
access_token TEXT NOT NULL,
updated_time INTEGER NOT NULL
)`,
(err) => {
if (err) return reject(err);
resolve();
}
);
});

// 保存或更新 token
function saveToken(shopDomain, accessToken) {
return initPromise.then(
() =>
new Promise((resolve, reject) => {
const now = Date.now();
// UPSERT:shop_domain 已存在则更新 token 与时间,否则插入新行(excluded 指本次待插入的值)
db.run(
`INSERT INTO shop_tokens (shop_domain, access_token, updated_time)
VALUES (?, ?, ?)
ON CONFLICT(shop_domain) DO UPDATE SET
access_token = excluded.access_token,
updated_time = excluded.updated_time`,
[shopDomain, accessToken, now],
(err) => {
if (err) return reject(err);
resolve();
}
);
})
);
}

// 获取 token
function getShopToken(shopDomain) {
return initPromise.then(
() =>
new Promise((resolve, reject) => {
db.get(
`SELECT access_token FROM shop_tokens WHERE shop_domain = ?`,
[shopDomain],
(err, row) => {
if (err) return reject(err);
resolve(row?.access_token || null);
}
);
})
);
}

module.exports = {
saveToken,
getShopToken,
};

数据库设计说明:

  • shop_domain: 店铺域名(主键)

  • access_token: 访问令牌

  • updated_time: 更新时间戳

并且创建 database 文件夹:

mkdir database

当应用启动时,会自动创建 tokens.db 文件,用于存储每个店铺的 access_token。

生产环境建议:

  • 不要使用 require("sqlite3").verbose()verbose 会输出额外的调试日志,影响性能与可读性;生产换成 require("sqlite3") 即可。
  • 数据库路径建议通过环境变量配置(例如 DB_PATH),避免相对路径在不同部署环境下出问题。
  • SQLite 仅适合演示,正式项目建议使用 PostgreSQL / MySQL 等独立数据库。

第五步:创建应用首页

5.1 实现首页路由

应用首页是用户授权成功后看到的第一个页面,我们使用该页面调用后端接口创建随机商品。后端接口在收到前端请求后,会先检查该店铺是否已经完成授权(即数据库中是否存在该店铺的 access_token)。如果未授权,则重定向到授权流程;如果已授权,则调用 Shoplazza OpenAPI 创建随机商品。

在项目根目录下 index.js 中,改造原始首页路由:

const path = require("path");
const { getShopToken } = require("./utils/sqlite");
const { isValidShop } = require("./utils/index");

app.get("/", checkEnvMiddleware, async (req, res) => {
const { shop } = req.query;

if (!shop || !isValidShop(shop)) {
return res.send("请提供合法的 shop 参数。");
}

// 检查是否已授权
const token = await getShopToken(shop);
if (!token) {
return res.redirect(`/auth?shop=${shop}`);
}

// 返回应用首页(用 sendFile 避免每次请求都同步读文件)
res.sendFile(path.resolve(__dirname, "index.html"));
});

res.sendFile 替代 fs.readFileSync + res.send,避免每次请求都同步读盘;如果首页是纯静态资源,也可以挂 app.use(express.static(__dirname)) 由 Express 托管。

逻辑说明:

  1. 从查询参数获取 shop 参数

  2. 查询数据库检查该店铺是否已授权

  3. 如果未授权,重定向到 /auth 开始授权流程

  4. 如果已授权,返回应用首页 HTML

5.2 创建 HTML 页面

现在我们来创建应用的前端界面。

这个页面包含一个简单的按钮,用户点击后会调用后端接口创建随机商品。页面使用原生 HTML 和 JavaScript 实现,通过 fetch API 与后端通信,并实时展示创建结果。

在项目根目录创建 index.html 文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shoplazza App Demo</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f7;
color: #111;
}
.page {
max-width: 860px;
margin: 24px auto;
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px 18px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.card h2 { margin: 0 0 8px; font-size: 18px; }
.card p { margin: 6px 0; color: #444; }
.card pre {
background: #f8f8fb;
padding: 12px;
border-radius: 8px;
overflow: auto;
}
.btn {
border: none;
background: #111;
color: #fff;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.result { margin-top: 12px; color: #333; font-size: 14px; }
</style>
</head>
<body>
<div class="page">
<div class="card">
<h2>恭喜创建 Shoplazza App 🎉</h2>
<p>这是一个简化的演示页面,技术栈如下:</p>
<ul>
<li>框架: Express</li>
<li>界面: 简单 HTML</li>
<li>API: <a href="https://www.shoplazza.dev/reference/api-usage" target="_blank">Shoplazza OpenAPI</a></li>
<li>数据库: SQLite</li>
</ul>
</div>

<div class="card">
<h2>开始创建商品</h2>
<p>点击按钮创建一个随机商品。</p>
<button class="btn" id="createBtn">生成商品</button>
<div class="result" id="createResult"></div>
<pre id="productResult" style="display: none;"></pre>
</div>
</div>

<script>
function getShop() {
const params = new URLSearchParams(window.location.search);
return params.get("shop");
}

const shop = getShop();
const btn = document.getElementById("createBtn");
const result = document.getElementById("createResult");
const productResult = document.getElementById("productResult");

btn.addEventListener("click", async () => {
if (!shop) {
result.textContent = "请提供 shop 参数。";
return;
}

btn.disabled = true;
result.textContent = "创建中...";
productResult.textContent = "";

try {
const resp = await fetch(`/api/create-product?shop=${encodeURIComponent(shop)}`, {
method: "POST",
});
const data = await resp.json();

if (!data.ok) {
result.textContent = "创建失败:" + JSON.stringify(data.error);
} else {
const product = data.product || {};
result.textContent = "创建成功:" + (product.title || product.id || "OK");
productResult.textContent = JSON.stringify(product, null, 2);
productResult.style.display = "block";
}
} catch (e) {
result.textContent = "创建失败:" + e.message;
productResult.style.display = "none";
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>

页面功能:

  • 显示应用基本信息

  • 提供"生成商品"按钮

  • 点击按钮时调用 /api/create-product 接口

  • 展示创建结果和商品详情

第六步:调用 OpenAPI 创建商品

6.1 创建随机商品数据生成函数

为了演示商品创建功能,我们需要一个能够生成随机商品数据的函数。这个函数会随机组合商品标题、生成随机价格,并构造符合 Shoplazza OpenAPI 要求的数据结构。

utils/index.js 中添加:

function randomPick(list) {
return list[Math.floor(Math.random() * list.length)];
}

function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function buildRandomProductPayload() {
const prefix = ["春日", "轻盈", "舒适", "简约", "清新", "自然", "柔软"];
const material = ["棉麻", "纯棉", "雪纺", "牛仔", "针织", "丝滑"];
const category = ["T恤", "衬衫", "连衣裙", "外套", "卫衣", "长裤"];
const suffix = ["款", "风", "系列", "限定", "基础款"];

const title = `${randomPick(prefix)}${randomPick(material)}${randomPick(category)}${randomPick(suffix)}`;
const price = parseFloat((randomInt(79, 299) + randomInt(0, 99) / 100).toFixed(2));

return {
title,
images: [
{
src: "https://placehold.co/800x800",
},
],
variants: [
{
price,
position: 1,
},
],
};
}

module.exports = {
// ... 其他导出
buildRandomProductPayload,
};

数据结构说明:

  • title: 商品标题(随机组合生成)

  • images: 商品图片数组

    • src: 图片 URL
  • variants: 商品规格数组

    • price: 价格

    • position: 规格排序位置

6.2 实现创建商品接口

现在我们来实现核心功能:调用 Shoplazza OpenAPI 创建商品。这个接口会从数据库获取店铺的 access_token,生成随机商品数据,然后调用 Shoplazza 的商品创建 API。接口需要处理各种可能的错误情况,并返回友好的错误信息。

index.js 中添加 /api/create-product 路由:

const { buildRandomProductPayload, isValidShop } = require("./utils/index");

app.post("/api/create-product", checkEnvMiddleware, async (req, res) => {
const { shop } = req.query;

if (!shop || !isValidShop(shop)) {
return res.status(400).json({ ok: false, error: "请提供合法的 shop 参数。" });
}

const token = await getShopToken(shop);

if (!token) {
return res.status(401).json({ ok: false, error: "未授权,请先完成安装。" });
}

try {
const payload = buildRandomProductPayload();

const { data } = await axios.post(
`https://${shop}/openapi/2025-06/products`,
{ product: payload },
{
headers: {
"Access-Token": token,
},
}
);

res.json({ ok: true, product: data?.product || data });
} catch (err) {
const status = err.response?.status || 500;
const detail = err.response?.data || err.message || "创建失败";

res.status(status).json({ ok: false, error: detail });
}
});

创建商品是写操作,应使用 app.post(...)(前端 fetch 也需带 method: "POST")。app.get 在 RESTful 语义中代表只读,且部分浏览器/代理可能缓存 GET 请求或在重定向时重发。

OpenAPI 接口说明:

关于 Shoplazza OpenAPI 的详细参数说明、请求格式、响应结构等,请参考 Shoplazza 商品 API 文档

6.3 添加全局错误处理中间件

为防止异步路由中未捕获的异常导致进程崩溃,在 index.js 所有路由注册之后追加一个错误处理中间件:

// 注意:错误处理中间件必须是 4 个参数 (err, req, res, next),且放在所有路由之后
app.use((err, req, res, next) => {
console.error("Unhandled error:", err);
if (res.headersSent) return next(err);
res.status(500).json({ ok: false, error: "Internal server error" });
});

如果还存在不经过 Express 路由的异步代码(如全局事件回调),建议同时监听 process.on("unhandledRejection", ...)process.on("uncaughtException", ...) 做进程级兜底。

第七步:测试应用

7.1 启动应用

确保以下服务都在运行:

终端 1 - Express 服务:

node index.js

终端 2 - ngrok 服务:

ngrok http 3000

7.2 安装应用到测试店铺

  1. 通过 Partners 后台安装测试应用:登录 Shoplazza Partners,点击“应用详情页 -- 应用测试 -- 选择店铺 -- 安装App”

    Image

  2. 你将被重定向到 Shoplazza 授权页面

    Image

  3. 点击"安装App"按钮

  4. 授权成功后,你将被重定向到应用首页

    Image

7.3 创建商品

  1. 在应用首页点击"生成商品"按钮

  2. 等待几秒后,你将看到创建成功的提示和商品详情

    Image

  3. 登录 Shoplazza 店铺后台,在"商品"页面查看刚创建的商品

    Image

下一步

恭喜你!你成功使用 Node.js、Express.js、SQLite 创建了一个 Shoplazza App。接下来你可以: