remove-duplicate-projects.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /**
  2. * 删除重复的项目
  3. * 用途:查找并删除标题相同的重复项目,保留最新的一个
  4. *
  5. * 使用方法:
  6. * 1. 在 Parse Dashboard 的 Cloud Code 页面运行此脚本
  7. * 2. 或者通过 API 调用:POST /parse/functions/removeDuplicateProjects
  8. */
  9. Parse.Cloud.define('removeDuplicateProjects', async (request) => {
  10. const { title, dryRun = true } = request.params;
  11. console.log('🔍 开始查找重复项目...');
  12. console.log('查询条件:', { title, dryRun: dryRun ? '预览模式' : '执行删除' });
  13. try {
  14. const query = new Parse.Query('Project');
  15. // 如果提供了 title,只查找该标题的项目
  16. if (title) {
  17. query.equalTo('title', title);
  18. }
  19. query.notEqualTo('isDeleted', true);
  20. query.ascending('createdAt'); // 按创建时间升序,保留最新的
  21. query.limit(1000);
  22. const projects = await query.find({ useMasterKey: true });
  23. console.log(`📊 找到 ${projects.length} 个项目`);
  24. // 按标题分组
  25. const projectsByTitle = new Map();
  26. projects.forEach(project => {
  27. const projectTitle = project.get('title') || '无标题';
  28. if (!projectsByTitle.has(projectTitle)) {
  29. projectsByTitle.set(projectTitle, []);
  30. }
  31. projectsByTitle.get(projectTitle).push(project);
  32. });
  33. // 查找重复的项目
  34. const duplicates = [];
  35. const summary = [];
  36. for (const [projectTitle, projectList] of projectsByTitle.entries()) {
  37. if (projectList.length > 1) {
  38. console.log(`\n⚠️ 发现重复项目: "${projectTitle}" (${projectList.length} 个)`);
  39. // 按创建时间排序,保留最新的
  40. projectList.sort((a, b) => {
  41. const aTime = a.get('createdAt') || a.createdAt;
  42. const bTime = b.get('createdAt') || b.createdAt;
  43. return bTime.getTime() - aTime.getTime();
  44. });
  45. // 第一个是最新的,保留
  46. const keepProject = projectList[0];
  47. const deleteProjects = projectList.slice(1);
  48. console.log(` ✅ 保留: ${keepProject.id} (创建于: ${keepProject.get('createdAt')})`);
  49. deleteProjects.forEach(project => {
  50. console.log(` ❌ 删除: ${project.id} (创建于: ${project.get('createdAt')})`);
  51. duplicates.push({
  52. id: project.id,
  53. title: projectTitle,
  54. createdAt: project.get('createdAt'),
  55. currentStage: project.get('currentStage'),
  56. status: project.get('status')
  57. });
  58. if (!dryRun) {
  59. // 标记为已删除
  60. project.set('isDeleted', true);
  61. project.set('deletedAt', new Date());
  62. project.set('deleteReason', '重复项目(自动清理)');
  63. }
  64. });
  65. summary.push({
  66. title: projectTitle,
  67. total: projectList.length,
  68. kept: 1,
  69. deleted: deleteProjects.length,
  70. keptProjectId: keepProject.id
  71. });
  72. }
  73. }
  74. // 执行删除
  75. if (!dryRun && duplicates.length > 0) {
  76. const projectsToDelete = duplicates.map(d => {
  77. const project = new Parse.Object('Project');
  78. project.id = d.id;
  79. project.set('isDeleted', true);
  80. project.set('deletedAt', new Date());
  81. project.set('deleteReason', '重复项目(自动清理)');
  82. return project;
  83. });
  84. await Parse.Object.saveAll(projectsToDelete, { useMasterKey: true });
  85. console.log(`\n✅ 已删除 ${duplicates.length} 个重复项目`);
  86. }
  87. return {
  88. success: true,
  89. dryRun,
  90. summary: {
  91. totalProjects: projects.length,
  92. duplicatesFound: duplicates.length,
  93. titlesWithDuplicates: summary.length
  94. },
  95. details: summary,
  96. duplicates: duplicates.map(d => ({
  97. id: d.id,
  98. title: d.title,
  99. createdAt: d.createdAt,
  100. stage: d.currentStage,
  101. status: d.status
  102. }))
  103. };
  104. } catch (error) {
  105. console.error('❌ 删除重复项目失败:', error);
  106. throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `删除失败: ${error.message}`);
  107. }
  108. });
  109. /**
  110. * 删除指定的单个项目(用于手动删除)
  111. */
  112. Parse.Cloud.define('deleteProjectById', async (request) => {
  113. const { projectId, reason = '手动删除' } = request.params;
  114. if (!projectId) {
  115. throw new Parse.Error(Parse.Error.INVALID_QUERY, '缺少 projectId 参数');
  116. }
  117. try {
  118. const query = new Parse.Query('Project');
  119. const project = await query.get(projectId, { useMasterKey: true });
  120. if (!project) {
  121. throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, '项目不存在');
  122. }
  123. const projectTitle = project.get('title');
  124. console.log(`🗑️ 删除项目: ${projectTitle} (${projectId})`);
  125. // 软删除
  126. project.set('isDeleted', true);
  127. project.set('deletedAt', new Date());
  128. project.set('deleteReason', reason);
  129. await project.save(null, { useMasterKey: true });
  130. return {
  131. success: true,
  132. message: `项目 "${projectTitle}" 已删除`,
  133. projectId: projectId,
  134. title: projectTitle
  135. };
  136. } catch (error) {
  137. console.error('❌ 删除项目失败:', error);
  138. throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `删除失败: ${error.message}`);
  139. }
  140. });
  141. /**
  142. * 查找指定标题的所有项目(用于预览)
  143. */
  144. Parse.Cloud.define('findProjectsByTitle', async (request) => {
  145. const { title } = request.params;
  146. if (!title) {
  147. throw new Parse.Error(Parse.Error.INVALID_QUERY, '缺少 title 参数');
  148. }
  149. try {
  150. const query = new Parse.Query('Project');
  151. query.equalTo('title', title);
  152. query.notEqualTo('isDeleted', true);
  153. query.ascending('createdAt');
  154. query.limit(100);
  155. const projects = await query.find({ useMasterKey: true });
  156. return {
  157. success: true,
  158. total: projects.length,
  159. projects: projects.map(p => ({
  160. id: p.id,
  161. title: p.get('title'),
  162. createdAt: p.get('createdAt'),
  163. currentStage: p.get('currentStage'),
  164. status: p.get('status'),
  165. assignee: p.get('assignee')?.get('name') || '未分配'
  166. }))
  167. };
  168. } catch (error) {
  169. console.error('❌ 查找项目失败:', error);
  170. throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `查找失败: ${error.message}`);
  171. }
  172. });