/** * Phase 0: 风险通知体系 — 全表 Schema 迁移 + 数据迁移 * * 用法: cd backend && npx tsx scripts/migrate-risk-notification-schema.ts * * 涉及表: * 1. ResponsibilityAssignment — 删 userId, 加 user(Pointer→_User) * 2. RiskEvent — 删 storeName/districtName, 加 escalation/confirmed/groupOwner 字段 * 3. AppNotification — 加 riskEvent(Pointer→RiskEvent) * 4. GroupChat — 加 store(Pointer→Store), community(Pointer→Community), ownerPhone(String) */ import 'dotenv/config'; import Parse from '../src/db/parse-client.js'; async function testConnection(): Promise { try { const n = await new Parse.Query('_User').count({ useMasterKey: true }); console.log(`✅ Parse 连接正常 (_User: ${n} 条)\n`); } catch (err: any) { console.error(`❌ Parse 连接失败: ${err.message}`); process.exit(1); } } /* ═══════════════════════════════════════════════ 1. ResponsibilityAssignment ═══════════════════════════════════════════════ */ async function migrateResponsibilityAssignment(): Promise { console.log('═══ 1/4 ResponsibilityAssignment ═══'); const schema = new Parse.Schema('ResponsibilityAssignment'); // 读现有数据 const q = new Parse.Query('ResponsibilityAssignment'); q.limit(5000); const records = await q.find({ useMasterKey: true }) as any[]; console.log(` 现有记录: ${records.length} 条`); // userId → user Pointer 数据迁移 let migrated = 0; let skipped = 0; const toSave: Parse.Object[] = []; for (const obj of records) { const userId = obj.get('userId') as string | undefined; let changed = false; if (userId) { try { const userQ = new Parse.Query('_User'); const user = await userQ.get(userId, { useMasterKey: true }); obj.set('user', user); migrated++; changed = true; } catch { console.log(` ⚠ userId=${userId} 对应 _User 不存在,跳过`); skipped++; } } // 清除旧 userId 字段值 if (obj.get('userId') != null) { obj.unset('userId'); changed = true; } if (changed) toSave.push(obj); } if (toSave.length > 0) { await Parse.Object.saveAll(toSave, { useMasterKey: true }); } console.log(` 数据: ${migrated} 条转 Pointer, ${skipped} 条跳过`); // Schema 更新 try { await schema.get({ useMasterKey: true }); } catch { /* 不存在则创建 */ } try { await schema.deleteField('userId'); } catch { /* 已删除 */ } try { await schema.addField('user', 'Pointer', { targetClass: '_User' }); } catch { /* 已存在 */ } await schema.update({ useMasterKey: true }); console.log(' Schema: userId 已删除, user(Pointer→_User) 已添加, userName/phone 保留\n'); } /* ═══════════════════════════════════════════════ 2. RiskEvent ═══════════════════════════════════════════════ */ async function migrateRiskEvent(): Promise { console.log('═══ 2/4 RiskEvent ═══'); const schema = new Parse.Schema('RiskEvent'); // 删除冗余 String 字段 try { await schema.deleteField('storeName'); } catch { /* 不存在则跳过 */ } try { await schema.deleteField('districtName'); } catch { /* 不存在则跳过 */ } // 新增 escalation + confirmed + groupOwner 字段 const newFields: Array<{ name: string; type: string; opts?: Record }> = [ { name: 'escalationLevel', type: 'Number' }, { name: 'escalatedAt', type: 'Date' }, { name: 'confirmedBy', type: 'Pointer', opts: { targetClass: '_User' } }, { name: 'confirmedAt', type: 'Date' }, { name: 'groupOwnerName', type: 'String' }, { name: 'groupOwnerPhone', type: 'String' }, { name: 'escalationNotifiedUserIds', type: 'Array' }, ]; for (const f of newFields) { try { if (f.opts) { await schema.addField(f.name, f.type, f.opts); } else { await schema.addField(f.name, f.type); } console.log(` + ${f.name} (${f.type})`); } catch (err: any) { if (err.message?.includes('already')) { console.log(` ~ ${f.name} 已存在`); } else { console.warn(` ⚠ ${f.name} 添加失败: ${err.message}`); } } } await schema.update({ useMasterKey: true }); console.log(' Schema 更新完成'); // 已有 pending 事件补默认值 const q = new Parse.Query('RiskEvent'); q.equalTo('status', 'pending'); q.limit(5000); const pendingEvents = await q.find({ useMasterKey: true }) as any[]; console.log(` 待补全的 pending 事件: ${pendingEvents.length} 条`); if (pendingEvents.length > 0) { const toSave: Parse.Object[] = []; for (const ev of pendingEvents) { let changed = false; if (ev.get('escalationLevel') == null) { ev.set('escalationLevel', 1); changed = true; } if (ev.get('escalatedAt') == null) { ev.set('escalatedAt', ev.get('createdAt') || new Date()); changed = true; } if (ev.get('escalationNotifiedUserIds') == null) { ev.set('escalationNotifiedUserIds', []); changed = true; } if (ev.get('confirmedBy') == null && ev.get('confirmedAt') == null) { // 保持 null,不需要额外设置 } if (ev.get('groupOwnerName') == null) { ev.set('groupOwnerName', ''); changed = true; } if (ev.get('groupOwnerPhone') == null) { ev.set('groupOwnerPhone', ''); changed = true; } if (changed) toSave.push(ev); } if (toSave.length > 0) { await Parse.Object.saveAll(toSave, { useMasterKey: true }); console.log(` 已补全 ${toSave.length} 条 pending 事件默认值`); } } console.log(''); } /* ═══════════════════════════════════════════════ 3. AppNotification ═══════════════════════════════════════════════ */ async function migrateAppNotification(): Promise { console.log('═══ 3/4 AppNotification ═══'); const schema = new Parse.Schema('AppNotification'); try { await schema.addField('riskEvent', 'Pointer', { targetClass: 'RiskEvent' }); } catch { /* 已存在 */ } await schema.update({ useMasterKey: true }); console.log(' Schema: riskEvent(Pointer→RiskEvent) 已添加\n'); } /* ═══════════════════════════════════════════════ 4. GroupChat ═══════════════════════════════════════════════ */ async function migrateGroupChat(): Promise { console.log('═══ 4/4 GroupChat ═══'); const schema = new Parse.Schema('GroupChat'); const additions: Array<{ name: string; type: string; opts?: Record }> = [ { name: 'store', type: 'Pointer', opts: { targetClass: 'Store' } }, { name: 'community', type: 'Pointer', opts: { targetClass: 'Community' } }, { name: 'ownerPhone', type: 'String' }, ]; for (const add of additions) { try { if (add.opts) { await schema.addField(add.name, add.type, add.opts); } else { await schema.addField(add.name, add.type); } console.log(` + ${add.name} (${add.type})`); } catch (err: any) { if (err.message?.includes('already')) { console.log(` ~ ${add.name} 已存在`); } else { console.warn(` ⚠ ${add.name} 添加失败: ${err.message}`); } } } await schema.update({ useMasterKey: true }); console.log(' Schema 更新完成\n'); } /* ═══════════════════════════════════════════════ Main ═══════════════════════════════════════════════ */ async function main(): Promise { console.log('=== 风险通知体系 Schema 迁移 ==='); console.log(`Server: ${Parse.serverURL}`); console.log(`App ID: ${Parse.applicationId}\n`); await testConnection(); await migrateResponsibilityAssignment(); await migrateRiskEvent(); await migrateAppNotification(); await migrateGroupChat(); console.log('=== 迁移完成 ===\n'); } main().catch((err) => { console.error('迁移失败:', err.message); process.exit(1); });