| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- /**
- * 云函数:douyinInsightManager(二阶段:爆款分析、选题池、日报)
- * actions:
- * analysisCreate | analysisList | analysisGet | analysisUpdate
- * topicCreate | topicList | topicUpdate | topicArchive
- * dailyReportCreate | dailyReportList | dailyReportGet
- *
- * 说明:本函数只负责业务资产的持久化和账号隔离。抖音原始数据抓取仍由 12-douyinManager 负责。
- */
- const PARSE_API_HOST = readEnv('PARSE_API_HOST') || 'https://server.fmode.cn';
- const PARSE_APP_ID = readEnv('PARSE_APP_ID') || 'ncloudmaster';
- async function handler(request, response) {
- try {
- await ensureTables();
- const action = pickParam(request, 'action') || '';
- const session = await requireSession(request);
- const requestedUserId = pickParam(request, 'userId') || '';
- if (requestedUserId && requestedUserId !== session.userId) {
- return response.json({ code: 403, success: false, error: '没有访问该账号数据的权限' });
- }
- const userId = session.userId;
- if (action === 'analysisCreate') return createRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'analysis', 'data') || {});
- if (action === 'analysisList') return listRows(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'limit') || 100);
- if (action === 'analysisGet') return getRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'id'));
- if (action === 'analysisUpdate') return updateRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'id'), pickParam(request, 'patch', 'analysis', 'data') || {});
- if (action === 'topicCreate') return createRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'topic', 'data') || {});
- if (action === 'topicList') return listRows(response, 'VideoflowTopicIdea', userId, pickParam(request, 'limit') || 500);
- if (action === 'topicUpdate') return updateRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'id'), pickParam(request, 'patch', 'topic', 'data') || {});
- if (action === 'topicArchive') return updateRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'id'), { status: 'archived' });
- if (action === 'dailyReportCreate') return createRow(response, 'VideoflowDailyReport', userId, pickParam(request, 'report', 'data') || {});
- if (action === 'dailyReportList') return listRows(response, 'VideoflowDailyReport', userId, pickParam(request, 'limit') || 100);
- if (action === 'dailyReportGet') return getRow(response, 'VideoflowDailyReport', userId, pickParam(request, 'id'));
- response.json({ code: 400, success: false, error: `未知 action: ${action}` });
- } catch (error) {
- console.error('douyinInsightManager failed:', error.message);
- response.json({ code: 500, success: false, error: error.message });
- }
- }
- async function ensureTables() {
- for (const table of ['VideoflowViralAnalysis', 'VideoflowTopicIdea', 'VideoflowDailyReport']) {
- await Psql.query(`
- CREATE TABLE IF NOT EXISTS "${table}" (
- "objectId" VARCHAR(50) PRIMARY KEY,
- "bizId" VARCHAR(255) NOT NULL,
- "userId" VARCHAR(255) NOT NULL,
- "data" JSONB NOT NULL DEFAULT '{}',
- "status" VARCHAR(50) DEFAULT '',
- "createdAt" TIMESTAMPTZ DEFAULT NOW(),
- "updatedAt" TIMESTAMPTZ DEFAULT NOW()
- )
- `);
- await Psql.query(`DROP INDEX IF EXISTS idx_${table.toLowerCase()}_biz`);
- await Psql.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${table.toLowerCase()}_user_biz ON "${table}" ("userId", "bizId")`);
- await Psql.query(`CREATE INDEX IF NOT EXISTS idx_${table.toLowerCase()}_user ON "${table}" ("userId")`);
- }
- await Psql.query(`
- CREATE TABLE IF NOT EXISTS "AppSession" (
- "token" VARCHAR(120) PRIMARY KEY,
- "userId" VARCHAR(50) NOT NULL,
- "expiresAt" TIMESTAMPTZ NOT NULL,
- "createdAt" TIMESTAMPTZ DEFAULT NOW()
- )
- `);
- }
- async function createRow(response, table, userId, data) {
- const now = new Date().toISOString();
- const bizId = data.id || data.bizId || generateId();
- const merged = { ...data, id: bizId, userId, createdAt: data.createdAt || now, updatedAt: now };
- const existing = await Psql.query(
- `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
- [bizId, userId]
- );
- if (existing.length) {
- await Psql.query(
- `UPDATE "${table}" SET "data"=$1, "status"=$2, "updatedAt"=NOW() WHERE "bizId"=$3 AND "userId"=$4`,
- [JSON.stringify(merged), merged.status || '', bizId, userId]
- );
- return response.json({ code: 200, success: true, data: merged });
- }
- await Psql.query(
- `INSERT INTO "${table}" ("objectId","bizId","userId","data","status")
- VALUES ($1,$2,$3,$4,$5)`,
- [generateId(), bizId, userId, JSON.stringify(merged), merged.status || '']
- );
- response.json({ code: 200, success: true, data: merged });
- }
- async function listRows(response, table, userId, limit) {
- const parsedLimit = parseInt(limit || '100', 10);
- const safeLimit = Number.isFinite(parsedLimit) ? Math.min(Math.max(parsedLimit, 1), 1000) : 100;
- const rows = await Psql.query(
- `SELECT * FROM "${table}" WHERE "userId"=$1 ORDER BY "updatedAt" DESC LIMIT $2`,
- [userId, safeLimit]
- );
- response.json({ code: 200, success: true, data: rows.map(rowToObj) });
- }
- async function getRow(response, table, userId, id) {
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
- const rows = await Psql.query(
- `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
- [id, userId]
- );
- if (!rows.length) return response.json({ code: 404, success: false, error: '未找到记录' });
- response.json({ code: 200, success: true, data: rowToObj(rows[0]) });
- }
- async function updateRow(response, table, userId, id, patch) {
- if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
- const rows = await Psql.query(
- `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
- [id, userId]
- );
- if (!rows.length) return response.json({ code: 404, success: false, error: '未找到记录' });
- const merged = { ...rowToObj(rows[0]), ...patch, id, userId, updatedAt: new Date().toISOString() };
- await Psql.query(
- `UPDATE "${table}" SET "data"=$1, "status"=$2, "updatedAt"=NOW() WHERE "bizId"=$3 AND "userId"=$4`,
- [JSON.stringify(merged), merged.status || '', id, userId]
- );
- response.json({ code: 200, success: true, data: merged });
- }
- 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];
- 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;
- }
- async function requireSession(request) {
- const token = clean(pickParam(request, 'sessionToken'));
- if (!token) throw new Error('请先登录');
- const rows = await Psql.query(
- `SELECT * FROM "AppSession" WHERE "token"=$1 AND "expiresAt" > NOW() LIMIT 1`,
- [token]
- );
- if (rows.length) return rows[0];
- const parseUser = await verifyParseSession(token);
- if (parseUser?.objectId) return { token, userId: parseUser.objectId, source: 'parse' };
- throw new Error('登录已过期,请重新登录');
- }
- async function verifyParseSession(sessionToken) {
- if (typeof fetch !== 'function') return null;
- const resp = await fetch(`${PARSE_API_HOST}/parse/users/me?include=company`, {
- method: 'GET',
- headers: {
- 'X-Parse-Application-Id': PARSE_APP_ID,
- 'X-Parse-Session-Token': sessionToken,
- },
- });
- const data = await resp.json().catch(() => ({}));
- return resp.ok && data.objectId ? data : null;
- }
- function clean(value) {
- return String(value || '').trim();
- }
- function generateId() {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- let s = '';
- for (let i = 0; i < 10; i++) s += chars.charAt(Math.floor(Math.random() * chars.length));
- return s;
- }
- function readEnv(name) {
- if (typeof process !== 'undefined' && process.env && process.env[name]) {
- return process.env[name];
- }
- return '';
- }
|