| 1234567891011121314151617181920 |
- #!/usr/bin/env node
- // Install only the affected routines. No historical reward backfill or user creation.
- import fs from 'node:fs';import path from 'node:path';import {randomUUID} from 'node:crypto';
- const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',key=process.env.XIAOSHU_MASTER_KEY,co=process.env.XIAOSHU_COMPANY_ID||'7pIbDBJmKx';
- if(!key)throw Error('缺少 XIAOSHU_MASTER_KEY');
- const out=process.env.XIAOSHU_PAYROLL_REPORT_DIR;if(!out)throw Error('必须指定私有备份目录 XIAOSHU_PAYROLL_REPORT_DIR');fs.mkdirSync(out,{recursive:true,mode:0o700});
- async function api(p,method='GET',body){const r=await fetch(base+p,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':key},...(body?{body:JSON.stringify(body)}:{})});const j=await r.json();if(!r.ok||j.error)throw Error(j.error||'HTTP '+r.status);return j;}
- const names=['xiaoshu_review_before_write','xiaoshu_review_after_write','xiaoshu_complete_review','xiaoshu_reward_immutable','xs_review_reward_confirm'];
- const sources=['review-rewards','payroll-integrity'].map(n=>fs.readFileSync(new URL('./sql/'+n+'.sql',import.meta.url),'utf8')).join('\n');
- const definitions=names.map(name=>{const re=new RegExp('CREATE OR REPLACE FUNCTION '+name+'\\([\\s\\S]*?END \\$\\$;');const m=sources.match(re);if(!m)throw Error('找不到函数 '+name);return m[0];});
- const sessions=(await api('/classes/_Session?include=user&limit=1000&order=-updatedAt')).results,session=sessions.find(s=>s.user?.adminRoleKey==='super-admin'&&!s.user?.isDisabled&&s.user?.company?.objectId===co&&new Date(s.expiresAt?.iso)>new Date());if(!session)throw Error('缺少有效的帐套超级管理员会话');
- const route='xiaoshu/system/payroll-repair-'+randomUUID(),expires=Date.now()+600000;let id;
- try{
- const readSql="SELECT proname,pg_get_functiondef(p.oid) AS definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])";
- const code=`async function handler(request,response){if(!request.user||request.user.id!==${JSON.stringify(session.user.objectId)}||Date.now()>${expires})return response.status(403).json({success:false});try{const apply=(request.body.params||{}).apply===true;if(apply)await Psql.tx(async t=>{await t.none(${JSON.stringify(definitions.join('\n'))});});return response.json({success:true,data:await Psql.query(${JSON.stringify(readSql)},[${JSON.stringify(names)}])});}catch(e){return response.status(500).json({success:false,message:e.message});}}`;
- new Function(code);id=(await api('/classes/Function','POST',{name:route,path:route,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'})).objectId;
- async function call(apply){const r=await fetch(base.replace(/\/parse$/,'/api/functions/')+route,{method:'POST',headers:{'Content-Type':'application/json','X-Parse-Application-Id':app},body:JSON.stringify({token:session.sessionToken,params:{apply}})}),j=await r.json();if(!r.ok||!j.success)throw Error(j.message||'执行失败');return j.data;}
- const stamp=Date.now();fs.writeFileSync(path.join(out,'routines-before-'+stamp+'.json'),JSON.stringify(await call(false),null,2),{mode:0o600});
- if(process.argv.includes('--apply')){const after=await call(true);if(after.length!==names.length)throw Error('安装后函数数量不符');fs.writeFileSync(path.join(out,'routines-after-'+stamp+'.json'),JSON.stringify(after,null,2),{mode:0o600});console.log(JSON.stringify({applied:true,routines:after.map(r=>r.proname)}));}else console.log(JSON.stringify({applied:false,routines:names}));
- }finally{if(id){await api('/classes/Function/'+id,'DELETE');const remaining=await api('/classes/Function?'+new URLSearchParams({where:JSON.stringify({objectId:id}),count:'1',limit:'0'}));if(remaining.count)throw Error('临时函数未清理');}}
|