13-douyinInsightManager.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /**
  2. * 云函数:douyinInsightManager(二阶段:爆款分析、选题池、日报)
  3. * actions:
  4. * analysisCreate | analysisList | analysisGet | analysisUpdate
  5. * topicCreate | topicList | topicUpdate | topicArchive
  6. * dailyReportCreate | dailyReportList | dailyReportGet
  7. *
  8. * 说明:本函数只负责业务资产的持久化和账号隔离。抖音原始数据抓取仍由 12-douyinManager 负责。
  9. */
  10. const PARSE_API_HOST = readEnv('PARSE_API_HOST') || 'https://server.fmode.cn';
  11. const PARSE_APP_ID = readEnv('PARSE_APP_ID') || 'ncloudmaster';
  12. async function handler(request, response) {
  13. try {
  14. await ensureTables();
  15. const action = pickParam(request, 'action') || '';
  16. const session = await requireSession(request);
  17. const requestedUserId = pickParam(request, 'userId') || '';
  18. if (requestedUserId && requestedUserId !== session.userId) {
  19. return response.json({ code: 403, success: false, error: '没有访问该账号数据的权限' });
  20. }
  21. const userId = session.userId;
  22. if (action === 'analysisCreate') return createRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'analysis', 'data') || {});
  23. if (action === 'analysisList') return listRows(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'limit') || 100);
  24. if (action === 'analysisGet') return getRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'id'));
  25. if (action === 'analysisUpdate') return updateRow(response, 'VideoflowViralAnalysis', userId, pickParam(request, 'id'), pickParam(request, 'patch', 'analysis', 'data') || {});
  26. if (action === 'topicCreate') return createRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'topic', 'data') || {});
  27. if (action === 'topicList') return listRows(response, 'VideoflowTopicIdea', userId, pickParam(request, 'limit') || 500);
  28. if (action === 'topicUpdate') return updateRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'id'), pickParam(request, 'patch', 'topic', 'data') || {});
  29. if (action === 'topicArchive') return updateRow(response, 'VideoflowTopicIdea', userId, pickParam(request, 'id'), { status: 'archived' });
  30. if (action === 'dailyReportCreate') return createRow(response, 'VideoflowDailyReport', userId, pickParam(request, 'report', 'data') || {});
  31. if (action === 'dailyReportList') return listRows(response, 'VideoflowDailyReport', userId, pickParam(request, 'limit') || 100);
  32. if (action === 'dailyReportGet') return getRow(response, 'VideoflowDailyReport', userId, pickParam(request, 'id'));
  33. response.json({ code: 400, success: false, error: `未知 action: ${action}` });
  34. } catch (error) {
  35. console.error('douyinInsightManager failed:', error.message);
  36. response.json({ code: 500, success: false, error: error.message });
  37. }
  38. }
  39. async function ensureTables() {
  40. for (const table of ['VideoflowViralAnalysis', 'VideoflowTopicIdea', 'VideoflowDailyReport']) {
  41. await Psql.query(`
  42. CREATE TABLE IF NOT EXISTS "${table}" (
  43. "objectId" VARCHAR(50) PRIMARY KEY,
  44. "bizId" VARCHAR(255) NOT NULL,
  45. "userId" VARCHAR(255) NOT NULL,
  46. "data" JSONB NOT NULL DEFAULT '{}',
  47. "status" VARCHAR(50) DEFAULT '',
  48. "createdAt" TIMESTAMPTZ DEFAULT NOW(),
  49. "updatedAt" TIMESTAMPTZ DEFAULT NOW()
  50. )
  51. `);
  52. await Psql.query(`DROP INDEX IF EXISTS idx_${table.toLowerCase()}_biz`);
  53. await Psql.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${table.toLowerCase()}_user_biz ON "${table}" ("userId", "bizId")`);
  54. await Psql.query(`CREATE INDEX IF NOT EXISTS idx_${table.toLowerCase()}_user ON "${table}" ("userId")`);
  55. }
  56. await Psql.query(`
  57. CREATE TABLE IF NOT EXISTS "AppSession" (
  58. "token" VARCHAR(120) PRIMARY KEY,
  59. "userId" VARCHAR(50) NOT NULL,
  60. "expiresAt" TIMESTAMPTZ NOT NULL,
  61. "createdAt" TIMESTAMPTZ DEFAULT NOW()
  62. )
  63. `);
  64. }
  65. async function createRow(response, table, userId, data) {
  66. const now = new Date().toISOString();
  67. const bizId = data.id || data.bizId || generateId();
  68. const merged = { ...data, id: bizId, userId, createdAt: data.createdAt || now, updatedAt: now };
  69. const existing = await Psql.query(
  70. `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
  71. [bizId, userId]
  72. );
  73. if (existing.length) {
  74. await Psql.query(
  75. `UPDATE "${table}" SET "data"=$1, "status"=$2, "updatedAt"=NOW() WHERE "bizId"=$3 AND "userId"=$4`,
  76. [JSON.stringify(merged), merged.status || '', bizId, userId]
  77. );
  78. return response.json({ code: 200, success: true, data: merged });
  79. }
  80. await Psql.query(
  81. `INSERT INTO "${table}" ("objectId","bizId","userId","data","status")
  82. VALUES ($1,$2,$3,$4,$5)`,
  83. [generateId(), bizId, userId, JSON.stringify(merged), merged.status || '']
  84. );
  85. response.json({ code: 200, success: true, data: merged });
  86. }
  87. async function listRows(response, table, userId, limit) {
  88. const parsedLimit = parseInt(limit || '100', 10);
  89. const safeLimit = Number.isFinite(parsedLimit) ? Math.min(Math.max(parsedLimit, 1), 1000) : 100;
  90. const rows = await Psql.query(
  91. `SELECT * FROM "${table}" WHERE "userId"=$1 ORDER BY "updatedAt" DESC LIMIT $2`,
  92. [userId, safeLimit]
  93. );
  94. response.json({ code: 200, success: true, data: rows.map(rowToObj) });
  95. }
  96. async function getRow(response, table, userId, id) {
  97. if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
  98. const rows = await Psql.query(
  99. `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
  100. [id, userId]
  101. );
  102. if (!rows.length) return response.json({ code: 404, success: false, error: '未找到记录' });
  103. response.json({ code: 200, success: true, data: rowToObj(rows[0]) });
  104. }
  105. async function updateRow(response, table, userId, id, patch) {
  106. if (!id) return response.json({ code: 400, success: false, error: '缺少 id' });
  107. const rows = await Psql.query(
  108. `SELECT * FROM "${table}" WHERE "bizId"=$1 AND "userId"=$2 LIMIT 1`,
  109. [id, userId]
  110. );
  111. if (!rows.length) return response.json({ code: 404, success: false, error: '未找到记录' });
  112. const merged = { ...rowToObj(rows[0]), ...patch, id, userId, updatedAt: new Date().toISOString() };
  113. await Psql.query(
  114. `UPDATE "${table}" SET "data"=$1, "status"=$2, "updatedAt"=NOW() WHERE "bizId"=$3 AND "userId"=$4`,
  115. [JSON.stringify(merged), merged.status || '', id, userId]
  116. );
  117. response.json({ code: 200, success: true, data: merged });
  118. }
  119. function rowToObj(row) {
  120. const data = typeof row.data === 'string' ? JSON.parse(row.data) : (row.data || {});
  121. return { ...data, objectId: row.objectId, createdAt: row.createdAt, updatedAt: row.updatedAt };
  122. }
  123. function pickParam(request, ...names) {
  124. const sources = [request.params, request.body, request];
  125. for (const src of sources) {
  126. if (!src || typeof src !== 'object') continue;
  127. for (const name of names) {
  128. const value = src[name];
  129. if (value !== undefined && value !== null && value !== '') return value;
  130. }
  131. }
  132. return null;
  133. }
  134. async function requireSession(request) {
  135. const token = clean(pickParam(request, 'sessionToken'));
  136. if (!token) throw new Error('请先登录');
  137. const rows = await Psql.query(
  138. `SELECT * FROM "AppSession" WHERE "token"=$1 AND "expiresAt" > NOW() LIMIT 1`,
  139. [token]
  140. );
  141. if (rows.length) return rows[0];
  142. const parseUser = await verifyParseSession(token);
  143. if (parseUser?.objectId) return { token, userId: parseUser.objectId, source: 'parse' };
  144. throw new Error('登录已过期,请重新登录');
  145. }
  146. async function verifyParseSession(sessionToken) {
  147. if (typeof fetch !== 'function') return null;
  148. const resp = await fetch(`${PARSE_API_HOST}/parse/users/me?include=company`, {
  149. method: 'GET',
  150. headers: {
  151. 'X-Parse-Application-Id': PARSE_APP_ID,
  152. 'X-Parse-Session-Token': sessionToken,
  153. },
  154. });
  155. const data = await resp.json().catch(() => ({}));
  156. return resp.ok && data.objectId ? data : null;
  157. }
  158. function clean(value) {
  159. return String(value || '').trim();
  160. }
  161. function generateId() {
  162. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  163. let s = '';
  164. for (let i = 0; i < 10; i++) s += chars.charAt(Math.floor(Math.random() * chars.length));
  165. return s;
  166. }
  167. function readEnv(name) {
  168. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  169. return process.env[name];
  170. }
  171. return '';
  172. }