lesson-workflow.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. // Appointment reads and commands share the same new-database identity.
  2. function assertLessonTenant(current) {
  3. const company=current.get('company');
  4. if ((company && (company.id || company.objectId)) !== DEFAULT_COMPANY_ID) fail(403, '无权访问该帐套');
  5. }
  6. function lessonFeedbackInput(input) {
  7. const raw = parseObject(input.feedback, '完课反馈'), feedback = {};
  8. for (const key of ['contentSummary','reviewSummary','trainingSummary','homework','notes','coachComment']) {
  9. const value = String(raw[key] || '').trim();
  10. if (value.length > 1000) fail(400, '课堂反馈每项最多1000字');
  11. feedback[key] = value;
  12. }
  13. for (const key of ['reviewStatus','trainingStatus']) {
  14. feedback[key] = String(raw[key] || '').trim();
  15. if (feedback[key].length > 200) fail(400, '复习情况和训练状态每项最多200字');
  16. }
  17. feedback.noHomework = raw.noHomework === true;
  18. if (!feedback.contentSummary || !feedback.trainingSummary
  19. || !feedback.noHomework && !feedback.homework) fail(400, '请完整填写学习内容、进度说明和作业');
  20. return feedback;
  21. }
  22. async function appLessonComplete(input, current) {
  23. assertLessonTenant(current);
  24. const id = String(input.orderId || '').trim(), key = String(input.requestKey || '');
  25. if (!id || key.length < 10 || key.length > 160 || !input.expectedUpdatedAt || Number.isNaN(Date.parse(input.expectedUpdatedAt))) fail(400, '缺少预约编号、请求编号或详情版本,请刷新后重试');
  26. const feedback = lessonFeedbackInput(input);
  27. try {
  28. return (await Psql.one('SELECT xs_lesson_complete($1,$2,$3,$4,$5::timestamptz,$6,$7::jsonb) AS result',
  29. [DEFAULT_COMPANY_ID,id,current.id,ownLegacyId(current),input.expectedUpdatedAt,key,JSON.stringify(feedback)])).result;
  30. } catch (error) {
  31. const message = String(error.message || error);
  32. if (message.includes('历史凭据')) await Psql.none('INSERT INTO "LessonWorkflowIssue" (company,"appointmentGeneralId",message) VALUES($1,$2,$3) ON CONFLICT(company,"appointmentGeneralId") DO UPDATE SET message=EXCLUDED.message,"updatedAt"=now()', [DEFAULT_COMPANY_ID,id,message]);
  33. fail(409, message);
  34. }
  35. }
  36. async function appLessonReview(input, current) {
  37. assertLessonTenant(current);
  38. const score = Number(input.score), comment = String(input.comment || '').trim(), id = String(input.orderId || '');
  39. if (!id || !Number.isInteger(score) || score < 1 || score > 5 || comment.length > 200) fail(400, '评分应为1至5分,评价最多200字');
  40. try { return (await Psql.one('SELECT xs_lesson_review($1,$2,$3,$4,$5,$6) AS result', [DEFAULT_COMPANY_ID,id,current.id,ownLegacyId(current),score,comment])).result; }
  41. catch (error) { fail(409, String(error.message || error)); }
  42. }
  43. async function lessonOrderCapabilities(order, current) {
  44. assertLessonTenant(current);
  45. const id = String(order.GeneralID), uid = ownLegacyId(current), status = number(order.dszt);
  46. const stateSql='SELECT c."objectId",a."sourceKey",a.kcid,a."updatedAt"::text AS "updatedAt",xs_appointment_valid_at(a.bxrq,a.yysj) AS "validToday",a."creditHoldStatus",a."creditHoldReason",a."bindingId",a."creditCostsSnapshot",a."kssj",a."jssj" FROM "CourseAppointment" a JOIN "CommonModel" c ON c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id WHERE c.company=$1 AND c."generalId"::text=$2';
  47. let state = await Psql.oneOrNone(stateSql, [DEFAULT_COMPANY_ID,id]);
  48. if (!state) fail(404, '预约不存在');
  49. let preparationReason='';
  50. if(!order.cancelled&&status===0&&state.validToday&&/^legacy[:\-]/.test(state.sourceKey||'')&&(uid===number(order.pl)||uid===number(order.szyh))&&(!state.bindingId||!state.creditCostsSnapshot?.length||state.creditHoldStatus!=='ready')){
  51. const prepared=await prepareLegacyAppointment({...order,objectId:state.objectId},current);
  52. preparationReason=prepared.blockedReason||'';
  53. if(!preparationReason)state=await Psql.oneOrNone(stateSql,[DEFAULT_COMPANY_ID,id]);
  54. }
  55. const lesson = await Psql.oneOrNone('SELECT pf,pjnr,pldp FROM "LessonRecord" WHERE company=$1 AND yyds::text=$2 ORDER BY "createdAt" LIMIT 1',[DEFAULT_COMPANY_ID,id]);
  56. const review = await Psql.oneOrNone('SELECT score,comment,"submittedAt" FROM "LessonReview" WHERE company=$1 AND "appointmentGeneralId"=$2',[DEFAULT_COMPANY_ID,id]);
  57. const teacher = uid === number(order.pl), student = uid === number(order.szyh), actions = [];
  58. let blockedReason = '';
  59. if (order.cancelled) blockedReason = '预约已取消';
  60. else if (teacher) {
  61. if (status === 0 && !state.validToday) blockedReason = '课程仅在预约当天可开始,其他日期请联系管理员改期';
  62. else if (status === 0 && (preparationReason||!state.bindingId || !Array.isArray(state.creditCostsSnapshot) || !state.creditCostsSnapshot.length || state.creditHoldStatus && state.creditHoldStatus !== 'ready')) blockedReason = preparationReason||state.creditHoldReason||'开课记录或课时凭据尚未核实,请联系管理员';
  63. else if (status === 0) actions.push('start');
  64. if (status === 10) actions.push('continue','complete');
  65. if ([11,20].includes(status)) actions.push('complete');
  66. }
  67. const reviewed = Boolean(review || number(lesson && lesson.pf) > 0);
  68. if (!order.cancelled && student && [30,40].includes(status) && !reviewed) actions.push('review');
  69. return {...order,...state,serverNow:new Date().toISOString(),allowedActions:actions,blockedReason,reviewStatus:reviewed?'submitted':'pending',studentReview:review || (reviewed ? {score:number(lesson.pf),comment:lesson.pjnr,submittedAt:null} : null)};
  70. }
  71. async function databaseAppointmentVersions(input,current) {
  72. assertLessonTenant(current);
  73. const coach = number(current.get('legacyGroupId')) === 3;
  74. const uid = ownLegacyId(current), selected = coach && number(input.studentId) ? await appLearningTarget(current,input.studentId) : 0;
  75. const row=await Psql.one('SELECT MAX(GREATEST(a."updatedAt",c."updatedAt"))::text AS changed_at,COUNT(*)::int AS total FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id WHERE a.company=$1 AND a."'+(coach?'pl':'szyh')+'"::text=$2 AND ($3::numeric=0 OR a.szyh::text=$3::text)',[DEFAULT_COMPANY_ID,String(uid),selected]);
  76. const value=String(row.changed_at||'')+':'+row.total;
  77. return {version:'appointments='+value,scopeVersions:{appointments:value},refreshedAt:new Date().toISOString(),syncStatus:{state:'current'},source:'database'};
  78. }