legacy-direct-supplement.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445
  1. async function directSupplement(p,co){
  2. async function all(action,args){const first=await directOld(action,{...args,psize:1000,cpage:1}),rows=[...first.items];for(let page=2;page<=Number(first.page.pageCount||1);page++)rows.push(...(await directOld(action,{...args,psize:1000,cpage:page})).items);return rows;}
  3. const users=await all('user_list',{uid:0}),nodes=await all('node_list',{nid:0});
  4. const result={accounts:{source:users.length,created:0,matched:0,missing:0,conflicts:0},nodes:{source:nodes.length,created:0,matched:0,missing:0},existingAccountBalancesUntouched:true};
  5. for(const row of users){
  6. const value=projection(row),id=value.legacyUserId;if(!Number.isSafeInteger(id)||id<1)throw Error('invalid_legacy_user_identity');
  7. await directLease(Psql,p,co);
  8. const found=await Psql.query('SELECT "objectId",username,"legacyUserData" FROM "_User" WHERE company=$1 AND "legacyUserId"=$2',[co,id]);
  9. if(found.length>1)throw Error('duplicate_legacy_user_identity');
  10. if(!found.length){
  11. await Psql.tx(async t=>{
  12. // Serialize retries for the same legacy identity even if the first HTTP response was lost.
  13. await t.any('SELECT pg_advisory_xact_lock(hashtext($1))',['direct-sync-user:'+co+':'+id]);
  14. if(await t.oneOrNone('SELECT "objectId" FROM "_User" WHERE company=$1 AND "legacyUserId"=$2',[co,id]))return;
  15. if(![1,2,3].includes(value.body.legacyGroupId))throw Error('new_legacy_user_role_requires_confirmation');
  16. let username=value.username;
  17. if(await Psql.oneOrNone('SELECT "objectId" FROM "_User" WHERE lower(username)=lower($1)',[username]))username='legacy_'+id+'_'+username;
  18. if(await Psql.oneOrNone('SELECT "objectId" FROM "_User" WHERE lower(username)=lower($1)',[username]))throw Error('new_legacy_user_login_conflict');
  19. const user=new Parse.User();
  20. for(const[k,v]of Object.entries(value.body))user.set(k,k==='company'?Parse.Object.createWithoutData('Company',co):v?.__type==='Date'?new Date(v.iso):v);
  21. user.set('identityType',value.body.legacyGroupId===3?'coach':value.body.legacyGroupId===2?'store':'member');user.setUsername(username);
  22. user.setPassword((await Psql.one('SELECT gen_random_uuid()::text||gen_random_uuid()::text AS password')).password);
  23. await user.save(null,{useMasterKey:true});
  24. const check=await Psql.query('SELECT "objectId" FROM "_User" WHERE company=$1 AND "legacyUserId"=$2',[co,id]);if(check.length!==1)throw Error('new_legacy_user_identity_verification_failed');
  25. result.accounts.created++;
  26. });
  27. }else{
  28. const old=found[0].legacyUserData||{},fields=Object.keys(value.body.legacyUserData).filter(k=>!['LastLoginTime','LoginTimes'].includes(k)&&String(value.body.legacyUserData[k]??'')!==String(old[k]??''));
  29. if(fields.length){result.accounts.conflicts++;
  30. await Psql.none('INSERT INTO "LegacySyncConflict" ("objectId",company,"sourceKey",dataset,"legacyKey","conflictType",message,resolved,resolution,"createdAt","updatedAt") SELECT substr(md5($1),1,20),$2,$1,\'users\',$3,\'direct-api-preserve-new\',$4,false,\'保留新系统账号及余额\',now(),now() WHERE NOT EXISTS(SELECT 1 FROM "LegacySyncConflict" WHERE company=$2 AND "sourceKey"=$1)',['direct-api:user:'+id,co,String(id),JSON.stringify({fields,policy:'preserve-new'})]);
  31. }
  32. }result.accounts.matched++;
  33. }
  34. const mapping={nodeId:'NodeID',parentId:'ParentID',nodeName:'NodeName',depth:'Depth',orderId:'OrderID',contentModel:'ContentModel',zstatus:'ZStatus',nodeType:'NodeType',nodeDir:'NodeDir',nodePicUrl:'NodePic',description:'Description',cuser:'CUser',cuname:'CUName'};
  35. const columns=await Psql.query('SELECT column_name,data_type FROM information_schema.columns WHERE table_schema=\'public\' AND table_name=\'Node\'');
  36. const knownNodes=new Set((await Psql.query('SELECT "nodeId" FROM "Node" WHERE company=$1',[co])).map(r=>Number(r.nodeId)));
  37. for(const row of nodes){const id=Number(row.NodeID);if(!Number.isSafeInteger(id)||id<1)throw Error('invalid_node_identity');if(knownNodes.has(id)){result.nodes.matched++;continue;}
  38. await Psql.tx(async t=>{await directLease(t,p,co);await t.any('SELECT pg_advisory_xact_lock(hashtext($1))',['direct-sync-node:'+co]);const found=await t.any('SELECT "objectId" FROM "Node" WHERE company=$1 AND "nodeId"=$2',[co,id]);if(found.length>1)throw Error('duplicate_node_identity');if(found.length)return;
  39. const body={objectId:(await t.one('SELECT substr(md5(gen_random_uuid()::text),1,20) id')).id,company:co,sourceKey:'legacy:node:'+id,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};
  40. for(const[field,key]of Object.entries(mapping)){const c=columns.find(c=>c.column_name===field);if(c&&row[key]!==undefined)body[field]=['numeric','double precision','integer'].includes(c.data_type)?Number(row[key]||0):c.data_type==='boolean'?row[key]===true||String(row[key])==='1':String(row[key]??'');}
  41. const fields=Object.keys(body).filter(k=>columns.some(c=>c.column_name===k));await t.none('INSERT INTO "Node" ('+fields.map(f=>'"'+f+'"').join(',')+') SELECT '+fields.map(f=>'v."'+f+'"').join(',')+' FROM jsonb_populate_record(NULL::"Node",$1::jsonb) v',[JSON.stringify(body)]);result.nodes.created++;
  42. });result.nodes.matched++;
  43. }
  44. return result;
  45. }