migrate-review-rewards.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839
  1. #!/usr/bin/env node
  2. import { readFileSync } from 'node:fs';
  3. import { randomBytes } from 'node:crypto';
  4. const sql=readFileSync(new URL('./sql/review-rewards.sql',import.meta.url),'utf8');
  5. if(!process.argv.includes('--apply')) {
  6. console.log('只读检查:复习奖励迁移脚本已就绪。使用 --apply 执行;历史已完成记录只生成零金额待核对凭据。');
  7. process.exit(0);
  8. }
  9. const master=process.env.XIAOSHU_MASTER_KEY;
  10. if(!master)throw new Error('缺少 XIAOSHU_MASTER_KEY');
  11. const parseUrl=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,'');
  12. const appId=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main';
  13. async function request(path,options={}) {
  14. const res=await fetch(parseUrl+path,{...options,headers:{'Content-Type':'application/json','X-Parse-Application-Id':appId,'X-Parse-Master-Key':master}});
  15. const data=await res.json();if(!res.ok||data.error)throw new Error(data.error||`HTTP ${res.status}`);return data;
  16. }
  17. // Register only the new API-visible payroll fields before installing physical constraints.
  18. for(const [name,fields] of Object.entries({PayrollLine:{reviewRewardId:{type:'String'},reviewTaskId:{type:'String'},plannedDate:{type:'String'}},PayrollBatch:{reviewCount:{type:'Number'},reviewAmount:{type:'Number'}}})) {
  19. const schema=await request('/schemas/'+name),missing=Object.fromEntries(Object.entries(fields).filter(([key])=>!schema.fields[key]));
  20. if(Object.keys(missing).length)await request('/schemas/'+name,{method:'PUT',body:JSON.stringify({fields:missing})});
  21. }
  22. const company=(await request('/classes/Company?limit=1&keys=objectId')).results?.[0];
  23. if(!company)throw new Error('未找到帐套');
  24. const suffix=Date.now()+'_'+randomBytes(5).toString('hex'),path='xiaoshu/system/review-rewards-'+suffix;
  25. let userId='',functionId='';
  26. try {
  27. const account=await request('/users',{method:'POST',body:JSON.stringify({username:'review_migration_'+suffix,password:randomBytes(32).toString('base64url'),isAdmin:true,adminRoleKey:'super-admin',roles:['admin','super-admin'],company:{__type:'Pointer',className:'Company',objectId:company.objectId},testCreatedBy:'review-rewards-migration'})});userId=account.objectId;
  28. if(!account.sessionToken)throw new Error('临时管理员没有会话');
  29. const code=`async function handler(request,response){try{const current=request.user;if(!current)return response.status(401).json({success:false});await current.fetch({useMasterKey:true});if(current.id!==${JSON.stringify(userId)}||current.get('adminRoleKey')!=='super-admin')return response.status(403).json({success:false});await Psql.none(${JSON.stringify(sql)});const counts=await Psql.query('SELECT "status",COUNT(*)::int AS count,SUM("amount") AS amount FROM "ReviewReward" GROUP BY "status"');return response.json({success:true,counts});}catch(error){return response.status(500).json({success:false,message:String(error.message||error)});}}`;
  30. const fn=await request('/classes/Function',{method:'POST',body:JSON.stringify({name:path,path,code,type:'standalone',desc:'复习奖励一次性数据库迁移',enabled:true,params:[],paramList:[],respType:'json'})});functionId=fn.objectId;
  31. const res=await fetch(parseUrl.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:account.sessionToken,params:{}})});
  32. const data=await res.json();if(!res.ok||!data.success)throw new Error(data.message||'迁移失败');console.log(JSON.stringify(data,null,2));
  33. } finally {
  34. // Cleanup failures are reported, including IDs, so no temporary privileged surface is silently left behind.
  35. const failures=[];
  36. if(functionId)try{await request('/classes/Function/'+functionId,{method:'DELETE'});}catch{failures.push('临时函数 '+functionId);}
  37. if(userId)try{await request('/users/'+userId,{method:'DELETE'});}catch{failures.push('临时管理员 '+userId);}
  38. if(failures.length)throw new Error('需要清理:'+failures.join('、'));
  39. }