feat: three nuxt frontends, demo seed, rounding + money-exponent + rate-cast fixes, archived specs
This commit is contained in:
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Seed demo data into a running vmall-api (dev).
|
||||
* Usage: node scripts/seed-demo.mjs [baseUrl]
|
||||
* Idempotent: existing slugs/emails are reused, products are re-published.
|
||||
*/
|
||||
const base = process.argv[2] ?? "http://localhost:8080/api";
|
||||
|
||||
async function call(method, path, { token, body } = {}) {
|
||||
const res = await fetch(base + path, {
|
||||
method,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
return { status: res.status, data };
|
||||
}
|
||||
|
||||
const fail = (what, r) => {
|
||||
console.error(`FAILED ${what}: ${r.status}`, r.data);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
// 1. admin login
|
||||
let r = await call("POST", "/auth/login", {
|
||||
body: { email: "admin@vmall.local", password: "admin1234" },
|
||||
});
|
||||
if (r.status !== 200) fail("admin login", r);
|
||||
const admin = r.data.token;
|
||||
|
||||
// 2. demo shop
|
||||
let shopId;
|
||||
r = await call("POST", "/admin/shops", {
|
||||
token: admin,
|
||||
body: {
|
||||
name: { en: "Demo Store", zh: "演示店铺" },
|
||||
slug: "demo-store",
|
||||
},
|
||||
});
|
||||
if (r.status === 201) shopId = r.data.id;
|
||||
else if (r.status === 409) {
|
||||
const shops = await call("GET", "/admin/shops", { token: admin });
|
||||
shopId = shops.data.find((s) => s.slug === "demo-store").id;
|
||||
} else fail("create shop", r);
|
||||
|
||||
// 3. demo shop owner (register may 409 if exists)
|
||||
const ownerEmail = "shop@vmall.local";
|
||||
await call("POST", "/auth/register", {
|
||||
body: { email: ownerEmail, password: "shop12345", display_name: "Demo Owner" },
|
||||
});
|
||||
r = await call("POST", "/auth/login", {
|
||||
body: { email: ownerEmail, password: "shop12345" },
|
||||
});
|
||||
if (r.status !== 200) fail("owner login", r);
|
||||
let owner = r.data.token;
|
||||
if (r.data.user.role !== "shop_owner") {
|
||||
r = await call("PUT", `/admin/users/${r.data.user.id}/role`, {
|
||||
token: admin,
|
||||
body: { role: "shop_owner", shop_id: shopId },
|
||||
});
|
||||
if (r.status !== 200) fail("assign owner", r);
|
||||
r = await call("POST", "/auth/login", {
|
||||
body: { email: ownerEmail, password: "shop12345" },
|
||||
});
|
||||
owner = r.data.token;
|
||||
}
|
||||
|
||||
// 4. demo customer
|
||||
await call("POST", "/auth/register", {
|
||||
body: { email: "customer@vmall.local", password: "customer123", display_name: "Demo Customer" },
|
||||
});
|
||||
|
||||
// 5. categories
|
||||
const cats = (await call("GET", "/categories")).data;
|
||||
const catBy = (slug) => cats.find((c) => c.slug === slug)?.id ?? null;
|
||||
|
||||
// 6. products
|
||||
const products = [
|
||||
{
|
||||
slug: "wireless-headphones",
|
||||
name: { en: "Wireless Headphones", zh: "无线耳机" },
|
||||
description: {
|
||||
en: "Noise-cancelling over-ear headphones with 40h battery.",
|
||||
zh: "降噪头戴式耳机,40 小时续航。",
|
||||
},
|
||||
category: "electronics",
|
||||
price: 7999,
|
||||
stock: 25,
|
||||
img: "https://picsum.photos/seed/headphones/600/600",
|
||||
},
|
||||
{
|
||||
slug: "mechanical-keyboard",
|
||||
name: { en: "Mechanical Keyboard", zh: "机械键盘" },
|
||||
description: {
|
||||
en: "Hot-swappable 75% keyboard, gasket mount.",
|
||||
zh: "热插拔 75% 配列机械键盘,Gasket 结构。",
|
||||
},
|
||||
category: "electronics",
|
||||
price: 12900,
|
||||
stock: 40,
|
||||
img: "https://picsum.photos/seed/keyboard/600/600",
|
||||
},
|
||||
{
|
||||
slug: "linen-shirt",
|
||||
name: { en: "Linen Shirt", zh: "亚麻衬衫" },
|
||||
description: { en: "Breathable 100% linen shirt.", zh: "透气纯亚麻衬衫。" },
|
||||
category: "fashion",
|
||||
price: 4599,
|
||||
stock: 60,
|
||||
img: "https://picsum.photos/seed/shirt/600/600",
|
||||
},
|
||||
{
|
||||
slug: "ceramic-mug",
|
||||
name: { en: "Ceramic Mug", zh: "陶瓷马克杯" },
|
||||
description: { en: "Hand-glazed 350ml mug.", zh: "手工上釉 350ml 马克杯。" },
|
||||
category: "home-living",
|
||||
price: 1999,
|
||||
stock: 100,
|
||||
img: "https://picsum.photos/seed/mug/600/600",
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of products) {
|
||||
r = await call("POST", "/shop/products", {
|
||||
token: owner,
|
||||
body: {
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
category_id: catBy(p.category),
|
||||
images: [p.img],
|
||||
},
|
||||
});
|
||||
let id;
|
||||
if (r.status === 201) id = r.data.id;
|
||||
else if (r.status === 409) {
|
||||
const list = await call("GET", "/shop/products?per_page=100", { token: owner });
|
||||
id = list.data.items.find((x) => x.slug === p.slug).id;
|
||||
} else fail(`create product ${p.slug}`, r);
|
||||
r = await call("POST", `/shop/products/${id}/skus`, {
|
||||
token: owner,
|
||||
body: {
|
||||
sku_code: `${p.slug}-std`,
|
||||
price_minor: p.price,
|
||||
currency: "USD",
|
||||
stock: p.stock,
|
||||
},
|
||||
});
|
||||
if (r.status !== 200) fail(`sku ${p.slug}`, r);
|
||||
r = await call("POST", `/shop/products/${id}/publish`, { token: owner });
|
||||
if (r.status !== 200) fail(`publish ${p.slug}`, r);
|
||||
console.log(`product ready: ${p.slug}`);
|
||||
}
|
||||
|
||||
console.log("\nDemo data seeded.");
|
||||
console.log(" platform admin : admin@vmall.local / admin1234 (apps/admin :3002)");
|
||||
console.log(" shop owner : shop@vmall.local / shop12345 (apps/shop-admin :3001)");
|
||||
console.log(" customer : customer@vmall.local / customer123 (apps/mall :3000)");
|
||||
Reference in New Issue
Block a user