| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- /**
- * 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<void> {
- 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<void> {
- 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<void> {
- 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<string, any> }> = [
- { 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<void> {
- 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<void> {
- console.log('═══ 4/4 GroupChat ═══');
- const schema = new Parse.Schema('GroupChat');
- const additions: Array<{ name: string; type: string; opts?: Record<string, any> }> = [
- { 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<void> {
- 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);
- });
|