|
@@ -1,131 +1,115 @@
|
|
|
/**
|
|
/**
|
|
|
* 云函数:taskManager(任务管理)
|
|
* 云函数:taskManager(任务管理)
|
|
|
* 替代:GET/POST/PUT/DELETE /api/tasks[/:id]
|
|
* 替代:GET/POST/PUT/DELETE /api/tasks[/:id]
|
|
|
- * actions: list | get | create | update | delete
|
|
|
|
|
|
|
+ * 支持 action:list | get | create | update | delete
|
|
|
*/
|
|
*/
|
|
|
|
|
+const { requireSession, assertRequestedUserMatchesSession } = require('./_session');
|
|
|
|
|
+
|
|
|
async function handler(request, response) {
|
|
async function handler(request, response) {
|
|
|
try {
|
|
try {
|
|
|
- await Psql.query(`
|
|
|
|
|
- CREATE TABLE IF NOT EXISTS "VideoflowTask" (
|
|
|
|
|
- "objectId" VARCHAR(50) PRIMARY KEY,
|
|
|
|
|
- "bizId" VARCHAR(255),
|
|
|
|
|
- "data" JSONB NOT NULL DEFAULT '{}',
|
|
|
|
|
- "userId" VARCHAR(255) DEFAULT '',
|
|
|
|
|
- "type" VARCHAR(50) DEFAULT '',
|
|
|
|
|
- "status" VARCHAR(50) DEFAULT '',
|
|
|
|
|
- "createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
- "updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
- )
|
|
|
|
|
- `);
|
|
|
|
|
- await Psql.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_videoflow_task_biz ON "VideoflowTask" ("bizId")`);
|
|
|
|
|
- await Psql.query(`CREATE INDEX IF NOT EXISTS idx_videoflow_task_user ON "VideoflowTask" ("userId")`);
|
|
|
|
|
- await Psql.query(`CREATE INDEX IF NOT EXISTS idx_videoflow_task_status ON "VideoflowTask" ("status")`);
|
|
|
|
|
-
|
|
|
|
|
const action = pickParam(request, 'action') || 'list';
|
|
const action = pickParam(request, 'action') || 'list';
|
|
|
- const userId = pickParam(request, 'userId') || '';
|
|
|
|
|
|
|
+ const session = await requireSession(request, Psql);
|
|
|
|
|
+ assertRequestedUserMatchesSession(request, session);
|
|
|
|
|
+ const userId = session.userId;
|
|
|
|
|
|
|
|
- if (action === 'list') {
|
|
|
|
|
- const conds = [], params = [];
|
|
|
|
|
- if (userId) { params.push(userId); conds.push(`"userId" = $${params.length}`); }
|
|
|
|
|
- const status = pickParam(request, 'status');
|
|
|
|
|
- if (status) { params.push(status); conds.push(`"status" = $${params.length}`); }
|
|
|
|
|
- const type = pickParam(request, 'type');
|
|
|
|
|
- if (type) { params.push(type); conds.push(`"type" = $${params.length}`); }
|
|
|
|
|
- const limit = Math.min(parseInt(pickParam(request, 'limit') || '500'), 1000);
|
|
|
|
|
- const where = conds.length ? `WHERE ${conds.join(' AND ')}` : '';
|
|
|
|
|
- params.push(limit);
|
|
|
|
|
- const rows = await Psql.query(
|
|
|
|
|
- `SELECT * FROM "VideoflowTask" ${where} ORDER BY "createdAt" DESC LIMIT $${params.length}`,
|
|
|
|
|
- params
|
|
|
|
|
- );
|
|
|
|
|
- response.json({ code: 200, success: true, data: rows.map(rowToObj) });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ await ensureTable();
|
|
|
|
|
|
|
|
- if (action === 'get') {
|
|
|
|
|
- const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
- const rows = await Psql.query(`SELECT * FROM "VideoflowTask" WHERE "bizId" = $1 LIMIT 1`, [id]);
|
|
|
|
|
- if (!rows.length) return response.json({ code: 404, success: false, error: '未找到任务' });
|
|
|
|
|
- response.json({ code: 200, success: true, data: rowToObj(rows[0]) });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (action === 'list') return listTasks(request, response, userId);
|
|
|
|
|
+ if (action === 'get') return getTask(request, response, userId);
|
|
|
|
|
+ if (action === 'create') return upsertTask(request, response, userId, 'create');
|
|
|
|
|
+ if (action === 'update') return upsertTask(request, response, userId, 'update');
|
|
|
|
|
+ if (action === 'delete') return deleteTask(request, response, userId);
|
|
|
|
|
|
|
|
- if (action === 'create') {
|
|
|
|
|
- const body = pickParam(request, 'task', 'data') || request.body || {};
|
|
|
|
|
- const bizId = body.id || body.bizId || generateId();
|
|
|
|
|
- const merged = { ...body, id: bizId, created_at: body.created_at || new Date().toISOString(), updated_at: new Date().toISOString() };
|
|
|
|
|
- const objectId = generateId();
|
|
|
|
|
- await Psql.query(
|
|
|
|
|
- `INSERT INTO "VideoflowTask" ("objectId","bizId","data","userId","type","status")
|
|
|
|
|
- VALUES ($1,$2,$3,$4,$5,$6)
|
|
|
|
|
- ON CONFLICT ("bizId") DO UPDATE SET "data"=$3, "type"=$5, "status"=$6, "updatedAt"=NOW()`,
|
|
|
|
|
- [objectId, bizId, JSON.stringify(merged), userId, merged.type || '', merged.status || '']
|
|
|
|
|
- );
|
|
|
|
|
- response.json({ code: 200, success: true, data: merged });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return response.json({ code: 400, success: false, error: `未知 action: ${action}` });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('taskManager failed:', error && error.message);
|
|
|
|
|
+ return response.json({ code: error.status || 500, success: false, error: error.message });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (action === 'update') {
|
|
|
|
|
- const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
- const patch = pickParam(request, 'task', 'data', 'patch') || request.body || {};
|
|
|
|
|
- const cur = await Psql.query(`SELECT * FROM "VideoflowTask" WHERE "bizId" = $1 LIMIT 1`, [id]);
|
|
|
|
|
- const now = new Date().toISOString();
|
|
|
|
|
- const previous = cur.length ? rowToObj(cur[0]) : {};
|
|
|
|
|
- const merged = {
|
|
|
|
|
- ...previous,
|
|
|
|
|
- ...patch,
|
|
|
|
|
- id,
|
|
|
|
|
- bizId: patch.bizId || patch.id || id,
|
|
|
|
|
- created_at: previous.created_at || patch.created_at || now,
|
|
|
|
|
- updated_at: now,
|
|
|
|
|
- };
|
|
|
|
|
- if (cur.length) {
|
|
|
|
|
- await Psql.query(
|
|
|
|
|
- `UPDATE "VideoflowTask" SET "data"=$1, "type"=$2, "status"=$3, "updatedAt"=NOW() WHERE "bizId"=$4`,
|
|
|
|
|
- [JSON.stringify(merged), merged.type || '', merged.status || '', id]
|
|
|
|
|
- );
|
|
|
|
|
- } else {
|
|
|
|
|
- await Psql.query(
|
|
|
|
|
- `INSERT INTO "VideoflowTask" ("objectId","bizId","data","userId","type","status")
|
|
|
|
|
- VALUES ($1,$2,$3,$4,$5,$6)
|
|
|
|
|
- ON CONFLICT ("bizId") DO UPDATE SET "data"=$3, "type"=$5, "status"=$6, "updatedAt"=NOW()`,
|
|
|
|
|
- [generateId(), id, JSON.stringify(merged), userId || merged.userId || '', merged.type || '', merged.status || '']
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- response.json({ code: 200, success: true, data: merged });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+async function ensureTable() {
|
|
|
|
|
+ await Psql.query(`
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS "VideoflowTask" (
|
|
|
|
|
+ "objectId" VARCHAR(50) PRIMARY KEY,
|
|
|
|
|
+ "bizId" VARCHAR(255),
|
|
|
|
|
+ "data" JSONB NOT NULL DEFAULT '{}',
|
|
|
|
|
+ "userId" VARCHAR(255) DEFAULT '',
|
|
|
|
|
+ "type" VARCHAR(50) DEFAULT '',
|
|
|
|
|
+ "status" VARCHAR(50) DEFAULT '',
|
|
|
|
|
+ "createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
+ "updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
+ )
|
|
|
|
|
+ `);
|
|
|
|
|
+ await Psql.query(`DROP INDEX IF EXISTS idx_videoflow_task_biz`);
|
|
|
|
|
+ await Psql.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_videoflow_task_user_biz ON "VideoflowTask" ("userId", "bizId")`);
|
|
|
|
|
+ await Psql.query(`CREATE INDEX IF NOT EXISTS idx_videoflow_task_user ON "VideoflowTask" ("userId")`);
|
|
|
|
|
+ await Psql.query(`CREATE INDEX IF NOT EXISTS idx_videoflow_task_status ON "VideoflowTask" ("status")`);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (action === 'update') {
|
|
|
|
|
- const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
- const patch = pickParam(request, 'task', 'data', 'patch') || request.body || {};
|
|
|
|
|
- const cur = await Psql.query(`SELECT * FROM "VideoflowTask" WHERE "bizId" = $1 LIMIT 1`, [id]);
|
|
|
|
|
- if (!cur.length) return response.json({ code: 404, success: false, error: '未找到任务' });
|
|
|
|
|
- const merged = { ...rowToObj(cur[0]), ...patch, updated_at: new Date().toISOString() };
|
|
|
|
|
- await Psql.query(
|
|
|
|
|
- `UPDATE "VideoflowTask" SET "data"=$1, "type"=$2, "status"=$3, "updatedAt"=NOW() WHERE "bizId"=$4`,
|
|
|
|
|
- [JSON.stringify(merged), merged.type || '', merged.status || '', id]
|
|
|
|
|
- );
|
|
|
|
|
- response.json({ code: 200, success: true, data: merged });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+async function listTasks(request, response, userId) {
|
|
|
|
|
+ const conds = ['"userId" = $1'];
|
|
|
|
|
+ const params = [userId];
|
|
|
|
|
+ const status = pickParam(request, 'status');
|
|
|
|
|
+ if (status) {
|
|
|
|
|
+ params.push(status);
|
|
|
|
|
+ conds.push(`"status" = $${params.length}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ const type = pickParam(request, 'type');
|
|
|
|
|
+ if (type) {
|
|
|
|
|
+ params.push(type);
|
|
|
|
|
+ conds.push(`"type" = $${params.length}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ const limit = safeLimit(pickParam(request, 'limit'), 500, 1000);
|
|
|
|
|
+ params.push(limit);
|
|
|
|
|
+ const rows = await Psql.query(
|
|
|
|
|
+ `SELECT * FROM "VideoflowTask" WHERE ${conds.join(' AND ')} ORDER BY "createdAt" DESC LIMIT $${params.length}`,
|
|
|
|
|
+ params
|
|
|
|
|
+ );
|
|
|
|
|
+ return response.json({ code: 200, success: true, data: rows.map(rowToObj) });
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (action === 'delete') {
|
|
|
|
|
- const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
- const r = await Psql.query(`DELETE FROM "VideoflowTask" WHERE "bizId" = $1 RETURNING "bizId"`, [id]);
|
|
|
|
|
- response.json({ code: 200, success: true, data: { id, deleted: r.length > 0 } });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+async function getTask(request, response, userId) {
|
|
|
|
|
+ const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
+ if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
+ const rows = await Psql.query(`SELECT * FROM "VideoflowTask" WHERE "bizId" = $1 AND "userId" = $2 LIMIT 1`, [id, userId]);
|
|
|
|
|
+ if (!rows.length) return response.json({ code: 404, success: false, error: '未找到任务' });
|
|
|
|
|
+ return response.json({ code: 200, success: true, data: rowToObj(rows[0]) });
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- response.json({ code: 400, success: false, error: `未知 action: ${action}` });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('❌ taskManager 失败:', error.message);
|
|
|
|
|
- response.json({ code: 500, success: false, error: error.message });
|
|
|
|
|
|
|
+async function upsertTask(request, response, userId, mode) {
|
|
|
|
|
+ const body = pickParam(request, 'task', 'data', 'patch') || request.body || {};
|
|
|
|
|
+ const requestedId = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
+ const bizId = body.id || body.bizId || requestedId || generateId();
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+ const existing = await Psql.query(`SELECT * FROM "VideoflowTask" WHERE "bizId" = $1 AND "userId" = $2 LIMIT 1`, [bizId, userId]);
|
|
|
|
|
+ const previous = existing.length ? rowToObj(existing[0]) : {};
|
|
|
|
|
+ if (mode === 'update' && requestedId && requestedId !== bizId && existing.length) {
|
|
|
|
|
+ return response.json({ code: 400, success: false, error: '任务 id 不一致' });
|
|
|
}
|
|
}
|
|
|
|
|
+ const merged = {
|
|
|
|
|
+ ...previous,
|
|
|
|
|
+ ...body,
|
|
|
|
|
+ id: bizId,
|
|
|
|
|
+ bizId,
|
|
|
|
|
+ userId,
|
|
|
|
|
+ created_at: previous.created_at || body.created_at || now,
|
|
|
|
|
+ updated_at: now,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ await Psql.query(
|
|
|
|
|
+ `INSERT INTO "VideoflowTask" ("objectId","bizId","data","userId","type","status")
|
|
|
|
|
+ VALUES ($1,$2,$3,$4,$5,$6)
|
|
|
|
|
+ ON CONFLICT ("userId","bizId") DO UPDATE SET "data"=$3, "type"=$5, "status"=$6, "updatedAt"=NOW()`,
|
|
|
|
|
+ [existing[0]?.objectId || generateId(), bizId, JSON.stringify(merged), userId, merged.type || '', merged.status || '']
|
|
|
|
|
+ );
|
|
|
|
|
+ return response.json({ code: 200, success: true, data: merged });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteTask(request, response, userId) {
|
|
|
|
|
+ const id = pickParam(request, 'id', 'taskId', 'bizId');
|
|
|
|
|
+ if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
|
|
|
|
|
+ const result = await Psql.query(`DELETE FROM "VideoflowTask" WHERE "bizId" = $1 AND "userId" = $2 RETURNING "bizId"`, [id, userId]);
|
|
|
|
|
+ return response.json({ code: 200, success: true, data: { id, deleted: result.length > 0 } });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function rowToObj(row) {
|
|
function rowToObj(row) {
|
|
@@ -134,20 +118,25 @@ function rowToObj(row) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function pickParam(request, ...names) {
|
|
function pickParam(request, ...names) {
|
|
|
- const sources = [request.params, request.body, request];
|
|
|
|
|
|
|
+ const sources = [request.params, request.body, request.query, request];
|
|
|
for (const src of sources) {
|
|
for (const src of sources) {
|
|
|
if (!src || typeof src !== 'object') continue;
|
|
if (!src || typeof src !== 'object') continue;
|
|
|
- for (const n of names) {
|
|
|
|
|
- const v = src[n];
|
|
|
|
|
- if (v !== undefined && v !== null && v !== '') return v;
|
|
|
|
|
|
|
+ for (const name of names) {
|
|
|
|
|
+ const value = src[name];
|
|
|
|
|
+ if (value !== undefined && value !== null && value !== '') return value;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function safeLimit(value, fallback, max) {
|
|
|
|
|
+ const parsed = parseInt(value || fallback, 10);
|
|
|
|
|
+ return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), max) : fallback;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function generateId() {
|
|
function generateId() {
|
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
- let s = '';
|
|
|
|
|
- for (let i = 0; i < 10; i++) s += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
|
|
|
- return s;
|
|
|
|
|
|
|
+ let value = '';
|
|
|
|
|
+ for (let i = 0; i < 10; i += 1) value += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
|
|
|
+ return value;
|
|
|
}
|
|
}
|