| 1234567891011121314151617 |
- #!/usr/bin/env node
- // Install only the affected routines, preserving their deployed definitions.
- import{readFileSync,writeFileSync,mkdirSync}from'node:fs';import{randomUUID}from'node:crypto';
- const names=['xs_lesson_complete'];
- const sql=readFileSync(new URL('./sql/lesson-workflow.sql',import.meta.url),'utf8').match(/CREATE OR REPLACE FUNCTION xs_lesson_complete\([\s\S]*?END \$\$;/)[0]+'\nREVOKE ALL ON FUNCTION xs_lesson_complete(text,text,text,numeric,timestamptz,text,jsonb) FROM PUBLIC;';
- if(!process.argv.includes('--apply')){console.log('课堂课本选择及多词库学习汇总已就绪;--apply 仅安装函数,不改变预约或余额。');process.exit(0);}
- const key=process.env.XIAOSHU_MASTER_KEY,out=process.env.XIAOSHU_LESSON_COURSES_REPORT_DIR,base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',co=process.env.XIAOSHU_COMPANY_ID||'7pIbDBJmKx';
- if(!key||!out)throw Error('需要部署凭据与私有备份目录');mkdirSync(out,{recursive:true,mode:0o700});
- async function api(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':key},...(body?{body:JSON.stringify(body)}:{})});const j=await r.json();if(!r.ok||j.error)throw Error(j.error||'部署请求失败');return j;}
- const sessions=(await api('/classes/_Session?include=user&limit=1000&order=-updatedAt')).results,s=sessions.find(s=>s.user?.adminRoleKey==='super-admin'&&!s.user?.isDisabled&&s.user?.company?.objectId===co&&new Date(s.expiresAt?.iso)>new Date());if(!s)throw Error('缺少有效的帐套管理员会话');
- const path='xiaoshu/system/lesson-courses-'+randomUUID(),expiry=Date.now()+600000;let id;
- try{
- const code=`async function handler(request,response){if(!request.user||request.user.id!==${JSON.stringify(s.user.objectId)}||Date.now()>${expiry})return response.status(403).json({success:false});try{if((request.body.params||{}).apply)await Psql.tx(async t=>t.none(${JSON.stringify(sql)}));return response.json({success:true,data:await Psql.query('SELECT proname,pg_get_functiondef(oid) AS definition FROM pg_proc WHERE proname=ANY($1::text[])',[${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:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'})).objectId;
- async function call(apply){const r=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json','X-Parse-Application-Id':app},body:JSON.stringify({token:s.sessionToken,params:{apply}})}),j=await r.json();if(!r.ok||!j.success)throw Error(j.message||'安装失败');return j.data;}
- const stamp=Date.now();writeFileSync(out+'/routine-before-'+stamp+'.json',JSON.stringify(await call(false),null,2),{mode:0o600});const after=await call(true);if(after.length!==names.length)throw Error('函数安装未确认');writeFileSync(out+'/routine-after-'+stamp+'.json',JSON.stringify(after,null,2),{mode:0o600});console.log('Installed multi-book lesson completion routine; no appointments or balances changed.');
- }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('临时函数未清理');}}
|