Просмотр исходного кода

perf(admin): speed data reads and separate dashboard estimates from settlement checks

彭峰 23 часов назад
Родитель
Сommit
30495e7e81

+ 30 - 0
perf/admin-data-2026-10-15/browser-case.mjs

@@ -0,0 +1,30 @@
+import {chromium} from 'playwright';
+export async function startBrowser(){return chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});}
+const pages={ 'ops/users/list':['members','app-operations-users table tbody tr'], 'ops/stores/list':['stores','app-operations-stores table tbody tr'], 'ops/schedule/calendar':['schedule','[aria-busy="false"].content-card'], 'ops/dashboard/summary':['dashboard','.overview-metrics'], 'ops/payroll/overview':['payroll','.payroll-summary-table tbody tr'] };
+export async function browserCase(browser,{variant,operation,path,token,me,pair,hash,origin='http://127.0.0.1:4300'}){
+ const [pagePath,selector]=pages[operation],ctx=await browser.newContext({viewport:{width:1440,height:1000}}),records=[],errors=[];
+ await ctx.addInitScript(auth=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth)),{objectId:me.objectId,sessionToken:token,username:'性能验收',roles:me.roles,operationsRole:'ops-manager',isSuperAdmin:true});
+ let complete;const received=new Promise(resolve=>complete=resolve);
+ const page=await ctx.newPage();page.on('pageerror',e=>errors.push(e.message));
+ await page.route('**/cloud-functions/xiaoshu/ops/gateway-v3',async route=>{
+  const req=route.request(),data=req.postDataJSON();if(data?.params?.operation!==operation)return route.continue();
+  const start=performance.now();try{const r=await route.fetch({url:'https://server.xiaoshu.pro/api/functions/'+path,timeout:90000}),raw=await r.body(),json=JSON.parse(raw.toString());records.push({ms:Math.round(performance.now()-start),bytes:raw.length,hash:hash(json.data),success:json.success!==false,status:r.status()});await route.fulfill({response:r,body:raw});complete();}catch(e){errors.push(e.message);await route.abort().catch(()=>{});complete();}
+ });
+ try{
+  const start=performance.now();await page.goto(origin+'/admin/'+pagePath,{waitUntil:'domcontentloaded'});await received;
+  const ready=operation==='ops/users/list'?'table tbody tr':operation==='ops/stores/list'?'table tbody tr':selector;
+  await page.locator(ready).first().waitFor({state:'visible',timeout:60000});
+  await page.waitForFunction(()=>!document.querySelector('.schedule-loading')&&!document.querySelector('[role="alert"]'));
+  if(!records.length||records.some(r=>!r.success))throw Error('No successful primary data request');
+  await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));const readyMs=Math.round(performance.now()-start);
+  if(pair===0){await page.screenshot({path:`tmp/admin-data-2026-10-15/browser-${pagePath}-${variant}-1440.png`,fullPage:false,animations:'disabled'});await page.setViewportSize({width:1000,height:900});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));await page.waitForTimeout(200);await page.screenshot({path:`tmp/admin-data-2026-10-15/browser-${pagePath}-${variant}-1000.png`,fullPage:false,animations:'disabled'});}
+  let menuReturn;
+  if(process.env.PERF_MENU_RETURN&&variant==='B'&&operation!=='ops/schedule/calendar'){
+   await page.setViewportSize({width:1440,height:1000});
+   await page.locator('#admin-sidebar a[href="/admin/support-requests"]').last().click();await page.waitForTimeout(500);
+   const count=records.length,at=performance.now();await page.locator('#admin-sidebar a[href="/admin/'+pagePath+'"]').last().click();await page.locator(ready).first().waitFor({state:'visible'});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
+   menuReturn={ms:Math.round(performance.now()-at),primaryRequests:records.length-count};
+  }
+  return{operation,pair,variant,ms:readyMs,requests:records,menuReturn,errors};
+ }finally{await ctx.close();}
+}

+ 114 - 0
perf/admin-data-2026-10-15/browser-results.json

@@ -0,0 +1,114 @@
+[
+  {
+    "operation": "ops/users/list",
+    "A": {
+      "n": 10,
+      "p50": 2111,
+      "p75": 2264,
+      "p95": 2516,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        23190
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 1129,
+      "p75": 1169,
+      "p95": 1281,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        23190
+      ]
+    },
+    "p75ReductionPercent": 48.4
+  },
+  {
+    "operation": "ops/stores/list",
+    "A": {
+      "n": 10,
+      "p50": 3299,
+      "p75": 3489,
+      "p95": 3546,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        5264
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 2308,
+      "p75": 2430,
+      "p95": 3428,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        5264
+      ]
+    },
+    "p75ReductionPercent": 30.4
+  },
+  {
+    "operation": "ops/schedule/calendar",
+    "A": {
+      "n": 10,
+      "p50": 11901,
+      "p75": 12032,
+      "p95": 12249,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        439819
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 2501,
+      "p75": 2584,
+      "p95": 2914,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        439819
+      ]
+    },
+    "p75ReductionPercent": 78.5
+  },
+  {
+    "operation": "ops/dashboard/summary",
+    "A": {
+      "n": 10,
+      "p50": 15101,
+      "p75": 15519,
+      "p95": 16476,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        5528
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 6080,
+      "p75": 6202,
+      "p95": 6399,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        5124
+      ]
+    },
+    "p75ReductionPercent": 60
+  }
+]

+ 15 - 0
perf/admin-data-2026-10-15/candidate-code.mjs

@@ -0,0 +1,15 @@
+import {patchAdminReadHelpers} from '../../scripts/admin-read-performance-patch.mjs';
+import {readFileSync} from 'node:fs';
+// Replace only the two measured read helpers in an exact online baseline.
+export function candidateCode(baseline,{temporaryView=false}={}){
+ baseline=patchAdminReadHelpers(baseline);
+ if(!temporaryView)return baseline;
+ return withTemporaryIncomeView(baseline);
+}
+export function withTemporaryIncomeView(baseline,legacy=false){
+ // A transaction-local view exercises the SQL migration without changing the live view.
+ // It is dropped before releasing the pooled connection; failures roll the DDL back.
+ const ddl=readFileSync(legacy?'perf/admin-data-2026-10-15/legacy-income-view.sql':'scripts/sql/admin-read-performance.sql','utf8').replace('CREATE OR REPLACE VIEW','CREATE TEMP VIEW');
+ const proxy=`const __candidateSql=new Proxy({},{get(_,key){const fn=Psql[key];if(!['query','one','oneOrNone','any','manyOrNone'].includes(key))return typeof fn==='function'?fn.bind(Psql):fn;return function(sql,args){if(typeof sql!=='string'||!/ReviewRewardIncome|xs_payroll_cutoff_snapshot|xs_payroll_source_version/.test(sql))return fn.call(Psql,sql,args);return Psql.tx(async tx=>{await tx.none('SET LOCAL search_path=pg_temp,public');await tx.none(${JSON.stringify(ddl)});const result=await tx[key](sql,args);await tx.none('DROP VIEW pg_temp."ReviewRewardIncome"');return result;});};}});\n`;
+ return proxy+baseline.replaceAll('Psql.','__candidateSql.');
+}

+ 6 - 0
perf/admin-data-2026-10-15/legacy-income-view.sql

@@ -0,0 +1,6 @@
+CREATE OR REPLACE VIEW "ReviewRewardIncome" AS
+ SELECT projected.*,r.amount AS "originalAmount",c."objectId" AS "correctionId",c.reason AS "correctionReason",c.amount AS "correctionAmount",c."adjustmentBatchId"
+ FROM "ReviewReward" r LEFT JOIN "ReviewRewardCorrection" c ON c.company=r.company AND c."rewardId"=r."objectId"
+ CROSS JOIN LATERAL jsonb_populate_record(NULL::"ReviewReward",to_jsonb(r)||jsonb_build_object(
+ 'amount',r.amount+COALESCE(c.amount,0),'status',CASE WHEN c."objectId" IS NULL THEN r.status ELSE 'ineligible' END,
+ 'ineligibleReason',CASE WHEN c."objectId" IS NULL THEN r."ineligibleReason" ELSE 'corrected_overdue' END)) projected;

+ 3 - 1
perf/admin-data-2026-10-15/legacy-read-queries.json

@@ -1,4 +1,6 @@
 {
   "courses": "WITH RECURSIVE courses AS (SELECT DISTINCT n.\"company\",n.\"nodeId\",n.\"parentId\",n.\"nodeName\" FROM \"Node\" n WHERE ($1::text IS NULL OR n.\"company\"=$1) AND (CAST(n.\"nodeId\" AS text)='398' OR EXISTS(SELECT 1 FROM \"CourseBinding\" b WHERE b.\"company\"=n.\"company\" AND CAST(b.\"kcid\" AS text)=CAST(n.\"nodeId\" AS text)) OR EXISTS(SELECT 1 FROM \"CourseAppointment\" a WHERE a.\"company\"=n.\"company\" AND CAST(a.\"kcid\" AS text)=CAST(n.\"nodeId\" AS text)))), ancestors AS (SELECT c.\"company\",c.\"nodeId\" AS course_id,c.\"nodeId\",c.\"parentId\",c.\"nodeName\",0 AS depth FROM courses c UNION ALL SELECT a.\"company\",a.course_id,p.\"nodeId\",p.\"parentId\",p.\"nodeName\",a.depth+1 FROM ancestors a JOIN \"Node\" p ON p.\"company\"=a.\"company\" AND CAST(p.\"nodeId\" AS text)=CAST(a.\"parentId\" AS text) WHERE a.depth<12) SELECT CAST(c.\"nodeId\" AS text) AS id,c.\"nodeName\" AS name,COALESCE(ARRAY_AGG(a.\"nodeName\" ORDER BY a.depth) FILTER(WHERE COALESCE(a.\"nodeName\",'')<>''),ARRAY[]::text[]) AS ancestor_names FROM courses c LEFT JOIN ancestors a ON a.\"company\"=c.\"company\" AND a.course_id=c.\"nodeId\" GROUP BY c.\"nodeId\",c.\"nodeName\" ORDER BY c.\"nodeName\" LIMIT 2000",
-  "appointments": "SELECT a.\"szyh\"::text AS uid,COUNT(*)::int AS appointment_count,COUNT(*) FILTER(WHERE active AND COALESCE(a.\"dszt\",'0')::text IN ('0','10','11','20'))::int AS unfinished,COUNT(*) FILTER(WHERE active AND a.\"dslx\"::text='3')::int AS trial_count,MIN(CASE WHEN active AND COALESCE(a.\"dszt\",'0')::text='0' AND COALESCE(a.\"yysj\",a.\"bxrq\")>=to_char(CURRENT_DATE,'YYYY-MM-DD') THEN COALESCE(a.\"yysj\",a.\"bxrq\") END) AS next_at,COUNT(*) FILTER(WHERE active AND COALESCE(a.\"dszt\",'0')::text NOT IN ('0','10','11','20','30','40'))::int AS abnormal FROM (SELECT source.*,NOT EXISTS(SELECT 1 FROM \"CommonModel\" c WHERE c.\"company\"=source.\"company\" AND c.\"modelId\"::text='54' AND c.\"itemId\"::text=source.\"id\"::text AND c.\"status\"::text='-2') AS active FROM \"CourseAppointment\" source WHERE ($1::text IS NULL OR source.\"company\"=$1)) a GROUP BY a.\"szyh\"::text"
+  "appointments": "SELECT a.\"szyh\"::text AS uid,COUNT(*)::int AS appointment_count,COUNT(*) FILTER(WHERE active AND COALESCE(a.\"dszt\",'0')::text IN ('0','10','11','20'))::int AS unfinished,COUNT(*) FILTER(WHERE active AND a.\"dslx\"::text='3')::int AS trial_count,MIN(CASE WHEN active AND COALESCE(a.\"dszt\",'0')::text='0' AND COALESCE(a.\"yysj\",a.\"bxrq\")>=to_char(CURRENT_DATE,'YYYY-MM-DD') THEN COALESCE(a.\"yysj\",a.\"bxrq\") END) AS next_at,COUNT(*) FILTER(WHERE active AND COALESCE(a.\"dszt\",'0')::text NOT IN ('0','10','11','20','30','40'))::int AS abnormal FROM (SELECT source.*,NOT EXISTS(SELECT 1 FROM \"CommonModel\" c WHERE c.\"company\"=source.\"company\" AND c.\"modelId\"::text='54' AND c.\"itemId\"::text=source.\"id\"::text AND c.\"status\"::text='-2') AS active FROM \"CourseAppointment\" source WHERE ($1::text IS NULL OR source.\"company\"=$1)) a GROUP BY a.\"szyh\"::text",
+  "scheduleOptions": "WITH binding_courses AS (SELECT b.\"company\",CAST(b.\"kcid\" AS text) AS course_id,ARRAY_AGG(DISTINCT CAST(b.\"yhid\" AS text)) FILTER (WHERE COALESCE(CAST(b.\"yhid\" AS text),'')<>'') AS member_ids FROM \"CourseBinding\" b JOIN \"CommonModel\" c ON c.\"company\"=b.\"company\" AND CAST(c.\"modelId\" AS text)='58' AND CAST(c.\"itemId\" AS text)=CAST(b.\"id\" AS text) WHERE ($1::text IS NULL OR b.\"company\"=$1) AND COALESCE(CAST(c.\"status\" AS text),'99')<>'-2' GROUP BY b.\"company\",CAST(b.\"kcid\" AS text)), appointment_courses AS (SELECT DISTINCT \"company\",CAST(\"kcid\" AS text) AS course_id FROM \"CourseAppointment\" WHERE ($1::text IS NULL OR \"company\"=$1)), course_ids AS (SELECT \"company\",course_id FROM binding_courses UNION SELECT \"company\",course_id FROM appointment_courses) SELECT CAST(n.\"nodeId\" AS text) AS id,n.\"nodeName\" AS name,COALESCE(b.member_ids,ARRAY[]::text[]) AS member_ids FROM \"Node\" n LEFT JOIN binding_courses b ON b.\"company\"=n.\"company\" AND b.course_id=CAST(n.\"nodeId\" AS text) WHERE ($1::text IS NULL OR n.\"company\"=$1) AND (CAST(n.\"nodeId\" AS text)='398' OR EXISTS(SELECT 1 FROM course_ids i WHERE i.\"company\"=n.\"company\" AND i.course_id=CAST(n.\"nodeId\" AS text))) ORDER BY n.\"nodeName\" LIMIT 1000",
+  "memberLessons": "SELECT CAST(l.\"xymz\" AS text) AS uid,MAX(COALESCE(CAST(l.\"lessonAt\" AS text),NULLIF(l.\"plpjsj\",''),CAST(c.\"createdAt\" AS text),NULLIF(a.\"jssj\",''))) AS last_at,COUNT(*) FILTER (WHERE CAST(l.\"kclx\" AS text) IN ('1','2','4'))::int AS paid_count,COUNT(*) FILTER (WHERE CAST(l.\"kclx\" AS text)='3')::int AS trial_count FROM \"LessonRecord\" l LEFT JOIN \"CommonModel\" c ON c.\"company\"=l.\"company\" AND CAST(c.\"modelId\" AS text)='59' AND CAST(c.\"itemId\" AS text)=CAST(l.\"id\" AS text) LEFT JOIN \"CommonModel\" ac ON ac.\"company\"=l.\"company\" AND CAST(ac.\"modelId\" AS text)='54' AND CAST(ac.\"generalId\" AS text)=CAST(l.\"yyds\" AS text) LEFT JOIN \"CourseAppointment\" a ON a.\"company\"=l.\"company\" AND CAST(a.\"id\" AS text)=CAST(ac.\"itemId\" AS text) WHERE ($1::text IS NULL OR l.\"company\"=$1) GROUP BY CAST(l.\"xymz\" AS text)"
 }

+ 31 - 0
perf/admin-data-2026-10-15/paired-api.mjs

@@ -0,0 +1,31 @@
+import {startBrowser,browserCase} from './browser-case.mjs';
+import {readFileSync,writeFileSync} from 'node:fs';
+import {createHash,randomBytes} from 'node:crypto';
+import {candidateCode,withTemporaryIncomeView} from './candidate-code.mjs';
+const base='https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN,master=process.env.XIAOSHU_MASTER_KEY;
+if(!app||!token||!master)throw Error('Missing authorized configuration');
+const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},out='tmp/admin-data-2026-10-15';
+async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(20000)}),d=await r.json();if(!r.ok||d.error)throw Error('Setup HTTP '+r.status);return d;}
+const me=await(await fetch(base+'/users/me',{headers:{'X-Parse-Application-Id':app,'X-Parse-Session-Token':token}})).json();if(!me.objectId)throw Error('Invalid operator');
+const operations=process.argv.slice(2).length?process.argv.slice(2):['ops/users/list','ops/stores/list','ops/schedule/calendar','ops/dashboard/summary','ops/payroll/overview'];
+const saved=JSON.parse(readFileSync(out+'/online-before.json','utf8')),expiry=Date.now()+3600000,paths={},created=[];
+// Clock metadata and the dashboard-only settlement-status metadata are omitted.
+// Dashboard estimates explicitly cannot authorize settlement; money, counts and all other fields stay exact. Row ordering, amounts, permissions and statuses remain exact.
+function clean(x,path=[]){if(Array.isArray(x))return x.map((v,i)=>clean(v,[...path,String(i)]));if(x&&typeof x==='object')return Object.fromEntries(Object.keys(x).sort().filter(k=>!['refreshedAt','lagSeconds'].includes(k)&&!(path.join('.')==='payroll.sourceStatus'&&['sync','complete','stale'].includes(k))).map(k=>[k,clean(x[k],[...path,k])]));return x;}
+const hash=x=>createHash('sha256').update(JSON.stringify(clean(x))).digest('hex');
+const measurements=[],browser=process.env.PERF_BROWSER?await startBrowser():null;
+try{
+ for(const variant of ['A','B']){
+  const body=variant==='A'?withTemporaryIncomeView(saved.code,!process.env.PERF_CURRENT_VIEW):candidateCode(saved.code,{temporaryView:true});
+  const guard=`\nconst __original=handler;handler=async function(request,response){if(request.user?.id!==${JSON.stringify(me.objectId)}||Date.now()>${expiry}||!${JSON.stringify(operations)}.includes(request.body?.params?.operation))return response.status(403).json({success:false});return __original(request,response);};`;
+  const path='xiaoshu/system/perf-pair-'+randomBytes(10).toString('hex');const fn=await parse('/classes/Function','POST',{name:path,path,code:body+guard,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});created.push(fn.objectId);paths[variant]=path;
+ }
+ for(const operation of operations){for(let pair=0;pair<Number(process.env.PERF_ROUNDS||10);pair++){const results=[];
+  for(const variant of ['A','B']){if(browser){const entry=await browserCase(browser,{variant,operation,path:paths[variant],token,me,pair,hash});entry.hash=hash(entry.requests.map(r=>r.hash));measurements.push(entry);results.push(entry);continue;}const params={operation,page:1,pageSize:operation==='ops/vocabulary/list'?30:20,search:'',month:'2026-10',dateFrom:'2026-10-15',dateTo:'2026-10-21',source:'database',refresh:true,filters:operation==='ops/vocabulary/list'?{nodeId:0,status:'',missingAudio:false}:{status:'all'}},start=performance.now();
+   const r=await fetch(base.replace('/parse','/api/functions/')+paths[variant],{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params}),signal:AbortSignal.timeout(90000)});const ttfb=performance.now()-start,raw=await r.text(),data=JSON.parse(raw),elapsed=performance.now()-start;if(!r.ok||data.success===false)throw Error(operation+' '+variant+' failed: '+data.message);
+   if(pair===0)writeFileSync(out+'/pair-'+operation.replaceAll('/','-')+'-'+variant+'.json',raw,{mode:0o600});
+   const entry={operation,pair,variant,ms:Math.round(elapsed),ttfbMs:Math.round(ttfb),bytes:Buffer.byteLength(raw),hash:hash(data.data)};measurements.push(entry);results.push(entry);
+  }
+  const equal=results[0].hash===results[1].hash;console.log(JSON.stringify({operation,pair,A:results[0].ms,B:results[1].ms,equal}));writeFileSync(out+'/'+(process.env.PERF_OUTPUT||(browser?'paired-browser.json':'paired-api.json')),JSON.stringify(measurements,null,2),{mode:0o600});if(!equal)throw Error('Golden output changed: '+operation);
+ }}
+}finally{if(browser)await browser.close();for(const id of created)await parse('/classes/Function/'+id,'DELETE');}

+ 17 - 0
perf/admin-data-2026-10-15/profile-server.mjs

@@ -0,0 +1,17 @@
+import {candidateCode} from './candidate-code.mjs';
+import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
+import {randomBytes} from 'node:crypto';
+const base='https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;if(!master||!token)throw Error('Missing authorized config');
+const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
+async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(20000)});const d=await r.json();if(!r.ok||d.error)throw Error('Diagnostic setup failed HTTP '+r.status);return d;}
+const meR=await fetch(base+'/users/me',{headers:{'X-Parse-Application-Id':app,'X-Parse-Session-Token':token}}),me=await meR.json();if(!me.objectId)throw Error('Unknown operator');
+const row=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results;if(row.length!==1)throw Error('Ambiguous gateway');
+const out='tmp/admin-data-2026-10-15';mkdirSync(out,{recursive:true,mode:0o700});writeFileSync(out+'/online-before.json',JSON.stringify(row[0]),{mode:0o600});
+const operations=process.argv.slice(2).length?process.argv.slice(2):['ops/dashboard/summary','ops/stores/list','ops/users/list','ops/coaches/list','ops/schedule/calendar','ops/payroll/overview'];
+const prelude=`const __perf=[];const __perfSql=new Proxy({},{get(target,key){const f=Psql[key];if(typeof f!=='function')return f;return async function(...args){const at=Date.now();try{return await f.apply(Psql,args);}finally{__perf.push({kind:'sql',method:key,ms:Date.now()-at,query:typeof args[0]==='string'?args[0]:String(key),args:typeof args[0]==='string'?args[1]:undefined});}};}});class __PerfQuery extends Parse.Query {async find(...args){const at=Date.now();try{return await super.find(...args);}finally{__perf.push({kind:'parse',method:'find',className:this.className,ms:Date.now()-at});}}async count(...args){const at=Date.now();try{return await super.count(...args);}finally{__perf.push({kind:'parse',method:'count',className:this.className,ms:Date.now()-at});}}}`;
+const baseCode=process.env.PERF_CANDIDATE?candidateCode(row[0].code,{temporaryView:true}):row[0].code;
+const code=prelude+'\n'+baseCode.replaceAll('Psql.','__perfSql.').replaceAll('new Parse.Query(','new __PerfQuery(')+`\nconst __original=handler;handler=async function(request,response){if(request.user?.id!==${JSON.stringify(me.objectId)}||Date.now()>${Date.now()+900000}||!${JSON.stringify(operations)}.includes(request.body?.params?.operation))return response.status(403).json({success:false});const start=Date.now();let status=200;const wrapped={status(s){status=s;return this;},json(body){return response.status(status).json({...body,__profile:{elapsedMs:Date.now()-start,entries:__perf}});}};return __original(request,wrapped);};`;
+new Function(code);const path='xiaoshu/system/perf-read-'+randomBytes(10).toString('hex');let fn;
+try{fn=await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});
+for(const operation of operations){const at=Date.now(),params={operation,page:1,pageSize:20,month:'2026-10',dateFrom:'2026-10-15',dateTo:'2026-10-21',source:'database',filters:{status:'all'}};const r=await fetch(base.replace('/parse','/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params}),signal:AbortSignal.timeout(90000)});const d=await r.json();writeFileSync(out+'/'+operation.replaceAll('/','-')+(process.env.PERF_CANDIDATE?'-candidate':'')+'-profile.json',JSON.stringify(d),{mode:0o600});console.log(JSON.stringify({operation,http:r.status,elapsedMs:Date.now()-at,serverMs:d.__profile?.elapsedMs,queries:d.__profile?.entries?.length,top:d.__profile?.entries?.toSorted((a,b)=>b.ms-a.ms).slice(0,6).map(({args,...e})=>({...e,query:e.query?.slice(0,200)})),error:d.message}));}
+}finally{if(fn)await parse('/classes/Function/'+fn.objectId,'DELETE');}

+ 6 - 0
perf/admin-data-2026-10-15/ratchet.mjs

@@ -0,0 +1,6 @@
+// Manual live-data gate: run in the same environment; keep noisy WAN checks out of offline CI.
+import {readFileSync} from 'node:fs';
+const baseline=JSON.parse(readFileSync(new URL('./browser-results.json',import.meta.url),'utf8'));
+const next=JSON.parse(readFileSync(process.argv[2],'utf8'));
+for(const expected of baseline){const actual=next.find(r=>r.operation===expected.operation);if(!actual||actual.B.n<10)throw Error('Missing 10 paired samples: '+expected.operation);if(actual.B.p75>expected.B.p75)throw Error('p75 regressed: '+expected.operation);}
+console.log('All measured p75 ceilings passed');

+ 12 - 0
perf/admin-data-2026-10-15/sql-diagnostic.mjs

@@ -0,0 +1,12 @@
+// Temporary, operator-bound read-only diagnostics; no persistent application changes.
+import {readFileSync,writeFileSync} from 'node:fs';
+import {randomBytes} from 'node:crypto';
+const app=process.env.XIAOSHU_PARSE_APP_ID,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN,master=process.env.XIAOSHU_MASTER_KEY,base='https://server.xiaoshu.pro/parse';
+if(!app||!token||!master)throw Error('Missing authorized config');
+const headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
+async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(20000)});const d=await r.json();if(!r.ok||d.error)throw Error('Diagnostic setup failed HTTP '+r.status);return d;}
+const me=await(await fetch(base+'/users/me',{headers:{'X-Parse-Application-Id':app,'X-Parse-Session-Token':token}})).json();if(!me.objectId)throw Error('Invalid operator');
+const jobs=JSON.parse(readFileSync(process.argv[2],'utf8'));if(jobs.some(j=>!/^\s*(SELECT|WITH|EXPLAIN)\b/i.test(j.sql)))throw Error('Select jobs required');
+const tempView=readFileSync('scripts/sql/admin-read-performance.sql','utf8').replace('CREATE OR REPLACE VIEW','CREATE TEMP VIEW');
+const path='xiaoshu/system/perf-sql-'+randomBytes(10).toString('hex'),code=`const jobs=${JSON.stringify(jobs)};async function handler(request,response){if(request.user?.id!==${JSON.stringify(me.objectId)}||Date.now()>${Date.now()+1200000})return response.status(403).json({success:false});const job=jobs.find(j=>j.name===request.body?.params?.name);if(!job)return response.status(400).json({success:false});const start=Date.now();try{const rows=await Psql.tx(async tx=>{if(job.tempView){await tx.none('SET LOCAL search_path=pg_temp,public');await tx.none(${JSON.stringify(tempView)});}else await tx.none('SET TRANSACTION READ ONLY');await tx.none(\"SET LOCAL statement_timeout='60s'\");if(job.jitOff)await tx.none('SET LOCAL jit=off');const rows=await tx.query(job.sql,job.args||[]);if(job.tempView)await tx.none('DROP VIEW pg_temp.\"ReviewRewardIncome\"');return rows;});return response.json({success:true,ms:Date.now()-start,rows});}catch(e){return response.status(500).json({success:false,ms:Date.now()-start,error:e.message});}}`;
+let fn;try{fn=await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});for(const j of jobs){const r=await fetch(base.replace('/parse','/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{name:j.name}}),signal:AbortSignal.timeout(75000)}),data=await r.json();writeFileSync('tmp/admin-data-2026-10-15/'+j.name+'.json',JSON.stringify(data),{mode:0o600});console.log(JSON.stringify({name:j.name,success:data.success,ms:data.ms,error:data.error,rows:data.rows?.length}));}}finally{if(fn)await parse('/classes/Function/'+fn.objectId,'DELETE');}

+ 11 - 0
perf/admin-data-2026-10-15/summarize.mjs

@@ -0,0 +1,11 @@
+import {readFileSync,writeFileSync} from 'node:fs';
+const files=process.argv.slice(2);if(!files.length)throw Error('Pass paired result JSON files');
+const rows=files.flatMap(f=>JSON.parse(readFileSync(f,'utf8'))),percentile=(values,p)=>values.toSorted((a,b)=>a-b)[Math.ceil(values.length*p)-1];
+const results=[];
+for(const operation of new Set(rows.map(r=>r.operation))){
+ const entry={operation};for(const variant of ['A','B']){const samples=rows.filter(r=>r.operation===operation&&r.variant===variant);if(samples.length!==10)throw Error(operation+' requires exactly 10 samples per variant');
+ entry[variant]={n:10,p50:percentile(samples.map(r=>r.ms),.5),p75:percentile(samples.map(r=>r.ms),.75),p95:percentile(samples.map(r=>r.ms),.95),requestsPerPage:[...new Set(samples.map(r=>r.requests?.length??1))],bytes:[...new Set(samples.flatMap(r=>r.requests?.map(x=>x.bytes)||[r.bytes]))]};}
+ for(let pair=0;pair<10;pair++){const a=rows.find(r=>r.operation===operation&&r.variant==='A'&&r.pair===pair),b=rows.find(r=>r.operation===operation&&r.variant==='B'&&r.pair===pair);if(a.hash!==b.hash)throw Error('Golden mismatch '+operation);}
+ entry.p75ReductionPercent=Math.round((1-entry.B.p75/entry.A.p75)*1000)/10;results.push(entry);
+}
+console.log(JSON.stringify(results,null,2));

+ 30 - 0
perf/admin-data-2026-10-15/survey.mjs

@@ -0,0 +1,30 @@
+import {chromium} from 'playwright';
+import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
+const env={};for(const line of readFileSync('/Users/mac/.config/xiaoshu/legacy-sync-nightly.env','utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m)env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
+const token=env.XIAOSHU_SYNC_OPERATOR_TOKEN;if(!token)throw Error('Missing operator');
+const origin=process.env.ADMIN_ORIGIN||'http://127.0.0.1:4300',out='tmp/admin-data-2026-10-15';mkdirSync(out,{recursive:true,mode:0o700});
+const userR=await fetch('https://server.xiaoshu.pro/parse/users/me',{headers:{'X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Session-Token':token}}),me=await userR.json();if(!userR.ok)throw Error('Invalid operator');
+const paths=process.argv.slice(2).length?process.argv.slice(2):['dashboard','members','stores','schedule','coaches','payroll','lessons','learning-reports','support-requests','vocabulary','special-training','privacy-requests','operator-accounts'];
+const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}),results=[];
+try{for(const path of paths){
+ const ctx=await browser.newContext({viewport:{width:1440,height:1000}});await ctx.addInitScript(auth=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth)),{objectId:me.objectId,sessionToken:token,username:'性能验收',roles:me.roles,operationsRole:'ops-manager',isSuperAdmin:true});
+ const page=await ctx.newPage(),requests=[],pending=new Set(),errors=[];let start,lastFinished=0;
+ await page.addInitScript(()=>{window.__perfLong=[];new PerformanceObserver(list=>window.__perfLong.push(...list.getEntries().map(e=>({start:e.startTime,duration:e.duration})))).observe({type:'longtask',buffered:true});});
+ page.on('pageerror',e=>errors.push(e.message));
+ page.on('request',r=>{if(!r.url().includes('/cloud-functions/'))return;const p=r.postDataJSON()?.params||{},entry={operation:p.operation||p.action,path:new URL(r.url()).pathname,startMs:Date.now()-start,params:p};requests.push(entry);r.__entry=entry;pending.add(r);});
+ page.on('requestfinished',async r=>{if(!r.__entry)return;const entry=r.__entry;try{const resp=await r.response(),body=await resp.body(),data=JSON.parse(body.toString()),t=r.timing();Object.assign(entry,{elapsedMs:Date.now()-start-entry.startMs,ttfbMs:Math.round(t.responseStart-t.requestStart),downloadMs:Math.round(t.responseEnd-t.responseStart),bytes:body.length,status:resp.status(),success:data.success!==false&&data.retcode!==-1,total:data.data?.total??null});}catch(e){entry.error=String(e.message).slice(0,100);}finally{lastFinished=Date.now();pending.delete(r);}});
+ page.on('requestfailed',r=>{if(r.__entry){r.__entry.failed=true;pending.delete(r);lastFinished=Date.now();}});
+ start=Date.now();await page.goto(origin+'/admin/'+path,{waitUntil:'domcontentloaded'});
+ while(Date.now()-start<35000){await page.waitForTimeout(100);if(requests.length&&pending.size===0&&Date.now()-lastFinished>300)break;}
+ await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
+ const first={settledMs:Date.now()-start,requests:requests.map(r=>({...r})),pending:pending.size,errors:[...errors],longTasks:await page.evaluate(()=>window.__perfLong),resources:await page.evaluate(()=>performance.getEntriesByType('resource').map(r=>({name:new URL(r.name).pathname,duration:Math.round(r.duration),transfer:r.transferSize,decoded:r.decodedBodySize}))),alertCount:await page.getByRole('alert').count()};
+ let back;
+ if(path!=='dashboard' && !process.env.FIRST_ONLY){
+  await page.locator('#admin-sidebar a[href="/admin/dashboard"]').last().click();await page.waitForTimeout(1200);
+  while(pending.size&&Date.now()-lastFinished<22000)await page.waitForTimeout(100);
+  const count=requests.length,begin=Date.now();await page.locator('#admin-sidebar a[href="/admin/'+path+'"]').last().click();await page.waitForTimeout(500);
+  while(pending.size&&Date.now()-begin<25000)await page.waitForTimeout(100);
+  back={settledMs:Date.now()-begin,requests:requests.slice(count)};
+ }
+ results.push({path,first,back});writeFileSync(out+'/survey.json',JSON.stringify({at:new Date().toISOString(),results},null,2),{mode:0o600});console.log(JSON.stringify({path,firstMs:first.settledMs,requests:first.requests.map(r=>({op:r.operation,ms:r.elapsedMs,ttfb:r.ttfbMs,bytes:r.bytes,status:r.status})),returnMs:back?.settledMs,returnRequests:back?.requests.map(r=>r.operation),errors}));await ctx.close();
+}}finally{await browser.close();}

+ 22 - 0
perf/admin-data-2026-10-15/verification.json

@@ -0,0 +1,22 @@
+{
+  "frontendBaseline": "2dd872a",
+  "sharedDatabaseRecovery": "4c34288",
+  "preparedAt": "2026-10-15T06:32:46.090Z",
+  "baselineGatewaySha256": "9af0e4182828d83a314596710b2ba79830facf6d07e883ac3e7bdb0e9bfd8744",
+  "candidateGatewaySha256": "8e3cf896fa5bfe0538bb41852fd9ce4411b9d131faea3431a0d237cd399125b6",
+  "viewport": [
+    1440,
+    1000
+  ],
+  "visualWidths": [
+    1440,
+    1000
+  ],
+  "pairedRunsPerVariant": 10,
+  "businessRecordsWritten": false,
+  "temporaryDiagnosticFunctionsRemaining": 0,
+  "regressionTestsPassed": 46,
+  "cloudDefinitionsValidated": 11,
+  "adminProductionBuildPassed": true,
+  "deployment": "prepared-not-published"
+}

+ 58 - 0
perf/admin-data-2026-10-15/vocabulary-api-results.json

@@ -0,0 +1,58 @@
+[
+  {
+    "operation": "ops/vocabulary/tree",
+    "A": {
+      "n": 10,
+      "p50": 752,
+      "p75": 788,
+      "p95": 1200,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        85234
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 728,
+      "p75": 783,
+      "p95": 817,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        85234
+      ]
+    },
+    "p75ReductionPercent": 0.6
+  },
+  {
+    "operation": "ops/vocabulary/list",
+    "A": {
+      "n": 10,
+      "p50": 1904,
+      "p75": 1928,
+      "p95": 2339,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        13878
+      ]
+    },
+    "B": {
+      "n": 10,
+      "p50": 1865,
+      "p75": 1886,
+      "p95": 1896,
+      "requestsPerPage": [
+        1
+      ],
+      "bytes": [
+        13878
+      ]
+    },
+    "p75ReductionPercent": 2.2
+  }
+]

+ 21 - 0
perf/admin-data-2026-10-15/基线.md

@@ -0,0 +1,21 @@
+# 2026-10-15 后台数据加载调查
+
+基线前端与网关代码对应 2dd872a。使用授权运营账号、Chrome 1440×1000、本机 Angular 前端连接真实业务服务器。首次调查的页面耗时包含 300ms 静默判断,仅用于定位,不是最终可用时间统计。
+
+| 页面 | 首次调查 | 主要观察 |
+|---|---:|---|
+| 工作台 | 超过 20 秒超时 | 工资来源版本、核验与课程分类都在统计的关键路径上 |
+| 会员 | 约 3.4 秒 | 主接口约 2.75 秒,23KB;返回已有菜单约 0.56 秒且无重复主接口 |
+| 门店 | 约 4.8 秒 | 主接口约 4.23 秒,5KB;返回已有菜单约 0.54 秒且无重复主接口 |
+| 排课、老师、工资 | 超过 20 秒超时 | 重复课程扫描、全量历史工资读取及来源核验 |
+| 上课记录 | 约 13.9 秒 | 在诊断并发期间测到,需独立复核 |
+| 学习报表 | 约 5.9 秒 | 在诊断并发期间测到,需独立复核 |
+| 反馈与休假 | 约 0.84 秒 | 主接口约 0.22 秒 |
+| 词库 | 约 16.7 秒 | 两个读取接口;需独立复核并发影响 |
+| 专项训练 | 约 2.27 秒 | 返回约 233KB,已有树形内容 |
+| 注销申请 | 约 1.26 秒 | 身份元数据与申请读取 |
+| 运营账号 | 约 1.35 秒 | 身份元数据与账号读取 |
+
+主要慢点是服务器等待。小响应接口的实际传输耗时只有数毫秒到几十毫秒,不能归因为下载带宽。数据库现有 CommonModel 约 82 万条、预约约 3.2 万条、上课记录约 1.9 万条。部分 SQL 在每个课程节点下重复扫描业务表;预约统计重复计算取消状态;数字列转文本使现有数字索引无法直接匹配。
+
+测试期间另一个经用户授权的工资故障任务部署了 4c34288:只针对工资函数关闭 JIT,并采用等价的收入视图。它保留 156,057 条收入投影及来源版本校验值。最终对照必须在该修复后的相同数据库环境下交替运行;不能把部署前后的时间混合计算提升。

+ 10 - 0
perf/admin-data-2026-10-15/实验日志.md

@@ -0,0 +1,10 @@
+# 实验日志
+
+1. 课程映射的 OR + correlated EXISTS 会为各节点反复扫描预约/绑定。改为一次集合查询,返回的 41 门课程及祖先名称一致。单项诊断约 4.2s → 27ms;后续同条件交替查询 10 轮均一致。排课课程选项存在同类问题,约 4.8s → 383ms,课程与学员绑定一致。
+2. 会员预约统计在多个聚合表达式中重复计算取消状态。改为先去重取消编号、再按公司与数字编号关联。560 组汇总一致;10 轮交替对照全部通过。测试包含重复元数据、缺失元数据、取消/异常状态和跨帐套相同编号。
+3. 会员上课记录的数字编号转文本关联没有充分使用已有索引。保持数字类型进行关联与模型筛选,548 组汇总一致,诊断约 1.4s → 282ms。保留日期回退、正式/体验课计数和帐套范围。
+4. ReviewRewardIncome 先把整张历史表转 JSON 再还原为行,阻碍过滤下推。直接投影同一组字段,保留金额精度、列顺序、纠正凭据及不可计薪规则。单月 1,278 条记录哈希一致,诊断 3.9s → 133ms。该视图已被并行的工资故障恢复任务用于线上恢复;后续配对不把生产环境变化算成本任务收益。
+5. 词库正常读取执行了 3 条 ALTER TABLE IF NOT EXISTS。改为先只读检查权限字段;全部存在时为 0 条 DDL,遗留库缺字段时仍兼容修复,失败可重试。它的主要收益是避免等待/阻塞业务表结构锁,不能把初始并发诊断的 16s 直接算为此项提速。
+6. 仅查询优化后,工作台 p75 仍为 13.47s(前 15.317s),未达目标。发现展示预估工资也在执行结算资格核验。为服务端内部工作台调用增加 display-only 参数:实时来源和计薪公式不变,省去工作台不使用的来源版本和截止核验;结果明确 settlementReady=false / not_checked。工资模块和生成、审核路径默认仍执行完整校验;请求体伪造标志不能跳过。此项须重新进行工作台 10 轮配对,金额、课次、人数及全部可见内容相同,仅工作台内部校验元数据允许有意变化。
+
+最终计时按真实页面主要数据响应成功、真实列表/统计已显示、两帧渲染完成。不是骨架屏时间。后台“工资核验”的只读 metadata 差异不会被当作金额或业务列表一致。

+ 52 - 0
perf/admin-data-2026-10-15/报告.md

@@ -0,0 +1,52 @@
+# 管理后台数据加载优化验收
+
+已检查 13 个主要运营菜单。瓶颈主要是服务器重复扫描/关联、工资核验进入工作台关键路径,以及词库读请求里的表结构操作;小接口只有数 KB 到几十 KB,传输带宽不是主因。
+
+## 真实数据页面对照
+
+同一 Mac、Chrome、本地管理端连接真实线上业务数据库,旧/新临时受限网关 A/B 交替各 10 次。每次使用独立浏览器上下文;终点为主接口成功、真实列表/统计可见并完成渲染。临时网关只允许当前授权运营账号和指定读取操作,带有效期,测试结束删除。页面字节、主要数据请求数未靠删除业务数据缩减。
+
+工资故障恢复提交 4c34288 在测试期间已上线。因此最终对照统一使用恢复后的数据库环境;工作台对照的旧版本也使用已修复的收入视图,不能把该应急恢复重复计入本次提升。初始工作台/排课曾超过前端 20 秒超时,不进入下面成功样本的百分比计算。
+
+| 页面 | 旧 p50/p75/p95 | 新 p50/p75/p95 | p75 缩短 |
+|---|---:|---:|---:|
+| 会员管理 | 2.11 / 2.26 / 2.52 秒 | 1.13 / 1.17 / 1.28 秒 | 48.4% |
+| 门店管理 | 3.30 / 3.49 / 3.55 秒 | 2.31 / 2.43 / 3.43 秒 | 30.4% |
+| 排课中心 | 11.90 / 12.03 / 12.25 秒 | 2.50 / 2.58 / 2.91 秒 | 78.5% |
+| 工作台 | 15.10 / 15.52 / 16.48 秒 | 6.08 / 6.20 / 6.40 秒 | 60% |
+
+
+原始明细、截图、线上基线及授权信息仅保留在本机私有/忽略目录,仓库只保存不含个人数据的汇总。
+
+## 改动
+
+- 课程分类和排课选项改为集合查询,消除按课程节点重复扫描预约/绑定。
+- 会员预约取消状态先去重,再关联汇总;保留重复元数据、缺失元数据和跨帐套情况的原有语义。
+- 上课记录关联使用已有数字索引,保留历史日期回退、正式/体验课口径。
+- 收入视图直接投影字段,使公司/月筛选可以下推;列顺序、数值精度及历史纠正金额保持一致。该 SQL 已被工资故障恢复任务采用上线,这里补齐安装源文件和独立迁移文件。
+- 词库先只读检查已迁移字段,正常读请求的 ALTER TABLE 从 3 次变为 0 次;缺字段的遗留库仍兼容修复。
+- 工作台只执行实时预估金额计算,省去它不使用的来源版本和结算截止核验。实际计薪公式、来源和金额不变。工作台内部校验 metadata 明确标记 `not_checked / settlementReady=false`,不会授权结算。工资模块以及生成、审核工资仍完整校验,用户在请求体伪造 `forDashboard` 不能跳过。真实工资概览的新旧响应也逐项一致,包含来源版本和结算状态(单次读取对照约 13.9s / 12.0s,不作为稳定速度结论)。
+
+## 验证
+
+46 项相关回归测试通过;11 个云函数定义校验通过;管理端生产构建通过。测试覆盖租户隔离、重复/取消/空记录、金额纠正、数字精度、字段序列、历史来源回退、失败重试、只读预估不能绕过结算校验,以及发布补丁保留线上其他修复。
+
+会员、门店、排课各 10 轮所有业务响应一致;工作台各 10 轮的金额、课次、人数及其他业务字段一致,仅有意改变其不参与结算的内部校验 metadata。所有页面只忽略刷新时间/延迟秒数等时钟字段。1440px、1000px 截图核对通过:工作台/门店/排课像素一致;会员仅刷新时间差异,变化低于 0.07%。
+
+原有菜单复用保持:会员、门店切往反馈页再返回的单次功能验收分别约 98ms、82ms,均 0 个重复主数据请求。这是原有缓存能力的验证,不计为本次新提速收益。
+
+词库树/列表按真实默认筛选各做了 10 轮 API 对照(见 vocabulary-api-results.json),短请求耗时没有显著改善;保留该改动的依据是正常读路径表结构操作从 3 次确定性降为 0 次,不把初始 16s 计为它的收益。另在诊断筛选条件下独立探测上课记录约 1.93s、学习报表约 1.35s;初始并发调查中的较大耗时不能当作其稳定基线。反馈、注销申请、运营账号的接口为亚秒级。没有公开匿名后台地址的 CrUX 数据,以上为实验室真实数据对照,不代表所有用户网络条件。
+
+## 剩余瓶颈及下一步
+
+门店没有达到减半目标;会员接近减半。后续可测量将互不依赖的门店统计并行、将仅详情需要的历史汇总延后加载,需继续保持统计一致。
+
+工资模块和部分老师详情仍可能受到全量来源核验影响。建议下一阶段把“金额列表加载”和“结算资格核验”拆为两个状态:先显示真实金额,核验通过后才开放结算操作。进一步减少全量核验需设计可验证的变更版本机制,不能简单用过期缓存或最后更新时间代替完整性校验。本次保留完整财务核验。还可在保证重复课次判断和全量来源版本覆盖的前提下,按月份裁剪传回应用层的工资明细;该方案尚未实施。
+
+## 发布边界
+
+本次剩余网关优化尚未发布,已生成精确到 8 个读取/计算函数的补丁;不重写整个线上网关,不触碰业务记录。私有发布目录:`~/.local/state/xiaoshu/cloud-releases/2026-10-15-admin-read-performance`,包含实际线上原件、候选代码、前后 SHA-256 和清单。
+
+经授权后执行 `scripts/release-admin-read-performance.mjs --deploy`:检查同步空闲及线上版本未漂移,发布并回读哈希。随后复核真实页面、筛选、菜单返回及工资金额;回滚命令为同脚本 `--rollback`,仅在线上仍是本次候选哈希时恢复原件,防止覆盖后续发布。
+
+复测命令:`paired-api.mjs` 在 `PERF_BROWSER=1` 下测页面,`summarize.mjs` 汇总各 10 轮结果,`ratchet.mjs` 对照保存的 p75 上限。WAN 时间门禁须同环境人工运行;离线 CI 使用数据等价和零正常读 DDL 等确定性护栏。

+ 12 - 0
scripts/admin-read-performance-patch.mjs

@@ -0,0 +1,12 @@
+import {readFileSync} from 'node:fs';
+export const optimizedReadHelpers=['memberAppointmentStats','courseTeachingRows','professionalScheduleData','queryUsers','ensureVocabularyPhysicalSchema','professionalDashboardFast','payrollCanonicalSnapshot','payrollPreviewData'];
+// Preserve the actual deployed gateway, including unrelated fixes added after this work started.
+export function patchAdminReadHelpers(baseline){
+ const source=readFileSync(new URL('./deploy-admin-functions.mjs',import.meta.url),'utf8').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1]+'\n'+readFileSync(new URL('./cloud/payroll-integrity.js',import.meta.url),'utf8');
+ for(const name of optimizedReadHelpers){
+  const pattern=new RegExp('async function '+name+'\\([^]*?(?=\\n(?:async )?function |\\nconst [A-Z_])');
+  const replacement=[...source.matchAll(new RegExp(pattern.source,'g'))].at(-1)?.[0],match=[...baseline.matchAll(new RegExp(pattern.source,'g'))].at(-1);if(!replacement||!match)throw Error('Missing read helper '+name);
+  baseline=baseline.slice(0,match.index)+replacement+baseline.slice(match.index+match[0].length);
+ }
+ new Function(baseline);return baseline;
+}

+ 7 - 4
scripts/cloud/payroll-integrity.js

@@ -25,9 +25,12 @@ async function payrollCutoff(context,input,payload,confirm=false){
  return(await payrollSql('SELECT xs_payroll_cutoff_snapshot($1,$2) AS result',[co,month])).result;
 }
 
-async function payrollCanonicalSnapshot(context,input) {
+async function payrollCanonicalSnapshot(context,input,options={}) {
   const co=payrollTenant(context,input);
-  const [snapshot,sync]=await Promise.all([payrollSql('SELECT COALESCE(jsonb_agg(s),\'[]\'::jsonb) AS rows,xs_payroll_source_version($1) AS version FROM xs_payroll_sources($1) s',[co]),payrollSettlementSyncStatus(context,input)]);
+  // A display-only estimate cannot authorize settlement. Financial reads and all
+  // commands keep the full source version and cutoff checks by default.
+  const forDashboard=options.forDashboard===true;
+  const [snapshot,sync]=await Promise.all([payrollSql('SELECT COALESCE(jsonb_agg(s),\'[]\'::jsonb) AS rows,'+(forDashboard?"''::text":'xs_payroll_source_version($1)')+' AS version FROM xs_payroll_sources($1) s',[co]),forDashboard?Promise.resolve({state:'not_checked',settlementReady:false,datasets:[],message:'工作台仅展示实时预估金额;结算前须在工资模块核验来源'}):payrollSettlementSyncStatus(context,input)]);
   const users=await Psql.query('SELECT * FROM "_User" WHERE "company"=$1',[co]);
   const map=new Map(users.map(u=>[number(u.legacyUserId||legacyOf(u).UserID),u]));
   const items=(snapshot.rows||[]).map(row=>{
@@ -45,9 +48,9 @@ function payrollCalculatedLine(item,rates,settlementMonth,rules,mappings){
  const exception=item.commonStatus===-2?'上课记录已作废':!item.coachId?'缺少陪练身份':!date?'缺少有效上课日期':item.duplicate?'来源课次重复':configured&&!categoryKey?'课程未确认标准分类':!snapshot&&configured&&!rule?'课程分类、时长和授课方式缺少有效计价规则':!snapshot&&!configured&&type!==4&&!rule?'课型未配置有效单价':amount<0?'课酬金额无效':'';
  return {...item,kind:'lesson',standardLessonKey:payrollIdentity(item),lessonId:item.objectId,lessonAt:payrollDateIso(item.lessonAt),originalMonth:date.slice(0,7),settlementMonth,classType:type,classTypeLabel:categoryName||classType(type),courseCategoryKey:categoryKey,courseCategoryName:categoryName,deliveryMode:mode,deliveryLabel:deliveryLabel(mode),durationMinutes:minutes,durationHours:configured?minutes/60:type===4?0:number(rule?.durationHours,minutes/60),teachingRuleId:text(item.teachingRuleId||rule?.objectId),rateAmount:amount,amount:exception?0:amount,description:snapshot?'按已保存的完课/预约工资快照':'按上课日期匹配历史工资规则',exception};
 }
-async function payrollPreviewData(context,input,month,includeUsed=false,requireLive=false){
+async function payrollPreviewData(context,input,month,includeUsed=false,requireLive=false,options={}){
  if(!/^\d{4}-(0[1-9]|1[0-2])$/.test(month))fail(400,'工资月份无效');
- const co=payrollTenant(context,input),[source,rates,rules,mappings,used]=await Promise.all([payrollCanonicalSnapshot(context,{...input,month}),payRateRows(context,input),teachingRuleRows(context,input),courseTeachingRows(context,input),Psql.query('SELECT "standardLessonKey","batchId","excluded" FROM "PayrollLine" WHERE "company"=$1 AND "kind"=\'lesson\' AND COALESCE("excluded",false)=false',[co])]);
+ const co=payrollTenant(context,input),[source,rates,rules,mappings,used]=await Promise.all([payrollCanonicalSnapshot(context,{...input,month},{forDashboard:options.forDashboard===true&&includeUsed&&!requireLive}),payRateRows(context,input),teachingRuleRows(context,input),courseTeachingRows(context,input),Psql.query('SELECT "standardLessonKey","batchId","excluded" FROM "PayrollLine" WHERE "company"=$1 AND "kind"=\'lesson\' AND COALESCE("excluded",false)=false',[co])]);
  const other=new Set(used.filter(r=>r.batchId!==input.targetId).map(r=>r.standardLessonKey)),mappingMap=new Map(mappings.filter(r=>r.confirmed).map(r=>[r.courseId,r]));
  const previous=month.endsWith('-01')?(Number(month.slice(0,4))-1)+'-12':month.slice(0,5)+String(Number(month.slice(5))-1).padStart(2,'0');
  const allowLate=!includeUsed&&Boolean((await Psql.one('SELECT EXISTS(SELECT 1 FROM "PayrollBatch" WHERE "company"=$1 AND "month"=$2 AND "status" IN (\'reviewed\',\'paid\')) AS value',[co,previous])).value);

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
scripts/deploy-admin-functions.mjs


+ 33 - 0
scripts/release-admin-read-performance.mjs

@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+// --prepare is read-only. --deploy / --rollback require an explicit release decision.
+import {readFileSync,writeFileSync,mkdirSync,chmodSync} from 'node:fs';
+import {createHash} from 'node:crypto';
+import {homedir} from 'node:os';
+import {join} from 'node:path';
+import {patchAdminReadHelpers,optimizedReadHelpers} from './admin-read-performance-patch.mjs';
+const mode=process.argv.includes('--deploy')?'deploy':process.argv.includes('--rollback')?'rollback':'prepare';
+const dir=process.env.XIAOSHU_READ_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-10-15-admin-read-performance');
+const base=process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID,master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
+if(!app||!master||!token)throw Error('Missing authorized release configuration');
+const hash=s=>createHash('sha256').update(s).digest('hex'),headers={'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master};
+async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)}),d=await r.json();if(!r.ok||d.error)throw Error('Release API HTTP '+r.status);return d;}
+mkdirSync(dir,{recursive:true,mode:0o700});chmodSync(dir,0o700);
+if(mode==='prepare'){
+ const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results;
+ if(rows.length!==1)throw Error('Ambiguous operations gateway');const original=rows[0],code=patchAdminReadHelpers(original.code);
+ writeFileSync(join(dir,'original.json'),JSON.stringify(original),{mode:0o600});writeFileSync(join(dir,'candidate.js'),code,{mode:0o600});
+ const manifest={preparedAt:new Date().toISOString(),objectId:original.objectId,path:original.path,beforeHash:hash(original.code),afterHash:hash(code),helpers:optimizedReadHelpers};
+ writeFileSync(join(dir,'manifest.json'),JSON.stringify(manifest,null,2),{mode:0o600});console.log(JSON.stringify({mode,dir,...manifest}));
+}else{
+ const manifest=JSON.parse(readFileSync(join(dir,'manifest.json'),'utf8')),original=JSON.parse(readFileSync(join(dir,'original.json'),'utf8')),candidate=readFileSync(join(dir,'candidate.js'),'utf8');
+ if(hash(original.code)!==manifest.beforeHash||hash(candidate)!==manifest.afterHash)throw Error('Prepared release has changed');
+ const current=await parse('/classes/Function/'+manifest.objectId),expected=mode==='deploy'?manifest.beforeHash:manifest.afterHash,target=mode==='deploy'?candidate:original.code;
+ if(hash(current.code)===hash(target)){console.log(JSON.stringify({mode,unchanged:true}));process.exit(0);}
+ if(hash(current.code)!==expected)throw Error('Online gateway changed after preparation; reprepare and revalidate');
+ const sync=await fetch(base.replace(/\/parse\/?$/,'/api/functions/xiaoshu/sync/direct-v1'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(30000)}),status=await sync.json();
+ if(!sync.ok||status.success===false||!Array.isArray(status.data?.runs))throw Error('Unable to verify synchronization is idle');
+ if(status.data.runs.some(r=>r.status==='running'))throw Error('Synchronization is running; retry when idle');
+ await parse('/classes/Function/'+manifest.objectId,'PUT',{code:target});
+ const verified=await parse('/classes/Function/'+manifest.objectId);if(hash(verified.code)!==hash(target))throw Error('Release readback mismatch');
+ writeFileSync(join(dir,mode+'-receipt.json'),JSON.stringify({at:new Date().toISOString(),hash:hash(target)}),{mode:0o600});console.log(JSON.stringify({mode,verified:true,hash:hash(target)}));
+}

+ 57 - 3
scripts/tests/admin-read-performance.test.mjs

@@ -13,14 +13,16 @@ const sorted=rows=>rows.toSorted((a,b)=>JSON.stringify(a).localeCompare(JSON.str
 test('set-based course and appointment reads preserve tenant, cancellation, duplicates and missing metadata',async()=>{
  await db.exec(`CREATE TABLE "CommonModel"(company text,"modelId" numeric,"itemId" numeric,status numeric);
  CREATE TABLE "CourseAppointment"(company text,id numeric,szyh text,dszt text,dslx text,yysj text,bxrq text,kcid text);
- CREATE TABLE "CourseBinding"(company text,kcid text);
+ CREATE TABLE "CourseBinding"(company text,kcid text,yhid text DEFAULT 's1',id numeric DEFAULT 1);
  CREATE TABLE "Node"(company text,"nodeId" numeric,"parentId" numeric,"nodeName" text);
  INSERT INTO "Node" VALUES ('a',1,0,'小学'),('a',2,1,'课程甲'),('a',3,1,'课程乙'),('a',398,1,'写作'),('a',8,1,'未使用'),('b',2,0,'其他帐套课程');
- INSERT INTO "CourseBinding" VALUES('a','2'),('a','2'),('a','bad'),('b','8');
+ INSERT INTO "CourseBinding"(company,kcid) VALUES('a','2'),('a','2'),('a','bad'),('b','8');
  INSERT INTO "CourseAppointment" VALUES('a',1,'s1','0','3','2099-01-02',null,'3'),('a',2,'s1','30','1','2026-10-01',null,'3'),('a',3,'s2','11','3',null,'2099-01-03','3'),('a',4,'s2','99','1',null,null,null),('a',5,null,null,null,null,null,null),('b',1,'s1','20','3',null,null,'2');
- INSERT INTO "CommonModel" VALUES('a',54,1,99),('a',54,1,-2),('a',54,1,-2),('a',54,2,99),('a',59,3,-2),('b',54,3,-2),('b',54,1,99);`);
+ INSERT INTO "CommonModel" VALUES('a',54,1,99),('a',54,1,-2),('a',54,1,-2),('a',54,2,99),('a',59,3,-2),('b',54,3,-2),('b',54,1,99),('a',58,1,99),('a',58,1,99),('a',58,1,-2);`);
  const courseSql=vm.runInNewContext(source.match(/courseSql=('[\s\S]*?'),\[categories/)[1]);
+ const scheduleSql=vm.runInNewContext(source.match(/const courseRows=await Psql.query\(('([\s\S]*?)'),\[companyId\]/)[1]);
  for(const co of ['a','b',null]){
+  assert.deepEqual(await query(scheduleSql,[co]),await query(baseline.scheduleOptions,[co]));
   assert.deepEqual(sorted(await ctx.memberAppointmentStats(co)),sorted(await query(baseline.appointments,[co])));
   assert.deepEqual(await query(courseSql,[co]),await query(baseline.courses,[co]));
  }
@@ -31,3 +33,55 @@ test('set-based course and appointment reads preserve tenant, cancellation, dupl
  assert.equal(members.find(r=>r.uid==='s2').abnormal,1);
  assert.deepEqual((await query(courseSql,['a'])).map(r=>r.id).sort(),['2','3','398']);
 });
+test('income view preserves column types, corrections and source serialization without JSON row conversion',async()=>{
+ await db.exec(`CREATE TABLE "ReviewReward"("objectId" text,company text,"taskId" text,"taskKey" text,"coachId" double precision,"coachName" text,"studentId" double precision,"studentName" text,"courseName" text,"storeId" double precision,"storeName" text,"plannedDate" text,"completedAt" timestamptz,"incomeMonth" text,amount numeric(12,2),status text,exception text,"createdAt" timestamptz,"ineligibleReason" text);
+ CREATE TABLE "ReviewRewardCorrection"("objectId" text,company text,"rewardId" text,amount numeric(12,2),reason text,"adjustmentBatchId" text);
+ INSERT INTO "ReviewReward"("objectId",company,"taskId",amount,status,"incomeMonth","ineligibleReason") VALUES('one','a','t1',1,'earned','2026-10',''),('two','a','t2',1,'earned','2026-09',''),('old','a','t3',0,'historical','2026-09',''),('other','b','t4',1,'earned','2026-10','');
+ INSERT INTO "ReviewRewardCorrection" VALUES('c1','a','one',-1,'逾期','batch'),('cross','b','two',-1,'其他帐套','');
+ CREATE VIEW "ReviewRewardIncome" AS SELECT projected.*,r.amount AS "originalAmount",c."objectId" AS "correctionId",c.reason AS "correctionReason",c.amount AS "correctionAmount",c."adjustmentBatchId" FROM "ReviewReward" r LEFT JOIN "ReviewRewardCorrection" c ON c.company=r.company AND c."rewardId"=r."objectId" CROSS JOIN LATERAL jsonb_populate_record(NULL::"ReviewReward",to_jsonb(r)||jsonb_build_object('amount',r.amount+COALESCE(c.amount,0),'status',CASE WHEN c."objectId" IS NULL THEN r.status ELSE 'ineligible' END,'ineligibleReason',CASE WHEN c."objectId" IS NULL THEN r."ineligibleReason" ELSE 'corrected_overdue' END)) projected;`);
+ const rows=()=>query('SELECT to_jsonb(r) AS row FROM "ReviewRewardIncome" r ORDER BY "objectId"');
+ const types=()=>query(`SELECT attname,atttypid,atttypmod FROM pg_attribute WHERE attrelid='"ReviewRewardIncome"'::regclass AND attnum>0 ORDER BY attnum`);
+ const before=await rows(),beforeTypes=await types();
+ await db.exec(readFileSync(new URL('../sql/admin-read-performance.sql',import.meta.url),'utf8'));
+ assert.deepEqual(await rows(),before);assert.deepEqual(await types(),beforeTypes);
+ assert.equal((await rows()).find(r=>r.row.objectId==='one').row.amount,0);
+ assert.equal((await rows()).find(r=>r.row.objectId==='two').row.amount,1);
+});
+test('indexed numeric joins preserve lesson counts and date fallbacks',async()=>{
+ await db.exec(`ALTER TABLE "CommonModel" ADD COLUMN "generalId" numeric, ADD COLUMN "createdAt" timestamptz;
+ ALTER TABLE "CourseAppointment" ADD COLUMN jssj text;
+ CREATE TABLE "LessonRecord"(company text,id numeric,xymz text,"lessonAt" timestamptz,plpjsj text,kclx text,yyds text);
+ INSERT INTO "LessonRecord" VALUES('a',91,'s1','2026-10-01T10:00:00Z','','1','1001'),('a',92,'s1',null,'2026-10-02','3','1002'),('a',93,'s2',null,'','4','1003'),('b',91,'s1',null,'','2','1001');
+ INSERT INTO "CommonModel"(company,"modelId","itemId","generalId","createdAt") VALUES('a',59,91,1091,'2026-10-01'),('a',59,93,1093,'2026-10-03'),('a',54,1,1001,'2026-10-01'),('b',59,91,1091,'2026-09-01');`);
+ const part=source.slice(source.indexOf('async function queryUsers(context,input)')).split('\nfunction pageInput')[0];
+ const sql=vm.runInNewContext(part.match(/Psql.query\(('SELECT CAST\(l\.[\s\S]*?'),\[companyId\]/)[1]);
+ for(const co of ['a','b',null])assert.deepEqual(sorted(await query(sql,[co])),sorted(await query(baseline.memberLessons,[co])));
+ const row=(await query(sql,['a'])).find(r=>r.uid==='s1');assert.equal(row.paid_count,1);assert.equal(row.trial_count,1);
+});
+test('vocabulary reads do not take schema locks after migration; missing legacy columns still recover',async()=>{
+ const current=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
+ const present=['CommonModel','VocabularyWord','Node'].flatMap(table_name=>['_rperm','_wperm'].map(column_name=>({table_name,column_name})));
+ for(const missing of [false,true]){
+  const calls=[],c=vm.createContext({Psql:{query:async sql=>{calls.push(sql);return sql.startsWith('SELECT')?present.filter(r=>!missing||r.table_name!=='VocabularyWord'):[];}}});vm.runInContext(current,c);
+  await Promise.all([c.ensureVocabularyPhysicalSchema(),c.ensureVocabularyPhysicalSchema()]);await c.ensureVocabularyPhysicalSchema();
+  assert.equal(calls.filter(s=>s.startsWith('SELECT')).length,1);
+  assert.equal(calls.filter(s=>s.startsWith('ALTER')).length,missing?1:0);
+  if(missing)assert.match(calls[1],/ALTER TABLE "VocabularyWord"/);
+ }
+ let fail=true,attempts=0;const c=vm.createContext({Psql:{query:async()=>{attempts++;if(fail)throw Error('temporary schema read failure');return present;}}});vm.runInContext(current,c);
+ await assert.rejects(c.ensureVocabularyPhysicalSchema(),/temporary schema/);fail=false;await c.ensureVocabularyPhysicalSchema();assert.equal(attempts,2);
+});
+test('dashboard estimate uses identical fresh lesson data but cannot authorize settlement; request flags cannot skip checks',async()=>{
+ const gateway=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
+ let checks=0;const calls=[],c=vm.createContext({Date,Psql:{one:async sql=>{calls.push(sql);return{rows:[{objectId:'lesson',lessonObjectId:'lesson',standardLessonKey:'legacy:1',legacyGeneralId:1,jsmz:30,xymz:'20',teacherPaySnapshot:40,lessonAt:'2026-10-01T01:00:00Z'}],version:sql.includes('xs_payroll_source_version')?'authoritative':''};},query:async()=>[]}});
+ vm.runInContext(gateway+'\n'+readFileSync(new URL('../cloud/payroll-integrity.js',import.meta.url),'utf8'),c);c.companyIdOf=()=> 'co';c.payrollSettlementSyncStatus=async()=>{checks++;return{settlementReady:true,state:'cutoff_verified'};};
+ const manager={roles:['ops-manager'],current:{id:'admin'}};
+ const normal=await c.payrollCanonicalSnapshot(manager,{forDashboard:true});assert.equal(checks,1);assert.equal(normal.version,'authoritative');
+ const estimate=await c.payrollCanonicalSnapshot(manager,{}, {forDashboard:true});assert.equal(checks,1);assert.equal(estimate.version,'');assert.equal(estimate.sourceStatus.sync.settlementReady,false);assert.equal(estimate.sourceStatus.sync.state,'not_checked');assert.deepEqual(estimate.items,normal.items);
+ const modes=[];c.payrollCanonicalSnapshot=async(_c,_i,options)=>{modes.push(options.forDashboard);return{items:[],sourceStatus:{},version:'v'};};c.payRateRows=c.teachingRuleRows=c.courseTeachingRows=async()=>[];c.payrollResult=()=>({});c.Psql.one=async()=>({value:false});
+ await c.payrollPreviewData(manager,{forDashboard:true},'2026-10',true);
+ await c.payrollPreviewData(manager,{},'2026-10',true,false,{forDashboard:true});
+ await c.payrollPreviewData(manager,{},'2026-10',false,false,{forDashboard:true});
+ await c.payrollPreviewData(manager,{},'2026-10',true,true,{forDashboard:true});
+ assert.deepEqual(modes,[false,true,false,false]);
+});

+ 11 - 0
scripts/tests/admin-read-release.test.mjs

@@ -0,0 +1,11 @@
+import {test} from 'node:test';
+import assert from 'node:assert/strict';
+import {readFileSync} from 'node:fs';
+import {patchAdminReadHelpers,optimizedReadHelpers} from '../admin-read-performance-patch.mjs';
+const source=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1]+'\n'+readFileSync(new URL('../cloud/payroll-integrity.js',import.meta.url),'utf8');
+const strip=code=>{for(const name of optimizedReadHelpers)code=code.replace(new RegExp('async function '+name+'\\([^]*?(?=\\n(?:async )?function |\\nconst [A-Z_])','g'),'READ_HELPER_'+name);return code;};
+test('narrow release preserves newer unrelated gateway handlers and authentication',()=>{
+ const live=source+'\nfunction newerProductionFix(){return "preserve this";}';
+ const patched=patchAdminReadHelpers(live);assert.equal(strip(patched),strip(live));assert.equal(patchAdminReadHelpers(patched),patched);
+ assert.throws(()=>patchAdminReadHelpers(live.replace('async function queryUsers(','async function removedUsers(')),/Missing read helper/);
+});

Некоторые файлы не были показаны из-за большого количества измененных файлов