|
|
@@ -28,26 +28,37 @@ async function importLegacyFinance(co,uid,runId,readSource){
|
|
|
if(all.length!==total)throw Error('finance_source_count_mismatch');
|
|
|
const result=await Psql.tx(async t=>{
|
|
|
await t.any("SELECT pg_advisory_xact_lock(hashtext('xs-credit:'||$1))",[co]);
|
|
|
- let created=0;const ids=[];
|
|
|
- for(const raw of all){
|
|
|
+ let created=0;const ids=[],patches=[],inserts=[];
|
|
|
+ const sourceIds=all.map(raw=>String(raw.ExpHisID));
|
|
|
+ const hashes=await t.any('SELECT ord::int idx,md5(value::text) hash FROM jsonb_array_elements($1::jsonb) WITH ORDINALITY AS e(value,ord)',[JSON.stringify(all)]);
|
|
|
+ if(hashes.length!==all.length)throw Error('finance_hash_count_mismatch');
|
|
|
+ const existing=sourceIds.length?await t.any('SELECT to_jsonb(l) j FROM "'+table+'" l WHERE company=$1 AND ("legacyFinanceId"=ANY($2::text[]) OR "sourceKey"=ANY($3::text[]) OR to_jsonb(l)->>\'expHisId\'=ANY($2::text[]))',[co,sourceIds,sourceIds.map(id=>'[["ExpHisID",'+id+']]')]):[];
|
|
|
+ const priorById=new Map(),sourceIdSet=new Set(sourceIds);
|
|
|
+ for(const {j} of existing){
|
|
|
+ const keys=[j.legacyFinanceId,j.expHisId,/^\[\["ExpHisID",(\d+)\]\]$/.exec(String(j.sourceKey||''))?.[1]].filter(Boolean).map(String);
|
|
|
+ for(const key of new Set(keys))if(sourceIdSet.has(key)){const bucket=priorById.get(key)||[];bucket.push(j);priorById.set(key,bucket);}
|
|
|
+ }
|
|
|
+ for(let index=0;index<all.length;index++){
|
|
|
+ const raw=all[index];
|
|
|
const id=String(raw.ExpHisID),delta=Number(raw.score??raw.Score),before=raw.score_before===null||raw.score_before===undefined||raw.score_before===''?null:Number(raw.score_before);
|
|
|
const date=String(raw.HisTime||'').replace(' ','T'),iso=/([+-]\d{2}:?\d{2}|Z)$/.test(date)?date:date+'+08:00';
|
|
|
if((raw.score??raw.Score)===undefined||(raw.score??raw.Score)===null||!Number.isFinite(delta)||(before!==null&&!Number.isFinite(before))||Number.isNaN(Date.parse(iso)))throw Error('finance_invalid_values');
|
|
|
- const hash=(await t.one('SELECT md5($1::jsonb::text) hash',[JSON.stringify(raw)])).hash;
|
|
|
- const prior=await t.any('SELECT to_jsonb(l) j FROM "'+table+'" l WHERE company=$1 AND ("legacyFinanceId"=$2 OR "sourceKey"=$3 OR to_jsonb(l)->>\'expHisId\'=$2)',[co,id,'[["ExpHisID",'+id+']]']);
|
|
|
+ const hash=hashes[index].hash,prior=(priorById.get(id)||[]);
|
|
|
if(prior.length>1)throw Error('finance_duplicate_source');
|
|
|
- if(prior.length){const j=prior[0].j;if(j.legacyFinanceHash&&j.legacyFinanceHash!==hash)throw Error('finance_source_conflict_preserved');if(Number(j.userId)!==uid||Number(j.score)!==delta||((j.scoreBefore===null?null:Number(j.scoreBefore))!==before))throw Error('finance_existing_row_conflict');
|
|
|
+ if(prior.length){const j=prior[0];if(j.legacyFinanceId&&String(j.legacyFinanceId)!==id)throw Error('finance_existing_row_conflict');if(j.legacyFinanceHash&&j.legacyFinanceHash!==hash)throw Error('finance_source_conflict_preserved');if(Number(j.userId)!==uid||Number(j.score)!==delta||((j.scoreBefore===null?null:Number(j.scoreBefore))!==before))throw Error('finance_existing_row_conflict');
|
|
|
if(Date.parse(j.hisTime)!==Date.parse(iso)){
|
|
|
if(!untouchedLegacyFinanceTimezoneRow(j,raw,id,iso))throw Error('finance_existing_row_conflict');
|
|
|
const corrected=new Date(iso).toISOString();
|
|
|
await t.none('INSERT INTO "LegacySyncConflict"("objectId",company,"sourceKey",dataset,"legacyKey","conflictType",message,resolved,resolution,"createdAt","updatedAt") VALUES(xs_object_id(),$1,$2,\'finance\',$3,\'finance-legacy-timezone-repair\',$4,true,\'原始流水核验一致,仅修复早期导入时区偏移\',now(),now())',[co,'finance-timezone:'+account+':'+id,id,JSON.stringify({runId,account,studentId:uid,before:j,after:{...j,hisTime:corrected},sourceTime:raw.HisTime})]);
|
|
|
await t.none('UPDATE "'+table+'" SET "hisTime"=$2::timestamptz WHERE "objectId"=$1',[j.objectId,corrected]);
|
|
|
}
|
|
|
- await t.none('UPDATE "'+table+'" SET "legacyFinanceId"=$2,"legacyFinanceHash"=$3,"legacyFinanceRaw"=$4::jsonb WHERE "objectId"=$1',[j.objectId,id,hash,JSON.stringify(raw)]);
|
|
|
+ if(j.legacyFinanceId!==id||j.legacyFinanceHash!==hash||!j.legacyFinanceRaw)patches.push({oid:j.objectId,id,hash,raw});
|
|
|
}else{
|
|
|
- await t.none('INSERT INTO "'+table+'" SELECT * FROM jsonb_populate_record(NULL::"'+table+'",$1::jsonb)',[JSON.stringify({objectId:(await t.one('SELECT xs_object_id() id')).id,company:co,sourceKey:'legacy:finance:'+account+':'+id,userId:uid,score:delta,scoreBefore:before,hisTime:new Date(iso).toISOString(),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),detail:String(raw.Detail||''),remark:String(raw.Remark||''),operator:Number(raw.Operator)||0,scoreType:Number(raw.ScoreType)||0,legacyFinanceId:id,legacyFinanceRaw:raw,legacyFinanceHash:hash})]);created++;
|
|
|
+ inserts.push({company:co,sourceKey:'legacy:finance:'+account+':'+id,userId:uid,score:delta,scoreBefore:before,hisTime:new Date(iso).toISOString(),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),detail:String(raw.Detail||''),remark:String(raw.Remark||''),operator:Number(raw.Operator)||0,scoreType:Number(raw.ScoreType)||0,legacyFinanceId:id,legacyFinanceRaw:raw,legacyFinanceHash:hash});created++;
|
|
|
}ids.push(id);
|
|
|
}
|
|
|
+ for(let i=0;i<patches.length;i+=200)await t.none('UPDATE "'+table+'" l SET "legacyFinanceId"=x.id,"legacyFinanceHash"=x.hash,"legacyFinanceRaw"=x.raw FROM jsonb_to_recordset($1::jsonb) AS x(oid text,id text,hash text,raw jsonb) WHERE l."objectId"=x.oid',[JSON.stringify(patches.slice(i,i+200))]);
|
|
|
+ for(let i=0;i<inserts.length;i+=200){const rows=inserts.slice(i,i+200),objectIds=await t.any('SELECT xs_object_id() id FROM generate_series(1,$1::int)',[rows.length]);if(objectIds.length!==rows.length)throw Error('finance_object_id_allocation_failed');for(let j=0;j<rows.length;j++)rows[j].objectId=objectIds[j].id;await t.none('INSERT INTO "'+table+'" SELECT * FROM jsonb_populate_recordset(NULL::"'+table+'",$1::jsonb)',[JSON.stringify(rows)]);}
|
|
|
ids.sort((a,b)=>Number(a)-Number(b));
|
|
|
const actual=await t.any('SELECT "legacyFinanceId" id,"legacyFinanceHash" hash FROM "'+table+'" WHERE company=$1 AND "userId"=$2 AND "legacyFinanceId"=ANY($3::text[]) ORDER BY "legacyFinanceId"::numeric',[co,uid,ids]);
|
|
|
if(JSON.stringify(actual.map(r=>r.id))!==JSON.stringify(ids))throw Error('finance_id_reconciliation_failed');
|