legacy-appointment.js 6.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. async function readLegacyWritingOrder(id) {
  2. const config=await appLegacyConfig();
  3. if(!config.url||!config.apiId||!config.apiKey)throw Error('暂时无法核对旧系统写作预约,请刷新后重试');
  4. const params=new URLSearchParams({action:'content_list',modelId:'54',ids:String(id),cpage:'1',psize:'2',apiId:config.apiId,apiKey:config.apiKey});
  5. try {
  6. const url=config.url+(config.url.includes('?')?'&':'?')+params.toString();
  7. const response=await fetch(url,{headers:{accept:'application/json'},signal:AbortSignal.timeout(8000)});
  8. const payload=await response.json();
  9. const rows=parseArray(payload.result);
  10. if(response.ok&&[0,1].includes(Number(payload.retcode))&&rows.length===1&&
  11. Number(payload.page?.itemCount)===1&&Number(payload.page?.pageCount)===1)return rows[0];
  12. } catch (_) { /* The legacy URL contains credentials. Do not expose fetch errors. */ }
  13. throw Error('暂时无法核对旧系统写作预约,请刷新后重试');
  14. }
  15. function assertLegacyWritingPending(order,live) {
  16. const same=(key)=>String(live?.[key]??'').trim()===String(order[key]??'').trim();
  17. const time=(value)=>String(value??'').trim().replace('T',' ').slice(0,16);
  18. const model=live?.ModelID??live?.modelId;
  19. const writingBooking=model==null?Number(live?.NodeID??live?.nodeId)===29:Number(model)===54;
  20. if(String(live?.GeneralID??live?.generalId)!==String(order.GeneralID)||!writingBooking||
  21. !same('szyh')||!same('pl')||!same('kcid')||!same('dslx')||!same('jffs')||!same('bxrq')||
  22. time(live.yysj)!==time(order.yysj)||Number(live.Status??live.status??99)!==99){
  23. throw Error('旧系统写作预约与新系统不一致,请联系管理员核对');
  24. }
  25. if(String(live.dszt)!=='0'||String(live.kssj??'').trim()||String(live.jssj??'').trim()){
  26. throw Error('旧系统已开始上课或已完课,请等待记录同步,避免重复扣课时');
  27. }
  28. }
  29. async function confirmLegacyWritingStage(db,order,current) {
  30. // Keep the exact booking, student profile and the existing preparation in one transaction.
  31. const co=DEFAULT_COMPANY_ID,aid=order.objectId;
  32. const row=await db.oneOrNone(`SELECT c.title AS "studentName",a.szyh AS "studentId",
  33. a."yysj" AS "startsAt",a."updatedAt"::text AS "updatedAt",a."courseCategoryKey" AS category,
  34. a."categoryEvidence" AS evidence,a."bindingId",a."teacherPaySnapshot",a."creditCostsSnapshot",
  35. to_jsonb(u)->>'grade' AS grade,u."legacyUserData"->>'Position' AS position,
  36. u."legacyUserData"->'UserBase'->>'Position' AS "basePosition",
  37. u."legacyUserData"->>'HoneyName' AS "oldName",u."legacyUserData"->>'UserName' AS "oldUsername",
  38. u."realName",u.nickname,u.username
  39. FROM "CommonModel" c JOIN "CourseAppointment" a ON a.company=c.company AND a.id=c."itemId"
  40. JOIN "_User" u ON u.company=a.company AND u."legacyUserId"::text=a.szyh::text
  41. WHERE c.company=$1 AND c."objectId"=$2 AND c."modelId"=54 AND c."generalId"::text=$3 AND c.status=99
  42. AND c."sourceKey"='legacy:model:54:general:'||$3 AND a."sourceKey"=c."sourceKey"
  43. AND a.szyh::text=$4 AND a.pl::text=$5 AND a.yysj=$6 AND a.dslx::text=$7 AND a.jffs=$8 AND a.bxrq=$9
  44. AND a.kcid::text='398' AND a.yykcid::text='398' AND a.dszt='0'
  45. AND NULLIF(trim(a.kssj),'') IS NULL AND NULLIF(trim(a.jssj),'') IS NULL
  46. AND NOT COALESCE(a."teachingManaged",false) AND NOT COALESCE(u."isDisabled",false)
  47. AND NOT COALESCE((to_jsonb(u)->>'isDeleted')::boolean,false)
  48. AND NOT EXISTS(SELECT 1 FROM "LessonRecord" l WHERE l.company=a.company AND l.yyds::text=c."generalId"::text)
  49. AND NOT EXISTS(SELECT 1 FROM "LessonCompletionReport" r WHERE r.company=a.company AND r."appointmentGeneralId"::text=c."generalId"::text)
  50. AND NOT EXISTS(SELECT 1 FROM "CourseCreditReservation" r WHERE r.company=a.company AND r."appointmentId"=c."objectId" AND r.state='consumed')
  51. FOR UPDATE OF c,a,u`,[co,aid,String(order.GeneralID),String(order.szyh),String(order.pl),order.yysj,String(order.dslx),order.jffs,order.bxrq]);
  52. if(!row)throw Error('旧写作预约已变更,请刷新后核对');
  53. if(['primary_writing','middle_writing','high_writing'].includes(row.category)&&String(row.evidence??'').trim().length>=2)return;
  54. if(row.category||row.evidence||row.bindingId||row.teacherPaySnapshot!=null||row.creditCostsSnapshot?.length){
  55. throw Error('旧写作预约已有课程或课时凭据,请由管理员核对学段');
  56. }
  57. const prior=await db.query(`SELECT c."generalId" AS "appointmentGeneralId",a.szyh AS "studentId",
  58. a.yysj AS "startsAt",a."courseCategoryKey" AS category,a."categoryEvidence" AS evidence
  59. FROM "CommonModel" c JOIN "CourseAppointment" a ON a.company=c.company AND a.id=c."itemId"
  60. WHERE c.company=$1 AND c."modelId"=54 AND c.status=99 AND a.szyh::text=$2
  61. AND a."courseCategoryKey" IN ('primary_writing','middle_writing','high_writing')
  62. AND length(trim(COALESCE(a."categoryEvidence",'')))>=2 AND a."categoryEvidence" NOT LIKE '开课自动核对:%'`,[co,String(row.studentId)]);
  63. const today=appOverviewToday();
  64. const trusted=legacyWritingStageAssessment({studentId:row.studentId,startsAt:row.startsAt},prior,today);
  65. if(!trusted.canAutoConfirm)throw Error('缺少一致的本学年已确认写作课记录,请由管理员核对学段');
  66. const stage=legacyWritingStageAssessment(row,prior,today);
  67. if(!stage.canAutoConfirm)throw Error(stage.assessmentReason);
  68. await db.one('SELECT xs_payroll_writing_mapping($1,$2,$3,$4,$5,$6::timestamptz,$7) AS result',
  69. [co,current.id,aid,stage.suggestedCategoryKey,'开课自动核对:'+stage.evidence,row.updatedAt,'旧写作预约开课前按档案和本学年记录核对学段']);
  70. }
  71. async function prepareLegacyAppointment(order,current,startWriting=false) {
  72. // SQL rechecks ownership. Request parameters cannot supply the pricing rules.
  73. const prepare=async db=>(await db.one('SELECT xs_prepare_legacy_appointment($1,$2,$3,$4::jsonb) AS result',
  74. [DEFAULT_COMPANY_ID,order.objectId,current.id,JSON.stringify(legacyAppointmentSeedRules())])).result;
  75. try {
  76. if(String(order.kcid)!=='398')return await prepare(Psql);
  77. const uid=ownLegacyId(current);
  78. if(uid!==number(order.pl)||number(current.get('legacyGroupId'))!==3)throw Error('仅预约陪练可以核对旧写作课');
  79. if(!startWriting)assertLegacyWritingPending(order,await readLegacyWritingOrder(order.GeneralID));
  80. return await Psql.tx(async db=>{
  81. await db.one("SELECT pg_advisory_xact_lock(hashtext('xs-credit:'||$1))",[DEFAULT_COMPANY_ID]);
  82. if(startWriting)assertLegacyWritingPending(order,await readLegacyWritingOrder(order.GeneralID));
  83. await confirmLegacyWritingStage(db,order,current);
  84. const prepared=await prepare(db);
  85. if(startWriting)await db.one('SELECT xs_start_appointment_for_actor($1,$2,$3,$4) AS result',
  86. [DEFAULT_COMPANY_ID,order.objectId,current.id,uid]);
  87. return prepared;
  88. });
  89. } catch(error) {
  90. return {blockedReason:String(error.message||error).replace(/^.*ERROR:\s*/,'')};
  91. }
  92. }