/** * 手动脚本:从 Message 表的 senderName 回填 GroupMember 缺失的昵称 * * 用法: cd backend && npx tsx scripts/backfill-member-nicknames.ts * * 逻辑: * 1. 查找 nickname 为空或 '' 的 GroupMember * 2. 跨所有房间,从 Message 表查找同一 userId 的 senderName * 3. 批量写入 GroupMember.nickname */ import 'dotenv/config'; import Parse from '../src/db/parse-client.js'; const GUID = process.env.QIWE_GUID || ''; const CONTAINED_IN_BATCH = 500; // containedIn 单次最多 500 个 userId async function main(): Promise { console.log('=== GroupMember 昵称回填 ==='); console.log(`Server: ${Parse.serverURL}`); console.log(`GUID: ${GUID}\n`); // ── 1. 查找缺少昵称的成员 ── console.log('[1/3] 查找缺失昵称的成员...'); const memberQuery = new Parse.Query('GroupMember'); memberQuery.equalTo('guid', GUID); memberQuery.equalTo('status', 'active'); memberQuery.limit(10000); const allMembers = await memberQuery.find({ useMasterKey: true }); const noNick: Parse.Object[] = []; let hasNick = 0; for (const m of allMembers) { const nick = m.get('nickname'); if (!nick || (typeof nick === 'string' && nick.trim() === '')) { noNick.push(m); } else { hasNick++; } } console.log(` 总数: ${allMembers.length} | 有昵称: ${hasNick} | 缺昵称: ${noNick.length}`); if (noNick.length === 0) { console.log('\n✅ 所有成员已有昵称,无需回填。'); return; } // ── 2. 跨房间从 Message 查找 senderName ── // 收集所有无昵称成员的唯一 userId(去重) const noNickUserIds = [...new Set(noNick.map((m) => String(m.get('userId') || '')).filter(Boolean))]; console.log(` 去重后唯一 userId: ${noNickUserIds.length} 个`); // 分批用 containedIn 查询消息(避免单次参数过多) const userIdToName = new Map(); const batches: string[][] = []; for (let i = 0; i < noNickUserIds.length; i += CONTAINED_IN_BATCH) { batches.push(noNickUserIds.slice(i, i + CONTAINED_IN_BATCH)); } console.log(` 分 ${batches.length} 批查询消息...`); for (let bi = 0; bi < batches.length; bi++) { const batch = batches[bi]; try { const msgQuery = new Parse.Query('Message'); msgQuery.equalTo('guid', GUID); msgQuery.equalTo('isGroupChat', 1); msgQuery.containedIn('senderId', batch); msgQuery.exists('senderName'); msgQuery.notEqualTo('senderName', ''); msgQuery.limit(5000); const messages = await msgQuery.find({ useMasterKey: true }); for (const msg of messages) { const sid = msg.get('senderId') as string; const sname = msg.get('senderName') as string; if (sid && sname && !userIdToName.has(sid)) { userIdToName.set(sid, sname); } } console.log(` 批次 ${bi + 1}/${batches.length}: ${messages.length} 条消息, 已累积 ${userIdToName.size} 个 senderName`); } catch (err: any) { console.warn(` ⚠ 批次 ${bi + 1} 查询失败: ${err.message}`); } } // ── 3. 回填 ── console.log(`\n[3/3] 回填 GroupMember.nickname...`); let filled = 0; let skipped = 0; const toSave: Parse.Object[] = []; for (const m of noNick) { const uid = String(m.get('userId') || ''); const senderName = userIdToName.get(uid); if (senderName) { m.set('nickname', senderName); toSave.push(m); filled++; } else { skipped++; } } // 分批写入 for (let i = 0; i < toSave.length; i += 100) { await Parse.Object.saveAll(toSave.slice(i, i + 100), { useMasterKey: true }); } console.log(`\n✅ 完成 — 回填: ${filled} | 找不到消息: ${skipped} | 总计无昵称: ${noNick.length}`); } main().catch((err) => { console.error('回填失败:', err.message); process.exit(1); });