project-load.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * 组长看板数据云函数 - 支持时间范围筛选
  3. *
  4. * 请求参数:
  5. * - companyId: 公司ID
  6. * - baseDate: 基准日期 (可选,格式: YYYY-MM-DD,默认为当前日期)
  7. * - startDate: 开始日期 (可选,覆盖 baseDate 计算)
  8. * - endDate: 结束日期 (可选,覆盖 baseDate 计算)
  9. *
  10. * 时间范围逻辑:
  11. * - 如果不传时间参数,默认查询当前进行中的项目
  12. * - 如果传 baseDate,则以该日期为基准,查询该月的项目
  13. * - 如果传 startDate/endDate,则使用指定的日期范围
  14. */
  15. async function handler(request, response) {
  16. console.log('🚀 执行高性能 SQL 统计 (时间范围筛选版)...');
  17. try {
  18. // 获取公司ID
  19. let companyId = 'cDL6R1hgSi';
  20. if (request.company && request.company.id) companyId = request.company.id;
  21. else if (request.params && request.params.companyId) companyId = request.params.companyId;
  22. else if (request.body && request.body.companyId) companyId = request.body.companyId;
  23. // 获取时间参数
  24. let baseDate = request.body?.baseDate || request.params?.baseDate || null;
  25. let startDate = request.body?.startDate || request.params?.startDate || null;
  26. let endDate = request.body?.endDate || request.params?.endDate || null;
  27. // 计算时间范围
  28. const now = new Date();
  29. let rangeStart, rangeEnd;
  30. if (startDate && endDate) {
  31. // 使用指定范围
  32. rangeStart = new Date(startDate);
  33. rangeEnd = new Date(endDate);
  34. } else if (baseDate) {
  35. // 基于 baseDate 计算该月的范围
  36. const base = new Date(baseDate);
  37. rangeStart = new Date(base.getFullYear(), base.getMonth(), 1); // 月初
  38. rangeEnd = new Date(base.getFullYear(), base.getMonth() + 1, 0, 23, 59, 59); // 月末
  39. } else {
  40. // 默认:查询进行中的项目(不限制时间范围,或使用较宽的时间窗口)
  41. rangeStart = new Date(now.getFullYear() - 1, 0, 1); // 一年前
  42. rangeEnd = new Date(now.getFullYear() + 1, 11, 31); // 一年后
  43. }
  44. console.log(`📅 查询时间范围: ${rangeStart.toISOString()} ~ ${rangeEnd.toISOString()}`);
  45. // --- SQL 定义 (性能优化版) ---
  46. // 1. Workload 查询 - 保持不变,已经够高效
  47. const workloadSql = `
  48. SELECT
  49. u."objectId" as "id",
  50. u."name",
  51. COALESCE((u."data"->'tags'->'capacity'->>'weeklyProjects')::int, 3) as "weeklyCapacity",
  52. COUNT(DISTINCT pt."project") as "projectCount",
  53. COUNT(DISTINCT CASE WHEN p."deadline" < NOW() AND p."status" != '已完成' THEN p."objectId" END) as "overdueCount",
  54. SUM(CASE
  55. WHEN p."status" = '已完成' THEN 0
  56. 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))
  57. END) as "weightedLoad"
  58. FROM "Profile" u
  59. LEFT JOIN "ProjectTeam" pt ON pt."profile" = u."objectId" AND pt."isDeleted" IS NOT TRUE
  60. LEFT JOIN "Project" p ON pt."project" = p."objectId" AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
  61. WHERE u."company" = $1 AND u."roleName" = '组员' AND u."isDeleted" IS NOT TRUE
  62. GROUP BY u."objectId", u."name", u."data"
  63. ORDER BY "weightedLoad" DESC
  64. `;
  65. // 2. 项目查询 - 优化:分离子查询,先查项目基础信息
  66. const projectsSql = `
  67. SELECT
  68. p."objectId" as "id",
  69. p."title" as "name",
  70. p."status",
  71. p."currentStage",
  72. p."deadline",
  73. p."updatedAt",
  74. p."createdAt",
  75. p."data",
  76. p."date" as "projectDate",
  77. EXTRACT(DAY FROM (p."deadline" - NOW())) as "daysLeft"
  78. FROM "Project" p
  79. WHERE p."company" = $1
  80. AND p."isDeleted" IS NOT TRUE
  81. AND p."status" != '已完成'
  82. AND (
  83. p."deadline" >= $2 AND p."deadline" <= $3
  84. OR p."createdAt" >= $2 AND p."createdAt" <= $3
  85. OR p."createdAt" < $2 AND p."deadline" > $3
  86. OR p."deadline" IS NULL
  87. )
  88. ORDER BY p."updatedAt" DESC
  89. LIMIT 500
  90. `;
  91. // 2.1 项目团队成员查询 - 一次性获取所有相关项目的团队成员
  92. const projectTeamsSql = `
  93. SELECT
  94. pt."project" as "projectId",
  95. string_agg(pr."name", ', ' ORDER BY pr."name") as "designerName",
  96. array_agg(pt."profile") as "designerIds"
  97. FROM "ProjectTeam" pt
  98. JOIN "Profile" pr ON pt."profile" = pr."objectId"
  99. WHERE pt."isDeleted" IS NOT TRUE
  100. AND pt."project" IN (
  101. SELECT p."objectId"
  102. FROM "Project" p
  103. WHERE p."company" = $1
  104. AND p."isDeleted" IS NOT TRUE
  105. AND p."status" != '已完成'
  106. AND (
  107. p."deadline" >= $2 AND p."deadline" <= $3
  108. OR p."createdAt" >= $2 AND p."createdAt" <= $3
  109. OR p."createdAt" < $2 AND p."deadline" > $3
  110. OR p."deadline" IS NULL
  111. )
  112. )
  113. GROUP BY pt."project"
  114. `;
  115. // 3. Space Stats - 优化:简化查询结构
  116. const spaceStatsSql = `
  117. WITH ActiveProjectIds AS (
  118. SELECT p."objectId"
  119. FROM "Project" p
  120. WHERE p."company" = $1
  121. AND p."isDeleted" IS NOT TRUE
  122. AND p."status" != '已完成'
  123. AND (
  124. p."deadline" >= $2 AND p."deadline" <= $3
  125. OR p."createdAt" >= $2 AND p."createdAt" <= $3
  126. OR p."createdAt" < $2 AND p."deadline" > $3
  127. OR p."deadline" IS NULL
  128. )
  129. LIMIT 500
  130. ),
  131. ProjectSpaces AS (
  132. SELECT
  133. prod."objectId" as "spaceId",
  134. prod."productName" as "spaceName",
  135. prod."productType" as "spaceType",
  136. prod."project" as "projectId"
  137. FROM "Product" prod
  138. WHERE prod."project" IN (SELECT "objectId" FROM ActiveProjectIds)
  139. AND (prod."isDeleted" IS NULL OR prod."isDeleted" = false)
  140. ),
  141. Deliverables AS (
  142. SELECT
  143. COALESCE(pf."data"->>'spaceId', pf."data"->>'productId') as "spaceId",
  144. COUNT(*) as "fileCount",
  145. SUM(CASE WHEN pf."fileType" = 'delivery_white_model'
  146. OR pf."data"->>'deliveryType' IN ('white_model', 'delivery_white_model')
  147. THEN 1 ELSE 0 END) as "whiteModelCount",
  148. SUM(CASE WHEN pf."fileType" = 'delivery_soft_decor'
  149. OR pf."data"->>'deliveryType' IN ('soft_decor', 'delivery_soft_decor')
  150. THEN 1 ELSE 0 END) as "softDecorCount",
  151. SUM(CASE WHEN pf."fileType" = 'delivery_rendering'
  152. OR pf."data"->>'deliveryType' IN ('rendering', 'delivery_rendering')
  153. THEN 1 ELSE 0 END) as "renderingCount",
  154. SUM(CASE WHEN pf."fileType" = 'delivery_post_process'
  155. OR pf."data"->>'deliveryType' IN ('post_process', 'delivery_post_process')
  156. THEN 1 ELSE 0 END) as "postProcessCount"
  157. FROM "ProjectFile" pf
  158. WHERE pf."project" IN (SELECT "objectId" FROM ActiveProjectIds)
  159. AND (pf."isDeleted" IS NULL OR pf."isDeleted" = false)
  160. AND (pf."fileType" LIKE 'delivery_%' OR pf."data"->>'uploadStage' = 'delivery')
  161. GROUP BY COALESCE(pf."data"->>'spaceId', pf."data"->>'productId')
  162. )
  163. SELECT
  164. ps."projectId",
  165. ps."spaceId",
  166. ps."spaceName",
  167. ps."spaceType",
  168. COALESCE(d."fileCount", 0) as "totalFiles",
  169. COALESCE(d."whiteModelCount", 0) as "whiteModel",
  170. COALESCE(d."softDecorCount", 0) as "softDecor",
  171. COALESCE(d."renderingCount", 0) as "rendering",
  172. COALESCE(d."postProcessCount", 0) as "postProcess"
  173. FROM ProjectSpaces ps
  174. LEFT JOIN Deliverables d ON ps."spaceId" = d."spaceId"
  175. `;
  176. // Issue 查询 - 添加时间范围筛选
  177. const issuesSql = `
  178. SELECT
  179. i."objectId" as "id",
  180. i."title",
  181. i."description",
  182. i."priority",
  183. i."issueType",
  184. i."status",
  185. i."dueDate",
  186. i."createdAt",
  187. i."updatedAt",
  188. i."data",
  189. p."objectId" as "projectId",
  190. p."title" as "projectName",
  191. c."name" as "creatorName",
  192. a."name" as "assigneeName"
  193. FROM "ProjectIssue" i
  194. JOIN "Project" p ON i."project" = p."objectId"
  195. LEFT JOIN "Profile" c ON i."creator" = c."objectId"
  196. LEFT JOIN "Profile" a ON i."assignee" = a."objectId"
  197. WHERE p."company" = $1
  198. AND (i."isDeleted" IS NULL OR i."isDeleted" = false)
  199. AND i."status" IN ('待处理', '处理中')
  200. AND (
  201. i."createdAt" >= $2 OR i."dueDate" >= $2 OR i."dueDate" IS NULL
  202. )
  203. ORDER BY i."updatedAt" DESC
  204. LIMIT 50
  205. `;
  206. // --- 执行 SQL (并行优化) ---
  207. const queryParams = [companyId, rangeStart, rangeEnd];
  208. const [workloadResult, projectsResult, projectTeamsResult, spaceStatsResult, issuesResult] = await Promise.all([
  209. Psql.query(workloadSql, [companyId]),
  210. Psql.query(projectsSql, queryParams),
  211. Psql.query(projectTeamsSql, queryParams),
  212. Psql.query(spaceStatsSql, queryParams),
  213. Psql.query(issuesSql, queryParams)
  214. ]);
  215. // 构建项目团队成员映射
  216. const projectTeamsMap = {};
  217. projectTeamsResult.forEach(row => {
  218. projectTeamsMap[row.projectId] = {
  219. designerName: row.designerName || '待分配',
  220. designerIds: row.designerIds || []
  221. };
  222. });
  223. // --- 格式化数据 ---
  224. // 1. Workload
  225. const workload = workloadResult.map(w => {
  226. const capacity = w.weeklyCapacity || 3;
  227. const load = parseFloat(w.weightedLoad || 0);
  228. const loadRate = Math.round((load / capacity) * 100);
  229. let status = 'idle';
  230. if (loadRate > 80) status = 'overload';
  231. else if (loadRate > 50) status = 'busy';
  232. return {
  233. id: w.id,
  234. name: w.name,
  235. weeklyCapacity: capacity,
  236. projectCount: parseInt(w.projectCount),
  237. overdueCount: parseInt(w.overdueCount),
  238. weightedLoad: load,
  239. loadRate,
  240. status
  241. };
  242. });
  243. // 2. Projects
  244. const spaceAssigneeMap = {};
  245. const projects = projectsResult.map(p => {
  246. // 从映射中获取设计师信息
  247. const teamInfo = projectTeamsMap[p.id] || { designerName: '待分配', designerIds: [] };
  248. // 解析设计师分配信息
  249. if (p.projectDate && p.projectDate.designerAssignmentStats) {
  250. const stats = p.projectDate.designerAssignmentStats;
  251. if (stats.projectLeader && stats.projectLeader.assignedSpaces) {
  252. stats.projectLeader.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = stats.projectLeader.name; });
  253. }
  254. if (Array.isArray(stats.teamMembers)) {
  255. stats.teamMembers.forEach(member => {
  256. if (member.assignedSpaces && member.name) {
  257. member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
  258. }
  259. });
  260. }
  261. if (Array.isArray(stats.crossTeamCollaborators)) {
  262. stats.crossTeamCollaborators.forEach(member => {
  263. if (member.assignedSpaces && member.name) {
  264. member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
  265. }
  266. });
  267. }
  268. }
  269. let statusStr = 'normal';
  270. const days = parseFloat(p.daysLeft);
  271. if (days < 0) statusStr = 'overdue';
  272. else if (days <= 3) statusStr = 'urgent';
  273. // 从 data 字段提取扩展属性
  274. const data = p.data || {};
  275. return {
  276. id: p.id,
  277. name: p.name,
  278. status: p.status,
  279. currentStage: p.currentStage,
  280. deadline: p.deadline,
  281. updatedAt: p.updatedAt,
  282. createdAt: p.createdAt,
  283. urgency: data.urgency,
  284. type: data.projectType,
  285. phaseDeadlines: data.phaseDeadlines || {},
  286. daysLeft: Math.ceil(days),
  287. isOverdue: days < 0,
  288. statusStr,
  289. designerName: teamInfo.designerName,
  290. designerIds: teamInfo.designerIds,
  291. // 扩展字段
  292. data: data
  293. };
  294. });
  295. // 3. Space Stats (完全修复聚合逻辑)
  296. const spaceStats = {};
  297. // 创建项目名称映射
  298. const projectNameMap = {};
  299. projects.forEach(p => {
  300. projectNameMap[p.id] = p.name;
  301. });
  302. spaceStatsResult.forEach(row => {
  303. if (!spaceStats[row.projectId]) {
  304. spaceStats[row.projectId] = {
  305. spaces: []
  306. };
  307. }
  308. // 计算单个空间的完成度
  309. const hasFiles = parseInt(row.totalFiles) > 0;
  310. let completion = 0;
  311. if (hasFiles) {
  312. if (parseInt(row.whiteModel) > 0) completion += 25;
  313. if (parseInt(row.softDecor) > 0) completion += 25;
  314. if (parseInt(row.rendering) > 0) completion += 25;
  315. if (parseInt(row.postProcess) > 0) completion += 25;
  316. }
  317. const spaceInfo = {
  318. spaceId: row.spaceId,
  319. spaceName: row.spaceName,
  320. spaceType: row.spaceType,
  321. totalFiles: parseInt(row.totalFiles),
  322. deliverableTypes: {
  323. whiteModel: parseInt(row.whiteModel),
  324. softDecor: parseInt(row.softDecor),
  325. rendering: parseInt(row.rendering),
  326. postProcess: parseInt(row.postProcess)
  327. },
  328. hasDeliverables: hasFiles,
  329. completionRate: Math.min(100, completion)
  330. };
  331. spaceStats[row.projectId].spaces.push(spaceInfo);
  332. });
  333. Object.keys(spaceStats).forEach(pid => {
  334. const proj = spaceStats[pid];
  335. const totalSpaces = proj.spaces.length;
  336. // 计算整体完成率
  337. const sumCompletion = proj.spaces.reduce((sum, s) => sum + s.completionRate, 0);
  338. const overallCompletionRate = totalSpaces > 0 ? Math.round(sumCompletion / totalSpaces) : 0;
  339. const calcPhaseDetails = (typeKey) => {
  340. const spacesWithFile = proj.spaces.filter(s => s.deliverableTypes[typeKey] > 0);
  341. const completedCount = spacesWithFile.length;
  342. const rate = totalSpaces > 0 ? Math.round((completedCount / totalSpaces) * 100) : 0;
  343. const fileCount = proj.spaces.reduce((sum, s) => sum + s.deliverableTypes[typeKey], 0);
  344. const incomplete = proj.spaces
  345. .filter(s => s.deliverableTypes[typeKey] === 0)
  346. .map(s => ({
  347. spaceName: s.spaceName,
  348. assignee: spaceAssigneeMap[s.spaceId] || '未分配',
  349. spaceId: s.spaceId
  350. }));
  351. return {
  352. completionRate: rate,
  353. completedSpaces: completedCount,
  354. requiredSpaces: totalSpaces,
  355. totalFiles: fileCount,
  356. incompleteSpaces: incomplete
  357. };
  358. };
  359. const phaseProgress = {
  360. modeling: calcPhaseDetails('whiteModel'),
  361. softDecor: calcPhaseDetails('softDecor'),
  362. rendering: calcPhaseDetails('rendering'),
  363. postProcessing: calcPhaseDetails('postProcess')
  364. };
  365. spaceStats[pid] = {
  366. projectId: pid,
  367. projectName: projectNameMap[pid] || '未命名项目',
  368. totalSpaces,
  369. spaces: proj.spaces,
  370. totalDeliverableFiles: proj.spaces.reduce((sum, s) => sum + s.totalFiles, 0),
  371. totalByType: {
  372. whiteModel: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.whiteModel, 0),
  373. softDecor: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.softDecor, 0),
  374. rendering: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.rendering, 0),
  375. postProcess: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.postProcess, 0)
  376. },
  377. overallCompletionRate,
  378. phaseProgress
  379. };
  380. });
  381. // 4. Issues
  382. const zh2enStatus = (status) => {
  383. const map = {
  384. '待处理': 'open',
  385. '处理中': 'in_progress',
  386. '已解决': 'resolved',
  387. '已关闭': 'closed'
  388. };
  389. return map[status] || 'open';
  390. };
  391. const issues = issuesResult.map(row => ({
  392. id: row.id,
  393. title: row.title || (row.description ? row.description.slice(0, 40) : '未命名问题'),
  394. description: row.description,
  395. priority: row.priority || 'medium',
  396. type: row.issueType || 'task',
  397. status: zh2enStatus(row.status),
  398. projectId: row.projectId || '',
  399. projectName: row.projectName || '未知项目',
  400. relatedSpace: row.data?.relatedSpace,
  401. relatedStage: row.data?.relatedStage,
  402. assigneeName: row.assigneeName || '未指派',
  403. creatorName: row.creatorName || '未知',
  404. createdAt: row.createdAt,
  405. updatedAt: row.updatedAt,
  406. dueDate: row.dueDate,
  407. tags: row.data?.tags || []
  408. }));
  409. // 5. Stats
  410. const stats = {
  411. totalActive: projects.length,
  412. overdueCount: projects.filter(p => p.isOverdue).length,
  413. urgentCount: projects.filter(p => p.statusStr === 'urgent').length,
  414. avgLoadRate: workload.length > 0 ? Math.round(workload.reduce((sum, w) => sum + w.loadRate, 0) / workload.length) : 0
  415. };
  416. // 6. 返回时间范围信息
  417. const timeRange = {
  418. start: rangeStart.toISOString(),
  419. end: rangeEnd.toISOString(),
  420. baseDate: baseDate || now.toISOString().split('T')[0]
  421. };
  422. response.json({
  423. code: 200,
  424. success: true,
  425. data: {
  426. stats,
  427. workload,
  428. projects,
  429. spaceStats,
  430. issues,
  431. timeRange // 添加时间范围信息
  432. }
  433. });
  434. } catch (error) {
  435. console.error('❌ SQL 执行失败:', error.message);
  436. response.json({ code: 500, success: false, error: error.message });
  437. }
  438. }