backfill-member-nicknames.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. /**
  2. * 手动脚本:从 Message 表的 senderName 回填 GroupMember 缺失的昵称
  3. *
  4. * 用法: cd backend && npx tsx scripts/backfill-member-nicknames.ts
  5. *
  6. * 逻辑:
  7. * 1. 查找 nickname 为空或 '' 的 GroupMember
  8. * 2. 跨所有房间,从 Message 表查找同一 userId 的 senderName
  9. * 3. 批量写入 GroupMember.nickname
  10. */
  11. import 'dotenv/config';
  12. import Parse from '../src/db/parse-client.js';
  13. const GUID = process.env.QIWE_GUID || '';
  14. const CONTAINED_IN_BATCH = 500; // containedIn 单次最多 500 个 userId
  15. async function main(): Promise<void> {
  16. console.log('=== GroupMember 昵称回填 ===');
  17. console.log(`Server: ${Parse.serverURL}`);
  18. console.log(`GUID: ${GUID}\n`);
  19. // ── 1. 查找缺少昵称的成员 ──
  20. console.log('[1/3] 查找缺失昵称的成员...');
  21. const memberQuery = new Parse.Query('GroupMember');
  22. memberQuery.equalTo('guid', GUID);
  23. memberQuery.equalTo('status', 'active');
  24. memberQuery.limit(10000);
  25. const allMembers = await memberQuery.find({ useMasterKey: true });
  26. const noNick: Parse.Object[] = [];
  27. let hasNick = 0;
  28. for (const m of allMembers) {
  29. const nick = m.get('nickname');
  30. if (!nick || (typeof nick === 'string' && nick.trim() === '')) {
  31. noNick.push(m);
  32. } else {
  33. hasNick++;
  34. }
  35. }
  36. console.log(` 总数: ${allMembers.length} | 有昵称: ${hasNick} | 缺昵称: ${noNick.length}`);
  37. if (noNick.length === 0) {
  38. console.log('\n✅ 所有成员已有昵称,无需回填。');
  39. return;
  40. }
  41. // ── 2. 跨房间从 Message 查找 senderName ──
  42. // 收集所有无昵称成员的唯一 userId(去重)
  43. const noNickUserIds = [...new Set(noNick.map((m) => String(m.get('userId') || '')).filter(Boolean))];
  44. console.log(` 去重后唯一 userId: ${noNickUserIds.length} 个`);
  45. // 分批用 containedIn 查询消息(避免单次参数过多)
  46. const userIdToName = new Map<string, string>();
  47. const batches: string[][] = [];
  48. for (let i = 0; i < noNickUserIds.length; i += CONTAINED_IN_BATCH) {
  49. batches.push(noNickUserIds.slice(i, i + CONTAINED_IN_BATCH));
  50. }
  51. console.log(` 分 ${batches.length} 批查询消息...`);
  52. for (let bi = 0; bi < batches.length; bi++) {
  53. const batch = batches[bi];
  54. try {
  55. const msgQuery = new Parse.Query('Message');
  56. msgQuery.equalTo('guid', GUID);
  57. msgQuery.equalTo('isGroupChat', 1);
  58. msgQuery.containedIn('senderId', batch);
  59. msgQuery.exists('senderName');
  60. msgQuery.notEqualTo('senderName', '');
  61. msgQuery.limit(5000);
  62. const messages = await msgQuery.find({ useMasterKey: true });
  63. for (const msg of messages) {
  64. const sid = msg.get('senderId') as string;
  65. const sname = msg.get('senderName') as string;
  66. if (sid && sname && !userIdToName.has(sid)) {
  67. userIdToName.set(sid, sname);
  68. }
  69. }
  70. console.log(` 批次 ${bi + 1}/${batches.length}: ${messages.length} 条消息, 已累积 ${userIdToName.size} 个 senderName`);
  71. } catch (err: any) {
  72. console.warn(` ⚠ 批次 ${bi + 1} 查询失败: ${err.message}`);
  73. }
  74. }
  75. // ── 3. 回填 ──
  76. console.log(`\n[3/3] 回填 GroupMember.nickname...`);
  77. let filled = 0;
  78. let skipped = 0;
  79. const toSave: Parse.Object[] = [];
  80. for (const m of noNick) {
  81. const uid = String(m.get('userId') || '');
  82. const senderName = userIdToName.get(uid);
  83. if (senderName) {
  84. m.set('nickname', senderName);
  85. toSave.push(m);
  86. filled++;
  87. } else {
  88. skipped++;
  89. }
  90. }
  91. // 分批写入
  92. for (let i = 0; i < toSave.length; i += 100) {
  93. await Parse.Object.saveAll(toSave.slice(i, i + 100), { useMasterKey: true });
  94. }
  95. console.log(`\n✅ 完成 — 回填: ${filled} | 找不到消息: ${skipped} | 总计无昵称: ${noNick.length}`);
  96. }
  97. main().catch((err) => {
  98. console.error('回填失败:', err.message);
  99. process.exit(1);
  100. });