// 演示数据种子:登录 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 = `IMG ${String(i + 1).padStart(2, '0')}`; 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'},相册列表第一个即本相册)`);