migrate-risk-notification-schema.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * Phase 0: 风险通知体系 — 全表 Schema 迁移 + 数据迁移
  3. *
  4. * 用法: cd backend && npx tsx scripts/migrate-risk-notification-schema.ts
  5. *
  6. * 涉及表:
  7. * 1. ResponsibilityAssignment — 删 userId, 加 user(Pointer→_User)
  8. * 2. RiskEvent — 删 storeName/districtName, 加 escalation/confirmed/groupOwner 字段
  9. * 3. AppNotification — 加 riskEvent(Pointer→RiskEvent)
  10. * 4. GroupChat — 加 store(Pointer→Store), community(Pointer→Community), ownerPhone(String)
  11. */
  12. import 'dotenv/config';
  13. import Parse from '../src/db/parse-client.js';
  14. async function testConnection(): Promise<void> {
  15. try {
  16. const n = await new Parse.Query('_User').count({ useMasterKey: true });
  17. console.log(`✅ Parse 连接正常 (_User: ${n} 条)\n`);
  18. } catch (err: any) {
  19. console.error(`❌ Parse 连接失败: ${err.message}`);
  20. process.exit(1);
  21. }
  22. }
  23. /* ═══════════════════════════════════════════════
  24. 1. ResponsibilityAssignment
  25. ═══════════════════════════════════════════════ */
  26. async function migrateResponsibilityAssignment(): Promise<void> {
  27. console.log('═══ 1/4 ResponsibilityAssignment ═══');
  28. const schema = new Parse.Schema('ResponsibilityAssignment');
  29. // 读现有数据
  30. const q = new Parse.Query('ResponsibilityAssignment');
  31. q.limit(5000);
  32. const records = await q.find({ useMasterKey: true }) as any[];
  33. console.log(` 现有记录: ${records.length} 条`);
  34. // userId → user Pointer 数据迁移
  35. let migrated = 0;
  36. let skipped = 0;
  37. const toSave: Parse.Object[] = [];
  38. for (const obj of records) {
  39. const userId = obj.get('userId') as string | undefined;
  40. let changed = false;
  41. if (userId) {
  42. try {
  43. const userQ = new Parse.Query('_User');
  44. const user = await userQ.get(userId, { useMasterKey: true });
  45. obj.set('user', user);
  46. migrated++;
  47. changed = true;
  48. } catch {
  49. console.log(` ⚠ userId=${userId} 对应 _User 不存在,跳过`);
  50. skipped++;
  51. }
  52. }
  53. // 清除旧 userId 字段值
  54. if (obj.get('userId') != null) {
  55. obj.unset('userId');
  56. changed = true;
  57. }
  58. if (changed) toSave.push(obj);
  59. }
  60. if (toSave.length > 0) {
  61. await Parse.Object.saveAll(toSave, { useMasterKey: true });
  62. }
  63. console.log(` 数据: ${migrated} 条转 Pointer, ${skipped} 条跳过`);
  64. // Schema 更新
  65. try { await schema.get({ useMasterKey: true }); } catch { /* 不存在则创建 */ }
  66. try { await schema.deleteField('userId'); } catch { /* 已删除 */ }
  67. try { await schema.addField('user', 'Pointer', { targetClass: '_User' }); } catch { /* 已存在 */ }
  68. await schema.update({ useMasterKey: true });
  69. console.log(' Schema: userId 已删除, user(Pointer→_User) 已添加, userName/phone 保留\n');
  70. }
  71. /* ═══════════════════════════════════════════════
  72. 2. RiskEvent
  73. ═══════════════════════════════════════════════ */
  74. async function migrateRiskEvent(): Promise<void> {
  75. console.log('═══ 2/4 RiskEvent ═══');
  76. const schema = new Parse.Schema('RiskEvent');
  77. // 删除冗余 String 字段
  78. try { await schema.deleteField('storeName'); } catch { /* 不存在则跳过 */ }
  79. try { await schema.deleteField('districtName'); } catch { /* 不存在则跳过 */ }
  80. // 新增 escalation + confirmed + groupOwner 字段
  81. const newFields: Array<{ name: string; type: string; opts?: Record<string, any> }> = [
  82. { name: 'escalationLevel', type: 'Number' },
  83. { name: 'escalatedAt', type: 'Date' },
  84. { name: 'confirmedBy', type: 'Pointer', opts: { targetClass: '_User' } },
  85. { name: 'confirmedAt', type: 'Date' },
  86. { name: 'groupOwnerName', type: 'String' },
  87. { name: 'groupOwnerPhone', type: 'String' },
  88. { name: 'escalationNotifiedUserIds', type: 'Array' },
  89. ];
  90. for (const f of newFields) {
  91. try {
  92. if (f.opts) {
  93. await schema.addField(f.name, f.type, f.opts);
  94. } else {
  95. await schema.addField(f.name, f.type);
  96. }
  97. console.log(` + ${f.name} (${f.type})`);
  98. } catch (err: any) {
  99. if (err.message?.includes('already')) {
  100. console.log(` ~ ${f.name} 已存在`);
  101. } else {
  102. console.warn(` ⚠ ${f.name} 添加失败: ${err.message}`);
  103. }
  104. }
  105. }
  106. await schema.update({ useMasterKey: true });
  107. console.log(' Schema 更新完成');
  108. // 已有 pending 事件补默认值
  109. const q = new Parse.Query('RiskEvent');
  110. q.equalTo('status', 'pending');
  111. q.limit(5000);
  112. const pendingEvents = await q.find({ useMasterKey: true }) as any[];
  113. console.log(` 待补全的 pending 事件: ${pendingEvents.length} 条`);
  114. if (pendingEvents.length > 0) {
  115. const toSave: Parse.Object[] = [];
  116. for (const ev of pendingEvents) {
  117. let changed = false;
  118. if (ev.get('escalationLevel') == null) {
  119. ev.set('escalationLevel', 1);
  120. changed = true;
  121. }
  122. if (ev.get('escalatedAt') == null) {
  123. ev.set('escalatedAt', ev.get('createdAt') || new Date());
  124. changed = true;
  125. }
  126. if (ev.get('escalationNotifiedUserIds') == null) {
  127. ev.set('escalationNotifiedUserIds', []);
  128. changed = true;
  129. }
  130. if (ev.get('confirmedBy') == null && ev.get('confirmedAt') == null) {
  131. // 保持 null,不需要额外设置
  132. }
  133. if (ev.get('groupOwnerName') == null) {
  134. ev.set('groupOwnerName', '');
  135. changed = true;
  136. }
  137. if (ev.get('groupOwnerPhone') == null) {
  138. ev.set('groupOwnerPhone', '');
  139. changed = true;
  140. }
  141. if (changed) toSave.push(ev);
  142. }
  143. if (toSave.length > 0) {
  144. await Parse.Object.saveAll(toSave, { useMasterKey: true });
  145. console.log(` 已补全 ${toSave.length} 条 pending 事件默认值`);
  146. }
  147. }
  148. console.log('');
  149. }
  150. /* ═══════════════════════════════════════════════
  151. 3. AppNotification
  152. ═══════════════════════════════════════════════ */
  153. async function migrateAppNotification(): Promise<void> {
  154. console.log('═══ 3/4 AppNotification ═══');
  155. const schema = new Parse.Schema('AppNotification');
  156. try { await schema.addField('riskEvent', 'Pointer', { targetClass: 'RiskEvent' }); } catch { /* 已存在 */ }
  157. await schema.update({ useMasterKey: true });
  158. console.log(' Schema: riskEvent(Pointer→RiskEvent) 已添加\n');
  159. }
  160. /* ═══════════════════════════════════════════════
  161. 4. GroupChat
  162. ═══════════════════════════════════════════════ */
  163. async function migrateGroupChat(): Promise<void> {
  164. console.log('═══ 4/4 GroupChat ═══');
  165. const schema = new Parse.Schema('GroupChat');
  166. const additions: Array<{ name: string; type: string; opts?: Record<string, any> }> = [
  167. { name: 'store', type: 'Pointer', opts: { targetClass: 'Store' } },
  168. { name: 'community', type: 'Pointer', opts: { targetClass: 'Community' } },
  169. { name: 'ownerPhone', type: 'String' },
  170. ];
  171. for (const add of additions) {
  172. try {
  173. if (add.opts) {
  174. await schema.addField(add.name, add.type, add.opts);
  175. } else {
  176. await schema.addField(add.name, add.type);
  177. }
  178. console.log(` + ${add.name} (${add.type})`);
  179. } catch (err: any) {
  180. if (err.message?.includes('already')) {
  181. console.log(` ~ ${add.name} 已存在`);
  182. } else {
  183. console.warn(` ⚠ ${add.name} 添加失败: ${err.message}`);
  184. }
  185. }
  186. }
  187. await schema.update({ useMasterKey: true });
  188. console.log(' Schema 更新完成\n');
  189. }
  190. /* ═══════════════════════════════════════════════
  191. Main
  192. ═══════════════════════════════════════════════ */
  193. async function main(): Promise<void> {
  194. console.log('=== 风险通知体系 Schema 迁移 ===');
  195. console.log(`Server: ${Parse.serverURL}`);
  196. console.log(`App ID: ${Parse.applicationId}\n`);
  197. await testConnection();
  198. await migrateResponsibilityAssignment();
  199. await migrateRiskEvent();
  200. await migrateAppNotification();
  201. await migrateGroupChat();
  202. console.log('=== 迁移完成 ===\n');
  203. }
  204. main().catch((err) => {
  205. console.error('迁移失败:', err.message);
  206. process.exit(1);
  207. });