audit-mobile-teaching-readonly.mjs 4.7 KB

1234567891011121314151617181920
  1. import {writeFileSync,mkdirSync}from'node:fs';
  2. const base='https://server.xiaoshu.pro',h={'Content-Type':'application/json','X-Parse-Application-Id':'7pIbDBJmKx_main','X-Parse-Master-Key':process.env.XIAOSHU_MASTER_KEY};
  3. async function parse(path){const r=await fetch(base+'/parse'+path,{headers:h});const d=await r.json();if(!r.ok)throw Error(d.error||r.status);return d;}
  4. const sessions=(await parse('/classes/_Session?include=user&limit=1000&order=-updatedAt')).results;
  5. const report={date:new Date().toISOString(),reads:[]};
  6. for(const uid of [Number(process.env.XIAOSHU_AUDIT_TEACHER_ID||88),Number(process.env.XIAOSHU_AUDIT_STUDENT_ID||1761)]){
  7. const s=sessions.find(s=>s.user?.legacyUserId===uid&&new Date(s.expiresAt.iso)>new Date());if(!s){report.reads.push({uid,error:'no session'});continue;}
  8. for(const params of [{action:'app_credit_overview',uid},{action:'app_review_page',uid,page:1,pageSize:20},{action:'app_review_page',uid,page:2,pageSize:20},{action:'app_companion_overview',uid,page:1,pageSize:20},{action:'app_learning_overview',uid,page:1,pageSize:20},{action:'reading/query',className:'SurveyItem',where:{type:'reading'},limit:1}]){
  9. const r=await fetch(base+'/api/functions/xiaoshu/app/gateway',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:s.sessionToken,params}),signal:AbortSignal.timeout(55000)});const d=await r.json(),v=d.result;
  10. const entry={uid,action:params.action,requestPage:params.page,status:r.status,error:d.retmsg,page:v?.page,summary:v?.summary,source:v?.source,items:(v?.items||v?.records||v?.schedules||[]).length,cancelled:v?.schedules?.filter(x=>x.cancelled).length,accounts:v?.accounts};report.reads.push(entry);console.log(JSON.stringify(entry));
  11. }
  12. const first=report.reads.find(r=>r.uid===uid&&r.action==='app_review_page');if(first?.page?.pageCount){const collected=[];for(let page=1;page<=first.page.pageCount;page+=4){const pages=Array.from({length:Math.min(4,first.page.pageCount-page+1)},(_,i)=>page+i);const responses=await Promise.all(pages.map(async p=>{const r=await fetch(base+'/api/functions/xiaoshu/app/gateway',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:s.sessionToken,params:{action:'app_review_page',uid,page:p,pageSize:20}}),signal:AbortSignal.timeout(55000)});const d=await r.json();if(!r.ok)throw Error(d.retmsg||r.status);return d.result.items;}));for(const rows of responses)collected.push(...rows);}const unique=new Set(collected.map(r=>String(r.GeneralID)));const result={uid,action:'review_all_pages',items:collected.length,unique:unique.size,total:first.page.itemCount,complete:unique.size===first.page.itemCount&&unique.size===collected.length};report.reads.push(result);console.log(JSON.stringify(result));}
  13. }
  14. const pending=[];for(let skip=0;;skip+=500){const rows=(await parse('/classes/CourseBinding?limit=500&skip='+skip+'&where='+encodeURIComponent(JSON.stringify({enrollmentStatus:{$ne:'active'}}))+'&keys=objectId,id,yhid,kcid,enrollmentStatus,categoryKey,pendingReason')).results;if(!rows.length)break;pending.push(...rows);}
  15. mkdirSync('tmp/mobile-teaching-verification',{recursive:true});writeFileSync('tmp/mobile-teaching-verification/pending-enrollments.json',JSON.stringify({generatedAt:report.date,count:pending.length,items:pending},null,2));report.pendingEnrollmentCount=pending.length;writeFileSync('tmp/mobile-teaching-verification/read-audit.json',JSON.stringify(report,null,2));
  16. report.businessCounts={};for(const [label,table,where] of [["activeEnrollments","CourseBinding",{enrollmentStatus:"active"}],["pendingEnrollments","CourseBinding",{enrollmentStatus:"pending"}]]){const value=await parse("/classes/"+table+"?count=1&limit=0&where="+encodeURIComponent(JSON.stringify(where)));report.businessCounts[label]=value.count;}console.log(JSON.stringify(report.businessCounts));writeFileSync("tmp/mobile-teaching-verification/read-audit.json",JSON.stringify(report,null,2));
  17. const admin=sessions.find(s=>s.user?.adminRoleKey==='super-admin'&&new Date(s.expiresAt.iso)>new Date());
  18. if(admin){const r=await fetch(base+'/api/functions/xiaoshu/ops/gateway-v3',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:admin.sessionToken,params:{operation:'ops/payroll/review-rewards',companyId:'7pIbDBJmKx',page:1,pageSize:1}})});const d=await r.json();if(!r.ok||!d.success)throw Error(d.message||'工资对账失败');const v=d.data.summary;report.businessCounts={...report.businessCounts,earnedRewards:v.reviewCount,historicalRewards:v.historicalCount,exceptionRewards:v.exceptionCount};console.log(JSON.stringify(report.businessCounts));writeFileSync('tmp/mobile-teaching-verification/read-audit.json',JSON.stringify(report,null,2));}else{throw Error('缺少工资只读核对会话');}