|
@@ -6,19 +6,70 @@ async function directBaseline(t,co,m,gid,row) {
|
|
|
await t.none('INSERT INTO "LegacyDirectBaseline" (company,model,gid,fingerprint) VALUES($1,$2,$3,md5($4::jsonb::text)) ON CONFLICT(company,model,gid) DO UPDATE SET fingerprint=EXCLUDED.fingerprint,"updatedAt"=now()',[co,m,gid,JSON.stringify([row.common,row.addon])]);
|
|
await t.none('INSERT INTO "LegacyDirectBaseline" (company,model,gid,fingerprint) VALUES($1,$2,$3,md5($4::jsonb::text)) ON CONFLICT(company,model,gid) DO UPDATE SET fingerprint=EXCLUDED.fingerprint,"updatedAt"=now()',[co,m,gid,JSON.stringify([row.common,row.addon])]);
|
|
|
}
|
|
}
|
|
|
async function directUnchanged(t,co,m,gid,c,a,fallback) {
|
|
async function directUnchanged(t,co,m,gid,c,a,fallback) {
|
|
|
- const r=await t.oneOrNone('SELECT fingerprint=md5($4::jsonb::text) matches FROM "LegacyDirectBaseline" WHERE company=$1 AND model=$2 AND gid=$3',[co,m,gid,JSON.stringify([c,a])]);
|
|
|
|
|
|
|
+ const snapshot=JSON.stringify([c,a]);
|
|
|
|
|
+ let priorSnapshot=null;
|
|
|
|
|
+ if(m===60&&a.assignmentVersion===null){
|
|
|
|
|
+ const {assignmentVersion,...legacyAddon}=a;
|
|
|
|
|
+ priorSnapshot=JSON.stringify([c,legacyAddon]);
|
|
|
|
|
+ }
|
|
|
|
|
+ const r=await t.oneOrNone('SELECT (fingerprint=md5($4::jsonb::text) OR COALESCE(fingerprint=md5($5::jsonb::text),false)) matches FROM "LegacyDirectBaseline" WHERE company=$1 AND model=$2 AND gid=$3',[co,m,gid,snapshot,priorSnapshot]);
|
|
|
return r?r.matches:fallback(c)&&fallback(a);
|
|
return r?r.matches:fallback(c)&&fallback(a);
|
|
|
}
|
|
}
|
|
|
-async function directOld(action,args={}) {
|
|
|
|
|
|
|
+async function directOld(action,args={},timeout=60000) {
|
|
|
const config=await Parse.Config.get({useMasterKey:true}),url=config.get('legacyScheduleApiUrl'),apiId=config.get('legacyScheduleApiId'),apiKey=config.get('legacyScheduleApiKey');
|
|
const config=await Parse.Config.get({useMasterKey:true}),url=config.get('legacyScheduleApiUrl'),apiId=config.get('legacyScheduleApiId'),apiKey=config.get('legacyScheduleApiKey');
|
|
|
if(!url||!apiId||!apiKey)throw Error('direct_api_not_configured');
|
|
if(!url||!apiId||!apiKey)throw Error('direct_api_not_configured');
|
|
|
const query=Object.entries({action,...args,apiId,apiKey}).map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(String(v))).join('&');
|
|
const query=Object.entries({action,...args,apiId,apiKey}).map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(String(v))).join('&');
|
|
|
let j;
|
|
let j;
|
|
|
- try {const r=await fetch(url+(url.includes('?')?'&':'?')+query,{signal:AbortSignal.timeout(60000)});if(!r.ok)throw Error();j=await r.json();} catch {throw Error('legacy_read_failed');}
|
|
|
|
|
|
|
+ try {const r=await fetch(url+(url.includes('?')?'&':'?')+query,{signal:AbortSignal.timeout(timeout)});if(!r.ok)throw Error();j=await r.json();} catch {throw Error('legacy_read_failed');}
|
|
|
if(Number(j.retcode)===-1)throw Error('legacy_read_rejected');
|
|
if(Number(j.retcode)===-1)throw Error('legacy_read_rejected');
|
|
|
const items=typeof j.result==='string'?JSON.parse(j.result):j.result;
|
|
const items=typeof j.result==='string'?JSON.parse(j.result):j.result;
|
|
|
if(!Array.isArray(items))throw Error('legacy_invalid_result');return {items,page:j.page||{}};
|
|
if(!Array.isArray(items))throw Error('legacy_invalid_result');return {items,page:j.page||{}};
|
|
|
}
|
|
}
|
|
|
|
|
+async function directReviewFeeProof(co,rows) {
|
|
|
|
|
+ const ids=rows.filter(row=>String(row.fxzt)==='1').map(row=>Number(row.GeneralID??row.generalId));
|
|
|
|
|
+ if(!ids.length)return rows;
|
|
|
|
|
+ const candidates=await Psql.query(`SELECT c."generalId",r."coachId"
|
|
|
|
|
+ FROM "CommonModel" c JOIN "MemoryPracticeRecord" m ON m.company=c.company AND m.id=c."itemId"
|
|
|
|
|
+ JOIN "ReviewReward" r ON r.company=m.company AND r."taskId"=m."objectId"
|
|
|
|
|
+ JOIN "ReviewRewardIncome" v ON v.company=r.company AND v."objectId"=r."objectId"
|
|
|
|
|
+ LEFT JOIN "LegacyReviewSyncState" s ON s.company=m.company AND s."taskId"=m."objectId"
|
|
|
|
|
+ WHERE c.company=$1 AND c."modelId"=60 AND c."generalId"=ANY($2::numeric[]) AND m.fxzt='1'
|
|
|
|
|
+ AND NOT COALESCE(s."pendingIncome",false) AND v.status IN ('historical','exception')
|
|
|
|
|
+ AND v."incomeMonth">='2026-09' AND r."completionRole"='historical_unknown'`,[co,ids]);
|
|
|
|
|
+ const verified=new Set(),fields=['yhid','plid','xxjlid','orderId','kcid','fxzt','wcsj','kywrq','kywsj'];
|
|
|
|
|
+ const deadline=Date.now()+15000;
|
|
|
|
|
+ for(const coach of new Set(candidates.map(row=>Number(row.coachId)))){
|
|
|
|
|
+ const coachIds=new Set(candidates.filter(row=>Number(row.coachId)===coach).map(row=>Number(row.generalId)));
|
|
|
|
|
+ const income=new Map();
|
|
|
|
|
+ let total, count, read=0;
|
|
|
|
|
+ try{
|
|
|
|
|
+ for(let page=1;;page++){
|
|
|
|
|
+ const remaining=deadline-Date.now();
|
|
|
|
|
+ if(remaining<=0)throw Error('legacy_review_income_proof_timeout');
|
|
|
|
|
+ const result=await directOld('e_get_21list',{uid:coach,status:1,shouru:1,cpage:page,psize:1000},Math.min(6000,remaining));
|
|
|
|
|
+ const items=Number(result.page.itemCount),pages=Math.max(1,Number(result.page.pageCount));
|
|
|
|
|
+ total??=items;count??=pages;
|
|
|
|
|
+ if(!Number.isSafeInteger(total)||!Number.isSafeInteger(count)||count>100||items!==total||pages!==count)throw Error('legacy_review_income_page_changed');
|
|
|
|
|
+ read+=result.items.length;
|
|
|
|
|
+ for(const row of result.items){
|
|
|
|
|
+ const id=Number(row.GeneralID??row.generalId);
|
|
|
|
|
+ if(coachIds.has(id))income.set(id,income.has(id)?null:row);
|
|
|
|
|
+ }
|
|
|
|
|
+ if(page>=count)break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if(read!==total)throw Error('legacy_review_income_page_incomplete');
|
|
|
|
|
+ for(const row of rows){
|
|
|
|
|
+ const id=Number(row.GeneralID??row.generalId),fee=income.get(id);
|
|
|
|
|
+ if(fee&&Number(fee.money)>0&&Number(row.plid)===coach&&fields.every(k=>String(fee[k]??'')===String(row[k]??'')))verified.add(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }catch {
|
|
|
|
|
+ // The old fee API can fail for individual accounts. Keep syncing the source page and retry on the next run.
|
|
|
|
|
+ console.warn(JSON.stringify({event:'legacy_review_income_proof_unavailable',coach}));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return rows.map(row=>({...row,reviewFeeVerified:verified.has(Number(row.GeneralID??row.generalId))}));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function directControl(p,co,u) {
|
|
async function directControl(p,co,u) {
|
|
|
const specs=__SPECS__;
|
|
const specs=__SPECS__;
|
|
|
if(p.action==='status')return {version:'2026-11-02-review-source-v1',reviewSyncVersion:'source-owned-v1',source:'old-api/WXAPP',bridgeUsed:false,runs:await Psql.query('SELECT "businessDate","runId",status,"startedAt","finishedAt",summary FROM "LegacyDirectRun" WHERE company=$1 ORDER BY "startedAt" DESC LIMIT 3',[co])};
|
|
if(p.action==='status')return {version:'2026-11-02-review-source-v1',reviewSyncVersion:'source-owned-v1',source:'old-api/WXAPP',bridgeUsed:false,runs:await Psql.query('SELECT "businessDate","runId",status,"startedAt","finishedAt",summary FROM "LegacyDirectRun" WHERE company=$1 ORDER BY "startedAt" DESC LIMIT 3',[co])};
|
|
@@ -55,7 +106,7 @@ async function directControl(p,co,u) {
|
|
|
if(!Number.isSafeInteger(p.page)||p.page<1||p.page>10000)throw Error('invalid_page');
|
|
if(!Number.isSafeInteger(p.page)||p.page<1||p.page>10000)throw Error('invalid_page');
|
|
|
const result=await directOld('content_list',{modelId:m,psize:p.countOnly?1:1000,cpage:p.page,...(m===2?{nid:3}:{})});
|
|
const result=await directOld('content_list',{modelId:m,psize:p.countOnly?1:1000,cpage:p.page,...(m===2?{nid:3}:{})});
|
|
|
if(!p.countOnly){const ids=result.items.map(x=>Number(x.GeneralID??x.generalId));if(ids.some(id=>!Number.isSafeInteger(id)||id<1))throw Error('invalid_source_id');
|
|
if(!p.countOnly){const ids=result.items.map(x=>Number(x.GeneralID??x.generalId));if(ids.some(id=>!Number.isSafeInteger(id)||id<1))throw Error('invalid_source_id');
|
|
|
- if(m===60)await Psql.one('SELECT xs_legacy_review_observe($1,$2::jsonb)',[co,JSON.stringify(result.items)]);
|
|
|
|
|
|
|
+ if(m===60)await Psql.one('SELECT xs_legacy_review_observe($1,$2::jsonb)',[co,JSON.stringify(await directReviewFeeProof(co,result.items))]);
|
|
|
await Psql.none('INSERT INTO "LegacyDirectSourcePage" (company,"runId",model,page,ids,total,checksum) VALUES($1,$2,$3,$4,$5::jsonb,$6,md5($7::jsonb::text)) ON CONFLICT(company,"runId",model,page) DO UPDATE SET ids=EXCLUDED.ids,total=EXCLUDED.total,checksum=EXCLUDED.checksum',[co,p.runId,m,p.page,JSON.stringify(ids),Number(result.page.itemCount??ids.length),JSON.stringify(result.items)]);
|
|
await Psql.none('INSERT INTO "LegacyDirectSourcePage" (company,"runId",model,page,ids,total,checksum) VALUES($1,$2,$3,$4,$5::jsonb,$6,md5($7::jsonb::text)) ON CONFLICT(company,"runId",model,page) DO UPDATE SET ids=EXCLUDED.ids,total=EXCLUDED.total,checksum=EXCLUDED.checksum',[co,p.runId,m,p.page,JSON.stringify(ids),Number(result.page.itemCount??ids.length),JSON.stringify(result.items)]);
|
|
|
}return result;
|
|
}return result;
|
|
|
}
|
|
}
|