CLOUD_FUNCTION_FINAL.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. async function handler(request, response) {
  2. console.log('🚀 执行高性能 SQL 统计 (完全体 - 最终修正版)...');
  3. try {
  4. let companyId = 'cDL6R1hgSi';
  5. if (request.company && request.company.id) companyId = request.company.id;
  6. else if (request.params && request.params.companyId) companyId = request.params.companyId;
  7. else if (request.body && request.body.companyId) companyId = request.body.companyId;
  8. // --- SQL 定义 ---
  9. const workloadSql = `
  10. SELECT
  11. u."objectId" as "id",
  12. u."name",
  13. COALESCE((u."data"->'tags'->'capacity'->>'weeklyProjects')::int, 3) as "weeklyCapacity",
  14. COUNT(DISTINCT pt."project") as "projectCount",
  15. COUNT(DISTINCT CASE WHEN p."deadline" < NOW() AND p."status" != '已完成' THEN p."objectId" END) as "overdueCount",
  16. SUM(CASE
  17. WHEN p."status" = '已完成' THEN 0
  18. ELSE ((CASE WHEN p."data"->>'projectType' = 'hard' THEN 2.0 ELSE 1.0 END) * (CASE WHEN p."deadline" < NOW() THEN 1.5 ELSE 1.0 END))
  19. END) as "weightedLoad"
  20. FROM "Profile" u
  21. LEFT JOIN "ProjectTeam" pt ON pt."profile" = u."objectId" AND pt."isDeleted" IS NOT TRUE
  22. LEFT JOIN "Project" p ON pt."project" = p."objectId" AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
  23. WHERE u."company" = $1 AND u."roleName" = '组员' AND u."isDeleted" IS NOT TRUE
  24. GROUP BY u."objectId", u."name", u."data"
  25. ORDER BY "weightedLoad" DESC
  26. `;
  27. const projectsSql = `
  28. SELECT
  29. p."objectId" as "id",
  30. p."title" as "name",
  31. p."status",
  32. p."currentStage",
  33. p."deadline",
  34. p."updatedAt",
  35. p."createdAt",
  36. p."data"->>'urgency' as "urgency",
  37. p."data"->>'projectType' as "type",
  38. p."data"->'phaseDeadlines' as "phaseDeadlines",
  39. p."date" as "projectDate",
  40. EXTRACT(DAY FROM (p."deadline" - NOW())) as "daysLeft",
  41. (
  42. SELECT string_agg(pr."name", ', ')
  43. FROM "ProjectTeam" pt
  44. JOIN "Profile" pr ON pt."profile" = pr."objectId"
  45. WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
  46. ) as "designerName",
  47. (
  48. SELECT array_agg(pt."profile")
  49. FROM "ProjectTeam" pt
  50. WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
  51. ) as "designerIds"
  52. FROM "Project" p
  53. WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
  54. ORDER BY p."updatedAt" DESC
  55. LIMIT 1000
  56. `;
  57. const spaceStatsSql = `
  58. WITH ActiveProjects AS (
  59. SELECT p."objectId"
  60. FROM "Project" p
  61. WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
  62. LIMIT 1000
  63. ),
  64. ProjectSpaces AS (
  65. SELECT
  66. p."objectId" as "spaceId",
  67. p."productName" as "spaceName",
  68. p."productType" as "spaceType",
  69. p."project" as "projectId"
  70. FROM "Product" p
  71. WHERE p."project" IN (SELECT "objectId" FROM ActiveProjects)
  72. AND (p."isDeleted" IS NULL OR p."isDeleted" = false)
  73. ),
  74. Deliverables AS (
  75. SELECT
  76. COALESCE(d."data"->>'spaceId', d."data"->>'productId') as "spaceId",
  77. COUNT(*) as "fileCount",
  78. SUM(CASE WHEN
  79. d."fileType" = 'delivery_white_model' OR
  80. d."data"->>'deliveryType' IN ('white_model', 'delivery_white_model')
  81. THEN 1 ELSE 0 END) as "whiteModelCount",
  82. SUM(CASE WHEN
  83. d."fileType" = 'delivery_soft_decor' OR
  84. d."data"->>'deliveryType' IN ('soft_decor', 'delivery_soft_decor')
  85. THEN 1 ELSE 0 END) as "softDecorCount",
  86. SUM(CASE WHEN
  87. d."fileType" = 'delivery_rendering' OR
  88. d."data"->>'deliveryType' IN ('rendering', 'delivery_rendering')
  89. THEN 1 ELSE 0 END) as "renderingCount",
  90. SUM(CASE WHEN
  91. d."fileType" = 'delivery_post_process' OR
  92. d."data"->>'deliveryType' IN ('post_process', 'delivery_post_process')
  93. THEN 1 ELSE 0 END) as "postProcessCount"
  94. FROM "ProjectFile" d
  95. WHERE d."project" IN (SELECT "objectId" FROM ActiveProjects)
  96. AND (d."isDeleted" IS NULL OR d."isDeleted" = false)
  97. AND (
  98. d."fileType" LIKE 'delivery_%' OR
  99. d."data"->>'uploadStage' = 'delivery'
  100. )
  101. GROUP BY COALESCE(d."data"->>'spaceId', d."data"->>'productId')
  102. )
  103. SELECT
  104. ps."projectId",
  105. ps."spaceId",
  106. ps."spaceName",
  107. ps."spaceType",
  108. COALESCE(d."fileCount", 0) as "totalFiles",
  109. COALESCE(d."whiteModelCount", 0) as "whiteModel",
  110. COALESCE(d."softDecorCount", 0) as "softDecor",
  111. COALESCE(d."renderingCount", 0) as "rendering",
  112. COALESCE(d."postProcessCount", 0) as "postProcess"
  113. FROM ProjectSpaces ps
  114. LEFT JOIN Deliverables d ON ps."spaceId" = d."spaceId"
  115. `;
  116. // 关键修正:通过 Project 表关联查询,获取更多字段
  117. const issuesSql = `
  118. SELECT
  119. i."objectId" as "id",
  120. i."title",
  121. i."description",
  122. i."priority",
  123. i."issueType",
  124. i."status",
  125. i."dueDate",
  126. i."createdAt",
  127. i."updatedAt",
  128. i."data",
  129. p."objectId" as "projectId",
  130. p."title" as "projectName",
  131. c."name" as "creatorName",
  132. a."name" as "assigneeName"
  133. FROM "ProjectIssue" i
  134. JOIN "Project" p ON i."project" = p."objectId"
  135. LEFT JOIN "Profile" c ON i."creator" = c."objectId"
  136. LEFT JOIN "Profile" a ON i."assignee" = a."objectId"
  137. WHERE p."company" = $1
  138. AND (i."isDeleted" IS NULL OR i."isDeleted" = false)
  139. AND i."status" IN ('待处理', '处理中')
  140. ORDER BY i."updatedAt" DESC
  141. LIMIT 50
  142. `;
  143. // --- 执行 SQL ---
  144. const [workloadResult, projectsResult, spaceStatsResult, issuesResult] = await Promise.all([
  145. Psql.query(workloadSql, [companyId]),
  146. Psql.query(projectsSql, [companyId]),
  147. Psql.query(spaceStatsSql, [companyId]),
  148. Psql.query(issuesSql, [companyId])
  149. ]);
  150. // --- 格式化数据 ---
  151. // 1. Workload
  152. const workload = workloadResult.map(w => {
  153. const capacity = w.weeklyCapacity || 3;
  154. const load = parseFloat(w.weightedLoad || 0);
  155. const loadRate = Math.round((load / capacity) * 100);
  156. let status = 'idle';
  157. if (loadRate > 80) status = 'overload';
  158. else if (loadRate > 50) status = 'busy';
  159. return {
  160. id: w.id,
  161. name: w.name,
  162. weeklyCapacity: capacity,
  163. projectCount: parseInt(w.projectCount),
  164. overdueCount: parseInt(w.overdueCount),
  165. weightedLoad: load,
  166. loadRate,
  167. status
  168. };
  169. });
  170. // 2. Projects
  171. const spaceAssigneeMap = {};
  172. const projects = projectsResult.map(p => {
  173. if (p.projectDate && p.projectDate.designerAssignmentStats) {
  174. const stats = p.projectDate.designerAssignmentStats;
  175. if (stats.projectLeader && stats.projectLeader.assignedSpaces) {
  176. stats.projectLeader.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = stats.projectLeader.name; });
  177. }
  178. if (Array.isArray(stats.teamMembers)) {
  179. stats.teamMembers.forEach(member => {
  180. if (member.assignedSpaces && member.name) {
  181. member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
  182. }
  183. });
  184. }
  185. if (Array.isArray(stats.crossTeamCollaborators)) {
  186. stats.crossTeamCollaborators.forEach(member => {
  187. if (member.assignedSpaces && member.name) {
  188. member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
  189. }
  190. });
  191. }
  192. }
  193. let statusStr = 'normal';
  194. const days = parseFloat(p.daysLeft);
  195. if (days < 0) statusStr = 'overdue';
  196. else if (days <= 3) statusStr = 'urgent';
  197. return {
  198. id: p.id,
  199. name: p.name,
  200. status: p.status,
  201. currentStage: p.currentStage,
  202. deadline: p.deadline,
  203. updatedAt: p.updatedAt,
  204. createdAt: p.createdAt,
  205. urgency: p.urgency,
  206. type: p.type,
  207. phaseDeadlines: p.phaseDeadlines || {},
  208. daysLeft: Math.ceil(days),
  209. isOverdue: days < 0,
  210. statusStr,
  211. designerName: p.designerName || '待分配',
  212. designerIds: p.designerIds || []
  213. };
  214. });
  215. // 3. Space Stats (完全修复聚合逻辑)
  216. const spaceStats = {};
  217. // 创建项目名称映射,确保能获取到 projectName
  218. const projectNameMap = {};
  219. projects.forEach(p => {
  220. projectNameMap[p.id] = p.name;
  221. });
  222. spaceStatsResult.forEach(row => {
  223. if (!spaceStats[row.projectId]) {
  224. spaceStats[row.projectId] = {
  225. spaces: []
  226. };
  227. }
  228. // 计算单个空间的完成度
  229. const hasFiles = parseInt(row.totalFiles) > 0;
  230. let completion = 0;
  231. if (hasFiles) {
  232. if (parseInt(row.whiteModel) > 0) completion += 25;
  233. if (parseInt(row.softDecor) > 0) completion += 25;
  234. if (parseInt(row.rendering) > 0) completion += 25;
  235. if (parseInt(row.postProcess) > 0) completion += 25;
  236. }
  237. const spaceInfo = {
  238. spaceId: row.spaceId,
  239. spaceName: row.spaceName,
  240. spaceType: row.spaceType,
  241. totalFiles: parseInt(row.totalFiles),
  242. deliverableTypes: {
  243. whiteModel: parseInt(row.whiteModel),
  244. softDecor: parseInt(row.softDecor),
  245. rendering: parseInt(row.rendering),
  246. postProcess: parseInt(row.postProcess)
  247. },
  248. hasDeliverables: hasFiles,
  249. completionRate: Math.min(100, completion)
  250. };
  251. spaceStats[row.projectId].spaces.push(spaceInfo);
  252. });
  253. Object.keys(spaceStats).forEach(pid => {
  254. const proj = spaceStats[pid];
  255. const totalSpaces = proj.spaces.length;
  256. // 计算整体完成率
  257. const sumCompletion = proj.spaces.reduce((sum, s) => sum + s.completionRate, 0);
  258. const overallCompletionRate = totalSpaces > 0 ? Math.round(sumCompletion / totalSpaces) : 0;
  259. const calcPhaseDetails = (typeKey) => {
  260. const spacesWithFile = proj.spaces.filter(s => s.deliverableTypes[typeKey] > 0);
  261. const completedCount = spacesWithFile.length;
  262. const rate = totalSpaces > 0 ? Math.round((completedCount / totalSpaces) * 100) : 0;
  263. const fileCount = proj.spaces.reduce((sum, s) => sum + s.deliverableTypes[typeKey], 0);
  264. const incomplete = proj.spaces
  265. .filter(s => s.deliverableTypes[typeKey] === 0)
  266. .map(s => ({
  267. spaceName: s.spaceName,
  268. assignee: spaceAssigneeMap[s.spaceId] || '未分配',
  269. spaceId: s.spaceId
  270. }));
  271. return {
  272. completionRate: rate,
  273. completedSpaces: completedCount,
  274. requiredSpaces: totalSpaces,
  275. totalFiles: fileCount,
  276. incompleteSpaces: incomplete
  277. };
  278. };
  279. const phaseProgress = {
  280. modeling: calcPhaseDetails('whiteModel'),
  281. softDecor: calcPhaseDetails('softDecor'),
  282. rendering: calcPhaseDetails('rendering'),
  283. postProcessing: calcPhaseDetails('postProcess')
  284. };
  285. // 关键:确保 projectName 和 totalByType 存在
  286. spaceStats[pid] = {
  287. projectId: pid,
  288. projectName: projectNameMap[pid] || '未命名项目',
  289. totalSpaces,
  290. spaces: proj.spaces,
  291. totalDeliverableFiles: proj.spaces.reduce((sum, s) => sum + s.totalFiles, 0),
  292. totalByType: {
  293. whiteModel: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.whiteModel, 0),
  294. softDecor: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.softDecor, 0),
  295. rendering: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.rendering, 0),
  296. postProcess: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.postProcess, 0)
  297. },
  298. overallCompletionRate,
  299. phaseProgress
  300. };
  301. });
  302. // 4. Issues (恢复完整字段)
  303. const zh2enStatus = (status) => {
  304. const map = {
  305. '待处理': 'open',
  306. '处理中': 'in_progress',
  307. '已解决': 'resolved',
  308. '已关闭': 'closed'
  309. };
  310. return map[status] || 'open';
  311. };
  312. const issues = issuesResult.map(row => ({
  313. id: row.id,
  314. title: row.title || (row.description ? row.description.slice(0, 40) : '未命名问题'),
  315. description: row.description,
  316. priority: row.priority || 'medium',
  317. type: row.issueType || 'task',
  318. status: zh2enStatus(row.status),
  319. projectId: row.projectId || '',
  320. projectName: row.projectName || '未知项目',
  321. relatedSpace: row.data?.relatedSpace,
  322. relatedStage: row.data?.relatedStage,
  323. assigneeName: row.assigneeName || '未指派',
  324. creatorName: row.creatorName || '未知',
  325. createdAt: row.createdAt,
  326. updatedAt: row.updatedAt,
  327. dueDate: row.dueDate,
  328. tags: row.data?.tags || []
  329. }));
  330. // 5. Stats
  331. const stats = {
  332. totalActive: projects.length,
  333. overdueCount: projects.filter(p => p.isOverdue).length,
  334. urgentCount: projects.filter(p => p.statusStr === 'urgent').length,
  335. avgLoadRate: workload.length > 0 ? Math.round(workload.reduce((sum, w) => sum + w.loadRate, 0) / workload.length) : 0
  336. };
  337. response.json({
  338. code: 200,
  339. success: true,
  340. data: { stats, workload, projects, spaceStats, issues }
  341. });
  342. } catch (error) {
  343. console.error('❌ SQL 执行失败:', error.message);
  344. response.json({ code: 500, success: false, error: error.message });
  345. }
  346. }