/** * GroupChat 计数器回填脚本 * * 用法: cd qiwe-common && npx tsx scripts/backfill-group-counters.ts * * 回填内容: * 1. messageCountTotal: 每个群的历史消息总数 * 2. messageCountToday: 每个群的今日消息数 * 3. memberCount: 每个群的活跃成员数 * 4. lastActivityAt: 每个群的最后消息时间 * * 执行时机: 上线计数器维护后,运行一次。 * 幂等性: 可安全多次执行,只更新与现有值不同的记录。 */ import 'dotenv/config'; import Parse from '../src/config/parse-client.js'; const GUID = process.env.QIWE_GUID || ''; async function backfillMessageCounts(): Promise { console.log('\n═══ 1/4 messageCountTotal + messageCountToday 回填 ═══'); // 获取所有群 const gq = new Parse.Query('GroupChat'); gq.exists('roomId'); gq.limit(500); const groups = await gq.find({ useMasterKey: true }); console.log(`共 ${groups.length} 个群`); // 预计算今日起始时间戳 const today = new Date(); today.setHours(0, 0, 0, 0); let updated = 0; for (const group of groups) { const roomId = group.get('roomId') as string; if (!roomId) continue; let changed = false; // 回填 messageCountTotal(所有消息) try { const mq = new Parse.Query('Message'); mq.equalTo('roomId', roomId); mq.equalTo('guid', GUID); mq.containedIn('msgType', [0, 2]); // 仅文本 + 混合消息 mq.exists('content'); mq.notEqualTo('content', ''); mq.limit(0); // 只取 count const totalCount = await mq.count({ useMasterKey: true }); if (totalCount !== (group.get('messageCountTotal') ?? 0)) { group.set('messageCountTotal', totalCount); changed = true; } // 回填 messageCountToday(今日消息) const tmq = new Parse.Query('Message'); tmq.equalTo('roomId', roomId); tmq.equalTo('guid', GUID); tmq.containedIn('msgType', [0, 2]); tmq.exists('content'); tmq.notEqualTo('content', ''); tmq.greaterThanOrEqualTo('timestamp', today); tmq.limit(0); const todayCount = await tmq.count({ useMasterKey: true }); if (todayCount !== (group.get('messageCountToday') ?? 0)) { group.set('messageCountToday', todayCount); changed = true; } } catch (e: any) { console.warn(`roomId=${roomId} 消息计数查询失败: ${e.message}`); } if (changed) { await group.save(null, { useMasterKey: true }); updated++; } } console.log(`messageCountTotal/messageCountToday 回填完成: ${updated} 个群已更新`); return updated; } async function backfillMemberCount(): Promise { console.log('\n═══ 2/4 memberCount 回填 ═══'); const gq = new Parse.Query('GroupChat'); gq.exists('roomId'); gq.limit(500); const groups = await gq.find({ useMasterKey: true }); let updated = 0; for (const group of groups) { const roomId = group.get('roomId') as string; if (!roomId) continue; try { const mmq = new Parse.Query('GroupMember'); mmq.equalTo('roomId', roomId); mmq.equalTo('guid', GUID); mmq.equalTo('status', 'active'); mmq.limit(0); const memberCount = await mmq.count({ useMasterKey: true }); if (memberCount !== (group.get('memberCount') ?? 0)) { group.set('memberCount', memberCount); await group.save(null, { useMasterKey: true }); updated++; } } catch (e: any) { console.warn(`roomId=${roomId} memberCount 查询失败: ${e.message}`); } } console.log(`memberCount 回填完成: ${updated} 个群已更新`); return updated; } async function backfillLastActivityAt(): Promise { console.log('\n═══ 3/4 lastActivityAt 回填 ═══'); const gq = new Parse.Query('GroupChat'); gq.exists('roomId'); gq.limit(500); const groups = await gq.find({ useMasterKey: true }); let updated = 0; for (const group of groups) { const roomId = group.get('roomId') as string; if (!roomId) continue; try { const mq = new Parse.Query('Message'); mq.equalTo('roomId', roomId); mq.equalTo('guid', GUID); mq.containedIn('msgType', [0, 2]); mq.exists('content'); mq.notEqualTo('content', ''); mq.descending('timestamp'); mq.limit(1); const latestMsg = await mq.first({ useMasterKey: true }); if (latestMsg) { const ts = latestMsg.get('timestamp') as Date; group.set('lastActivityAt', ts); await group.save(null, { useMasterKey: true }); updated++; } } catch (e: any) { console.warn(`roomId=${roomId} lastActivityAt 查询失败: ${e.message}`); } } console.log(`lastActivityAt 回填完成: ${updated} 个群已更新`); return updated; } async function verifyCounters(): Promise { console.log('\n═══ 4/4 验证回填结果 ═══'); const gq = new Parse.Query('GroupChat'); gq.exists('roomId'); gq.limit(500); const groups = await gq.find({ useMasterKey: true }); let withTotal = 0; let withToday = 0; let withMember = 0; let withLastActivity = 0; let totalMessages = 0; let totalMembers = 0; for (const group of groups) { const tc = group.get('messageCountTotal') ?? 0; const td = group.get('messageCountToday') ?? 0; const mc = group.get('memberCount') ?? 0; const la = group.get('lastActivityAt'); if (tc > 0) withTotal++; if (td > 0) withToday++; if (mc > 0) withMember++; if (la) withLastActivity++; totalMessages += tc; totalMembers += mc; } console.log(`总群数: ${groups.length}`); console.log(`有 messageCountTotal (>0): ${withTotal}/${groups.length}`); console.log(`有 messageCountToday (>0): ${withToday}/${groups.length}`); console.log(`有 memberCount (>0): ${withMember}/${groups.length}`); console.log(`有 lastActivityAt: ${withLastActivity}/${groups.length}`); console.log(`累计消息: ${totalMessages}`); console.log(`累计成员: ${totalMembers}`); } async function main(): Promise { if (!GUID) { console.error('❌ QIWE_GUID 未配置,请在 .env 中设置'); process.exit(1); } console.log('🔧 GroupChat 计数器回填'); console.log(`GUID: ${GUID}`); console.log(`开始时间: ${new Date().toISOString()}`); const t0 = Date.now(); await backfillMessageCounts(); await backfillMemberCount(); await backfillLastActivityAt(); await verifyCounters(); const elapsed = ((Date.now() - t0) / 1000).toFixed(1); console.log(`\n✅ 回填完成,耗时 ${elapsed}s`); } main().catch((err) => { console.error('❌ 回填失败:', err); process.exit(1); });