migrate-lesson-records.mjs 3.1 KB

12345678910111213141516171819
  1. #!/usr/bin/env node
  2. import {readFileSync} from 'node:fs';
  3. import {randomUUID} from 'node:crypto';
  4. const sql=readFileSync(new URL('./sql/lesson-records.sql',import.meta.url),'utf8');
  5. if(!process.argv.includes('--apply')){console.log('上课记录迁移就绪:新增复习转交、审计和并发保护;不转交任何现有任务。使用 --apply 执行。');process.exit(0);}
  6. const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN||process.env.XIAOSHU_SESSION_TOKEN;
  7. if(!master||!token)throw Error('需要受保护环境中的主密钥和有效管理会话');
  8. const app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,'');
  9. async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:body?JSON.stringify(body):undefined});const d=await r.json();if(!r.ok||d.error)throw Error(d.error||'数据库请求失败');return d;}
  10. const sessions=(await parse('/classes/_Session?'+new URLSearchParams({where:JSON.stringify({sessionToken:token}),include:'user',limit:'2'}))).results;
  11. const s=sessions?.[0];if(sessions.length!==1||!['super-admin','ops-manager'].includes(s.user?.adminRoleKey)||s.user.isDisabled||s.user.isDeleted||new Date(s.expiresAt?.iso)<new Date())throw Error('管理会话不可用');
  12. const co=s.user.company?.objectId;if(!co)throw Error('缺少帐套');
  13. const schema=await parse('/schemas/MemoryPracticeRecord');if(!schema.fields?.assignmentVersion)await parse('/schemas/MemoryPracticeRecord','PUT',{fields:{assignmentVersion:{type:'Number'}}});
  14. const path='xiaoshu/system/lesson-records-migrate-'+randomUUID(),expires=Date.now()+600000;let id;
  15. try{
  16. const code=`async function handler(request,response){try{if(request.user?.id!==${JSON.stringify(s.user.objectId)}||Date.now()>${expires})return response.status(403).json({success:false});await request.user.fetch({useMasterKey:true});if(request.user.get('isDisabled')||request.user.get('isDeleted'))return response.status(403).json({success:false});const active=await Psql.one('SELECT count(*)::int n FROM "LegacyDirectRun" WHERE company=$1 AND status=$2 AND "leaseUntil">now()',[${JSON.stringify(co)},'running']);if(active.n)throw Error('夜间同步正在运行,暂缓迁移');await Psql.none(${JSON.stringify(sql)});return response.json({success:true});}catch(e){return response.status(500).json({success:false,message:e.message});}}`;
  17. id=(await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'})).objectId;
  18. const response=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{}})}),result=await response.json();if(!response.ok||!result.success)throw Error(result.message||'迁移失败');console.log('上课记录与复习转交迁移成功;现有任务未转交。');
  19. }finally{if(id)await parse('/classes/Function/'+id,'DELETE');}