/** * 云函数:remixManager(视频重塑记录) * 替代:GET/POST/DELETE /api/remixes[/:videoId[/:remixId]] * 支持 action: * listAll * listByVideo * upsert * delete */ const { requireSession, assertRequestedUserMatchesSession } = require('./_session'); async function handler(request, response) { try { await ensureTable(); const action = pickParam(request, 'action') || 'listAll'; const session = await requireSession(request, Psql); assertRequestedUserMatchesSession(request, session); const userId = session.userId; if (action === 'listAll') return listAll(response, userId); if (action === 'listByVideo') return listByVideo(request, response, userId); if (action === 'upsert') return upsertRemix(request, response, userId); if (action === 'delete') return deleteRemix(request, response, userId); return response.json({ code: 400, success: false, error: `未知 action: ${action}` }); } catch (error) { console.error('remixManager failed:', error && error.message); return response.json({ code: error.status || 500, success: false, error: error.message }); } } async function ensureTable() { await Psql.query(` CREATE TABLE IF NOT EXISTS "Remix" ( "objectId" VARCHAR(50) PRIMARY KEY, "videoId" VARCHAR(255) NOT NULL, "remixId" VARCHAR(255) NOT NULL, "data" JSONB NOT NULL DEFAULT '{}', "userId" VARCHAR(255) DEFAULT '', "createdAt" TIMESTAMPTZ DEFAULT NOW(), "updatedAt" TIMESTAMPTZ DEFAULT NOW() ) `); await Psql.query(`DROP INDEX IF EXISTS idx_remix_pair`); await Psql.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_remix_user_pair ON "Remix" ("userId", "videoId", "remixId")`); await Psql.query(`CREATE INDEX IF NOT EXISTS idx_remix_video ON "Remix" ("videoId")`); await Psql.query(`CREATE INDEX IF NOT EXISTS idx_remix_user ON "Remix" ("userId")`); } async function listAll(response, userId) { const rows = await Psql.query( `SELECT * FROM "Remix" WHERE "userId" = $1 ORDER BY "createdAt" DESC LIMIT 5000`, [userId] ); const grouped = {}; for (const row of rows) { if (!grouped[row.videoId]) grouped[row.videoId] = []; grouped[row.videoId].push(rowToObj(row)); } return response.json({ code: 200, success: true, data: grouped }); } async function listByVideo(request, response, userId) { const videoId = pickParam(request, 'videoId'); if (!videoId) return response.json({ code: 400, success: false, error: '缺少 videoId' }); const rows = await Psql.query( `SELECT * FROM "Remix" WHERE "videoId" = $1 AND "userId" = $2 ORDER BY "createdAt" DESC`, [videoId, userId] ); return response.json({ code: 200, success: true, data: rows.map(rowToObj) }); } async function upsertRemix(request, response, userId) { const videoId = pickParam(request, 'videoId'); const body = pickParam(request, 'remix', 'data') || request.body || {}; const remixId = body.remixId || pickParam(request, 'remixId'); if (!videoId || !remixId) { return response.json({ code: 400, success: false, error: '缺少 videoId 或 remixId' }); } const now = new Date().toISOString(); const existing = await Psql.query( `SELECT * FROM "Remix" WHERE "videoId"=$1 AND "remixId"=$2 AND "userId"=$3 LIMIT 1`, [videoId, remixId, userId] ); const previous = existing.length ? rowToObj(existing[0]) : {}; const merged = { ...previous, ...body, remixId, videoId, userId, created_at: previous.created_at || body.created_at || now, updated_at: now, }; await Psql.query( `INSERT INTO "Remix" ("objectId","videoId","remixId","data","userId") VALUES ($1,$2,$3,$4,$5) ON CONFLICT ("userId","videoId","remixId") DO UPDATE SET "data"=$4, "updatedAt"=NOW()`, [existing[0]?.objectId || generateId(), videoId, remixId, JSON.stringify(merged), userId] ); return response.json({ code: 200, success: true, data: merged }); } async function deleteRemix(request, response, userId) { const videoId = pickParam(request, 'videoId'); const remixId = pickParam(request, 'remixId'); if (!videoId || !remixId) { return response.json({ code: 400, success: false, error: '缺少 videoId 或 remixId' }); } const result = await Psql.query( `DELETE FROM "Remix" WHERE "videoId"=$1 AND "remixId"=$2 AND "userId"=$3 RETURNING "objectId"`, [videoId, remixId, userId] ); return response.json({ code: 200, success: true, data: { videoId, remixId, deleted: result.length > 0 } }); } function rowToObj(row) { const data = typeof row.data === 'string' ? JSON.parse(row.data) : (row.data || {}); return { ...data, objectId: row.objectId, createdAt: row.createdAt, updatedAt: row.updatedAt }; } function pickParam(request, ...names) { const sources = [request.params, request.body, request.query, request]; for (const src of sources) { if (!src || typeof src !== 'object') continue; for (const name of names) { const value = src[name]; if (value !== undefined && value !== null && value !== '') return value; } } return null; } function generateId() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let value = ''; for (let i = 0; i < 10; i += 1) value += chars.charAt(Math.floor(Math.random() * chars.length)); return value; }