backfill-group-counters.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * GroupChat 计数器回填脚本
  3. *
  4. * 用法: cd qiwe-common && npx tsx scripts/backfill-group-counters.ts
  5. *
  6. * 回填内容:
  7. * 1. messageCountTotal: 每个群的历史消息总数
  8. * 2. messageCountToday: 每个群的今日消息数
  9. * 3. memberCount: 每个群的活跃成员数
  10. * 4. lastActivityAt: 每个群的最后消息时间
  11. *
  12. * 执行时机: 上线计数器维护后,运行一次。
  13. * 幂等性: 可安全多次执行,只更新与现有值不同的记录。
  14. */
  15. import 'dotenv/config';
  16. import Parse from '../src/config/parse-client.js';
  17. const GUID = process.env.QIWE_GUID || '';
  18. async function backfillMessageCounts(): Promise<number> {
  19. console.log('\n═══ 1/4 messageCountTotal + messageCountToday 回填 ═══');
  20. // 获取所有群
  21. const gq = new Parse.Query('GroupChat');
  22. gq.exists('roomId');
  23. gq.limit(500);
  24. const groups = await gq.find({ useMasterKey: true });
  25. console.log(`共 ${groups.length} 个群`);
  26. // 预计算今日起始时间戳
  27. const today = new Date();
  28. today.setHours(0, 0, 0, 0);
  29. let updated = 0;
  30. for (const group of groups) {
  31. const roomId = group.get('roomId') as string;
  32. if (!roomId) continue;
  33. let changed = false;
  34. // 回填 messageCountTotal(所有消息)
  35. try {
  36. const mq = new Parse.Query('Message');
  37. mq.equalTo('roomId', roomId);
  38. mq.equalTo('guid', GUID);
  39. mq.containedIn('msgType', [0, 2]); // 仅文本 + 混合消息
  40. mq.exists('content');
  41. mq.notEqualTo('content', '');
  42. mq.limit(0); // 只取 count
  43. const totalCount = await mq.count({ useMasterKey: true });
  44. if (totalCount !== (group.get('messageCountTotal') ?? 0)) {
  45. group.set('messageCountTotal', totalCount);
  46. changed = true;
  47. }
  48. // 回填 messageCountToday(今日消息)
  49. const tmq = new Parse.Query('Message');
  50. tmq.equalTo('roomId', roomId);
  51. tmq.equalTo('guid', GUID);
  52. tmq.containedIn('msgType', [0, 2]);
  53. tmq.exists('content');
  54. tmq.notEqualTo('content', '');
  55. tmq.greaterThanOrEqualTo('timestamp', today);
  56. tmq.limit(0);
  57. const todayCount = await tmq.count({ useMasterKey: true });
  58. if (todayCount !== (group.get('messageCountToday') ?? 0)) {
  59. group.set('messageCountToday', todayCount);
  60. changed = true;
  61. }
  62. } catch (e: any) {
  63. console.warn(`roomId=${roomId} 消息计数查询失败: ${e.message}`);
  64. }
  65. if (changed) {
  66. await group.save(null, { useMasterKey: true });
  67. updated++;
  68. }
  69. }
  70. console.log(`messageCountTotal/messageCountToday 回填完成: ${updated} 个群已更新`);
  71. return updated;
  72. }
  73. async function backfillMemberCount(): Promise<number> {
  74. console.log('\n═══ 2/4 memberCount 回填 ═══');
  75. const gq = new Parse.Query('GroupChat');
  76. gq.exists('roomId');
  77. gq.limit(500);
  78. const groups = await gq.find({ useMasterKey: true });
  79. let updated = 0;
  80. for (const group of groups) {
  81. const roomId = group.get('roomId') as string;
  82. if (!roomId) continue;
  83. try {
  84. const mmq = new Parse.Query('GroupMember');
  85. mmq.equalTo('roomId', roomId);
  86. mmq.equalTo('guid', GUID);
  87. mmq.equalTo('status', 'active');
  88. mmq.limit(0);
  89. const memberCount = await mmq.count({ useMasterKey: true });
  90. if (memberCount !== (group.get('memberCount') ?? 0)) {
  91. group.set('memberCount', memberCount);
  92. await group.save(null, { useMasterKey: true });
  93. updated++;
  94. }
  95. } catch (e: any) {
  96. console.warn(`roomId=${roomId} memberCount 查询失败: ${e.message}`);
  97. }
  98. }
  99. console.log(`memberCount 回填完成: ${updated} 个群已更新`);
  100. return updated;
  101. }
  102. async function backfillLastActivityAt(): Promise<number> {
  103. console.log('\n═══ 3/4 lastActivityAt 回填 ═══');
  104. const gq = new Parse.Query('GroupChat');
  105. gq.exists('roomId');
  106. gq.limit(500);
  107. const groups = await gq.find({ useMasterKey: true });
  108. let updated = 0;
  109. for (const group of groups) {
  110. const roomId = group.get('roomId') as string;
  111. if (!roomId) continue;
  112. try {
  113. const mq = new Parse.Query('Message');
  114. mq.equalTo('roomId', roomId);
  115. mq.equalTo('guid', GUID);
  116. mq.containedIn('msgType', [0, 2]);
  117. mq.exists('content');
  118. mq.notEqualTo('content', '');
  119. mq.descending('timestamp');
  120. mq.limit(1);
  121. const latestMsg = await mq.first({ useMasterKey: true });
  122. if (latestMsg) {
  123. const ts = latestMsg.get('timestamp') as Date;
  124. group.set('lastActivityAt', ts);
  125. await group.save(null, { useMasterKey: true });
  126. updated++;
  127. }
  128. } catch (e: any) {
  129. console.warn(`roomId=${roomId} lastActivityAt 查询失败: ${e.message}`);
  130. }
  131. }
  132. console.log(`lastActivityAt 回填完成: ${updated} 个群已更新`);
  133. return updated;
  134. }
  135. async function verifyCounters(): Promise<void> {
  136. console.log('\n═══ 4/4 验证回填结果 ═══');
  137. const gq = new Parse.Query('GroupChat');
  138. gq.exists('roomId');
  139. gq.limit(500);
  140. const groups = await gq.find({ useMasterKey: true });
  141. let withTotal = 0;
  142. let withToday = 0;
  143. let withMember = 0;
  144. let withLastActivity = 0;
  145. let totalMessages = 0;
  146. let totalMembers = 0;
  147. for (const group of groups) {
  148. const tc = group.get('messageCountTotal') ?? 0;
  149. const td = group.get('messageCountToday') ?? 0;
  150. const mc = group.get('memberCount') ?? 0;
  151. const la = group.get('lastActivityAt');
  152. if (tc > 0) withTotal++;
  153. if (td > 0) withToday++;
  154. if (mc > 0) withMember++;
  155. if (la) withLastActivity++;
  156. totalMessages += tc;
  157. totalMembers += mc;
  158. }
  159. console.log(`总群数: ${groups.length}`);
  160. console.log(`有 messageCountTotal (>0): ${withTotal}/${groups.length}`);
  161. console.log(`有 messageCountToday (>0): ${withToday}/${groups.length}`);
  162. console.log(`有 memberCount (>0): ${withMember}/${groups.length}`);
  163. console.log(`有 lastActivityAt: ${withLastActivity}/${groups.length}`);
  164. console.log(`累计消息: ${totalMessages}`);
  165. console.log(`累计成员: ${totalMembers}`);
  166. }
  167. async function main(): Promise<void> {
  168. if (!GUID) {
  169. console.error('❌ QIWE_GUID 未配置,请在 .env 中设置');
  170. process.exit(1);
  171. }
  172. console.log('🔧 GroupChat 计数器回填');
  173. console.log(`GUID: ${GUID}`);
  174. console.log(`开始时间: ${new Date().toISOString()}`);
  175. const t0 = Date.now();
  176. await backfillMessageCounts();
  177. await backfillMemberCount();
  178. await backfillLastActivityAt();
  179. await verifyCounters();
  180. const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
  181. console.log(`\n✅ 回填完成,耗时 ${elapsed}s`);
  182. }
  183. main().catch((err) => {
  184. console.error('❌ 回填失败:', err);
  185. process.exit(1);
  186. });