| 123456789101112131415161718192021222324252627282930 |
- // Run only inside one database transaction after the original conversion is confirmed.
- // Existing business functions create every correction receipt and enforce reservations.
- export async function recoverCreditConversion(tx, scope) {
- const {companyId:co,actorId:actor,targetId:target,studentId:uid,sourceTransactionId:source,debitOperationIds:debits,key,reason}=scope;
- await tx.one("SELECT pg_advisory_xact_lock(hashtext('xs-credit:'||$1))",[co]);
- const keys=[key+':replace',key+':reverse'];
- const replacementPayload={account:'Purse',amount:4,direction:'adjust',adjustSign:'add',adjustMode:'single',evidence:'用户确认2次60分钟换4次30分钟,陪练总时长不变;关联原单 '+source+';原60分钟扣减 '+[...debits].sort().join(','),payMethod:'运营记账'};
- const reversalPayload={account:'Purse',amount:2,direction:'deduct',sourceTransactionId:source,payMethod:'运营记账'};
- const requests=[replacementPayload,reversalPayload].map(payload=>JSON.stringify({actor,target,payload,reason}));
- const prior=await tx.query('SELECT "idempotencyKey",result,("studentId"=$3 AND request=CASE "idempotencyKey" WHEN $4 THEN $5::jsonb ELSE $6::jsonb END) matched FROM "CreditOperation" WHERE company=$1 AND "idempotencyKey"=ANY($2::text[])',[co,keys,uid,keys[0],...requests]);
- if(prior.length){if(prior.length!==2||prior.some(x=>!x.matched))throw Error('修复回执不完整或请求冲突,请人工核查');return {alreadyApplied:true,receipts:keys.map(k=>prior.find(x=>x.idempotencyKey===k).result)};}
- const balances=async()=> (await tx.one('SELECT "legacyUserData" data FROM "_User" WHERE company=$1 AND "objectId"=$2 AND "legacyUserId"=$3 AND "identityType"=\'member\'',[co,target,uid])).data;
- const before=await balances();
- if(Number(before.Purse)!==3||Number(before.SilverCoin)!==2||Number(before.UserExp)!==0||Number(before.UserPoint)!==5.5)throw Error('学员余额已变化,请重新核对');
- const origin=await tx.one('SELECT * FROM "CreditTransaction" WHERE company=$1 AND "objectId"=$2 AND "studentId"=$3 AND account=\'Purse\' AND kind=\'transfer\' AND amount=2',[co,source,uid]);
- if(Number(origin.storeId)!==73||Number(before.ParentUserID)!==73)throw Error('原单门店不符');
- const companion=await tx.one('SELECT * FROM "CreditTransaction" WHERE company=$1 AND "operationId"=$2 AND account=\'UserPoint\' AND kind=\'transfer\' AND amount=1',[co,origin.operationId]);
- if(Number(companion.studentId)!==uid||Number(companion.storeId)!==73)throw Error('原单配套时长不符');
- const deductions=await tx.query('SELECT request FROM "CreditOperation" WHERE company=$1 AND "objectId"=ANY($2::text[]) AND "studentId"=$3',[co,debits,uid]);
- if(deductions.length!==2||deductions.some(x=>x.request.target!==target||x.request.payload.account!=='SilverCoin'||x.request.payload.direction!=='adjust'||x.request.payload.adjustSign!=='deduct'||x.request.payload.adjustMode!=='single'||Number(x.request.payload.amount)!==1))throw Error('原60分钟扣减凭据不符');
- const store=await tx.one('SELECT "objectId","legacyUserData" data FROM "_User" WHERE company=$1 AND "legacyUserId"=73 AND "identityType"=\'store\'',[co]);
- const held=async()=> (await tx.one("SELECT jsonb_object_agg(a,xs_reserved($1,$2::numeric,a)) data FROM unnest(ARRAY['Purse','SilverCoin','UserExp','UserPoint']) a",[co,uid])).data;
- const reserved=await held();
- const post=async(payload,idem)=>{const p=JSON.stringify(payload),plan=(await tx.one('SELECT xs_credit_plan($1,$2,$3::jsonb) result',[co,target,p])).result;return(await tx.one('SELECT xs_credit_operation($1,$2,$3,$4::jsonb,$5,$6,$7) result',[co,actor,target,p,reason,idem,plan.version])).result;};
- const replacement=await post(replacementPayload,keys[0]);
- const reversal=await post(reversalPayload,keys[1]);
- const after=await balances(),storeAfter=(await tx.one('SELECT "legacyUserData" data FROM "_User" WHERE company=$1 AND "objectId"=$2',[co,store.objectId])).data;
- if(Number(after.Purse)!==5||Number(after.SilverCoin)!==2||Number(after.UserExp)!==0||Number(after.UserPoint)!==4.5||JSON.stringify(await held())!==JSON.stringify(reserved)||Number(storeAfter.Purse)!==Number(store.data.Purse)+2||Number(storeAfter.UserPoint)!==Number(store.data.UserPoint)+1||Number(storeAfter.SilverCoin)!==Number(store.data.SilverCoin)||Number(storeAfter.UserExp)!==Number(store.data.UserExp))throw Error('更正后核账不符,整笔回滚');
- return {alreadyApplied:false,before,after,reserved,receipts:[replacement,reversal],storeBefore:store.data,storeAfter};
- }
|