Files
photo-live/scripts/demo-seed.mjs
T
2026-09-21 10:17:16 +08:00

151 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 演示数据种子:登录 admin → 建双语演示相册 + 分类 → sharp 生成 12 张真实照片 →
// 全链上传(presign PUT → complete)→ 等 worker 发布 → 建 embed slot 绑相册。
// 产出:观众页 URL + 嵌入宿主页 URL。可重复运行(相册名带时间戳)。
import { execSync } from 'node:child_process';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const sharp = require(
require('path').join(process.cwd(), 'services/api/node_modules/sharp'),
);
const CN = 'http://127.0.0.1:8080';
const stamp = new Date().toISOString().slice(11, 19).replace(/:/g, '');
const j = (r) => r.json();
const api = async (path, opts = {}) => {
const r = await fetch(CN + path, {
...opts,
headers: { 'content-type': 'application/json', ...(opts.headers || {}) },
});
if (!r.ok && r.status !== 201 && r.status !== 202 && r.status !== 204)
throw new Error(`${path} → ${r.status}: ${await r.text()}`);
return r.status === 204 ? null : j(r);
};
// 1. 登录
const login = await api('/v1/auth/password', {
method: 'POST',
body: JSON.stringify({
account: process.env.ADMIN_ACCOUNT || 'admin',
password: process.env.ADMIN_PASSWORD || 'admin-secret-change-me',
}),
});
const auth = { Authorization: `Bearer ${login.sessionToken}` };
console.log('✓ admin 登录');
// 2. 双语相册 + 分类
const album = await api('/v1/albums', {
method: 'POST',
headers: auth,
body: JSON.stringify({
translations: [
{ locale: 'zh-CN', title: '演示相册 · 现场直击', description: '这是本地演示环境生成的相册,照片由脚本真实上传并经 worker 处理发布。', venue: '广州·广交会展馆' },
{ locale: 'en', title: 'Demo Album · Live', description: 'Generated by the local demo seed; photos really uploaded and processed.', venue: 'Canton Fair Complex' },
],
defaultLocale: 'zh-CN',
startAt: new Date().toISOString(),
}),
});
const cat = await api(`/v1/albums/${album.id}/categories`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
sortOrder: 1,
translations: [
{ locale: 'zh-CN', name: '开幕现场' },
{ locale: 'en', name: 'Opening' },
],
}),
});
console.log(`✓ 相册 ${album.publicToken} + 分类`);
// 3. 12 张真实照片(不同色块/比例,横竖混排)
const palettes = [
['#e74c3c', '#c0392b'], ['#3498db', '#2980b9'], ['#2ecc71', '#27ae60'],
['#f39c12', '#e67e22'], ['#9b59b6', '#8e44ad'], ['#1abc9c', '#16a085'],
['#e91e63', '#ad1457'], ['#3f51b5', '#303f9f'], ['#00bcd4', '#0097a7'],
['#ff5722', '#e64a19'], ['#607d8b', '#455a64'], ['#8bc34a', '#689f38'],
];
const dims = [[1600, 1067], [1200, 1600], [1600, 900], [1080, 1350]];
const ids = [];
for (let i = 0; i < palettes.length; i++) {
const [w, h] = dims[i % dims.length];
const [a, b] = palettes[i];
const svg = `<svg width="${w}" height="${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${a}"/><stop offset="1" stop-color="${b}"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><text x="50%" y="50%" font-family="sans-serif" font-size="${Math.round(Math.min(w, h) / 6)}" fill="rgba(255,255,255,.85)" text-anchor="middle" dominant-baseline="middle">IMG ${String(i + 1).padStart(2, '0')}</text></svg>`;
const buf = await sharp(Buffer.from(svg)).jpeg({ quality: 88 }).toBuffer();
const up = await api('/v1/uploads', {
method: 'POST',
headers: auth,
body: JSON.stringify({
albumId: album.id,
categoryId: i < 6 ? cat.id : undefined,
clientTaskId: `demo-${stamp}-${i}`,
filename: `demo-${i + 1}.jpg`,
bytes: buf.length,
contentType: 'image/jpeg',
sourceDeviceId: 'demo-seed-000000000000001',
}),
});
const put = await fetch(up.credential.url, {
method: 'PUT',
headers: up.credential.headers,
body: buf,
});
if (!put.ok) throw new Error(`PUT ${i}: ${put.status}`);
await api(`/v1/uploads/${up.uploadId}/complete`, { method: 'POST', headers: auth });
ids.push(up.photoId);
}
console.log(`✓ ${ids.length} 张照片上传完成,等待 worker 发布...`);
// 4. 等 worker 发布(poll 公开列表)
let seen = 0;
for (let t = 0; t < 40 && seen < ids.length; t++) {
await new Promise((r) => setTimeout(r, 1000));
const list = await fetch(
`${CN}/v1/public/albums/${album.publicToken}/photos?limit=60`,
).then(j);
seen = list.items.length;
}
console.log(`✓ 公开列表可见 ${seen}/${ids.length}(worker+sync 已处理)`);
// 5. embed slot
const slot = await api('/v1/admin/embed-slots', {
method: 'POST',
headers: auth,
body: JSON.stringify({ name: `官网演示位 ${stamp}` }),
});
await api(`/v1/admin/embed-slots/${slot.id}`, {
method: 'PATCH',
headers: auth,
body: JSON.stringify({ albumId: album.id }),
});
console.log(`✓ 展示位 ${slot.publicToken} 已绑定相册`);
// 6. 点几个赞(让热门排序/气泡有内容)
const ov = 'http://127.0.0.1:8081';
for (const [idx, photoId] of ids.slice(0, 4).entries()) {
for (let k = 0; k < (5 - idx); k++) {
await fetch(`${ov}/v1/public/albums/${album.publicToken}/photos/${photoId}/likes`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sessionId: `demo-user-${idx}`, batchId: `b${idx}-${k}`, delta: 1 }),
});
}
}
// 浏览上报
for (let v = 0; v < 5; v++) {
await fetch(`${ov}/v1/public/albums/${album.publicToken}/visits`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ visitorId: `demo-visitor-${v}${v}${v}${v}`, requestId: `demo-req-${v}-${stamp}` }),
});
}
console.log('✓ 点赞 + 浏览上报完成');
console.log('\n════════ 演示入口 ════════');
console.log(`观众页(中文): http://localhost:5175/a/${album.publicToken}`);
console.log(`观众页(英文): http://localhost:5175/a/${album.publicToken}?lang=en`);
console.log(`嵌入组件宿主页: http://localhost:5175/__test__/embed-host.html?slot=${slot.publicToken}`);
console.log(`管理后台: http://localhost:5176 (admin / ${process.env.ADMIN_PASSWORD || 'admin-secret-change-me'},相册列表第一个即本相册)`);