Files
photo-live/scripts/demo-seed.mjs
T

151 lines
6.2 KiB
JavaScript
Raw Normal View History

// 演示数据种子:登录 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'},相册列表第一个即本相册)`);