student-lesson-statistics.js 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041
  1. // Display-only statistics from completed lesson facts. Never transition an appointment
  2. // or infer a balance from attendance; reservations and financial ledgers remain independent.
  3. function studentLessonStatisticsSql() {
  4. return `WITH facts AS (
  5. SELECT l.*,COALESCE(NULLIF(NULLIF(l.yyds,'0'),''),'lesson:'||l."objectId") AS "classKey",
  6. COALESCE(r."actualStartAt",l."lessonAt",xiaoshu_safe_shanghai_timestamp(l.plpjsj),c."createdAt",l."sourceCreatedAt",l."createdAt") AS "actualAt",
  7. COALESCE(NULLIF(CASE WHEN r."durationMinutes">0 AND r."durationMinutes"::text NOT IN ('NaN','Infinity','-Infinity') THEN r."durationMinutes" END,0),
  8. CASE WHEN a."durationMinutes">0 AND a."durationMinutes"::text NOT IN ('NaN','Infinity','-Infinity') THEN a."durationMinutes" END,
  9. CASE WHEN to_jsonb(l)->>'durationMinutes' ~ '^\\d+(\\.\\d+)?$' THEN NULLIF((to_jsonb(l)->>'durationMinutes')::numeric,0) END,
  10. CASE l.kclx WHEN '1' THEN 30 WHEN '2' THEN 60 WHEN '3' THEN 60 END) AS minutes,
  11. COALESCE(NULLIF(l."courseCategoryKey",''),NULLIF(r."courseCategoryKey",''),a."courseCategoryKey",'') AS category
  12. FROM "LessonRecord" l
  13. LEFT JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c.company=l.company AND c."modelId"=59 AND c."itemId"=l.id ORDER BY c."objectId" LIMIT 1) c ON true
  14. LEFT JOIN LATERAL (SELECT r.* FROM "LessonCompletionReport" r WHERE r.company=l.company AND r."studentId"::text=l.xymz
  15. AND (r."lessonId"=l."objectId" OR r."appointmentGeneralId"::numeric=CASE WHEN l.yyds~'^\\d+$' THEN l.yyds::numeric END)
  16. ORDER BY r."submittedAt" DESC NULLS LAST,r."objectId" DESC LIMIT 1) r ON true
  17. LEFT JOIN LATERAL (SELECT a.* FROM "CommonModel" ac JOIN "CourseAppointment" a ON a.company=ac.company AND a.id=ac."itemId" AND a.szyh=l.xymz
  18. WHERE ac.company=l.company AND ac."modelId"=54 AND ac."generalId"=CASE WHEN l.yyds~'^\\d+$' THEN l.yyds::numeric END ORDER BY ac."objectId" LIMIT 1) a ON true
  19. WHERE l.company=$1 AND ($2='' OR l.xymz=$2) AND c.status IS DISTINCT FROM -2
  20. ), ranked AS (SELECT facts.*,row_number() OVER(PARTITION BY company,xymz,"classKey" ORDER BY "actualAt" DESC NULLS LAST,"objectId") rn FROM facts),
  21. lessons AS (SELECT * FROM ranked WHERE rn=1)
  22. SELECT xymz AS "studentId",COUNT(*)::int total,
  23. COUNT(*) FILTER(WHERE minutes=30 AND kclx IS DISTINCT FROM '3' AND category<>'trial')::int t30,
  24. COUNT(*) FILTER(WHERE minutes=60 AND kclx IS DISTINCT FROM '3' AND category<>'trial')::int t60,
  25. COUNT(*) FILTER(WHERE kclx='3' OR category='trial')::int tiyan,
  26. COUNT(*) FILTER(WHERE minutes IS NULL)::int "unknownDurationCount",
  27. CASE WHEN COUNT(*) FILTER(WHERE minutes IS NULL)>0 THEN NULL ELSE SUM(minutes) END AS "totalMinutes"
  28. FROM lessons GROUP BY xymz`;
  29. }
  30. function studentUnmatchedLessonsSql() {
  31. return `SELECT a.szyh AS "studentId",COUNT(*)::int total FROM "CourseAppointment" a
  32. LEFT JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id ORDER BY c."objectId" LIMIT 1) c ON true
  33. WHERE a.company=$1 AND ($2='' OR a.szyh=$2) AND a.dszt ~ '^[0-9]+$' AND a.dszt::numeric>10 AND c.status IS DISTINCT FROM -2
  34. AND NOT EXISTS(SELECT 1 FROM "LessonRecord" l WHERE l.company=a.company AND l.xymz=a.szyh AND l.yyds=c."generalId"::text) GROUP BY a.szyh`;
  35. }
  36. async function studentLessonStatistics(companyId,studentId) {
  37. const [rows,missing]=await Promise.all([Psql.query(studentLessonStatisticsSql(),[companyId,String(studentId)]),Psql.query(studentUnmatchedLessonsSql(),[companyId,String(studentId)])]);
  38. const result=rows[0]||{total:0,t30:0,t60:0,tiyan:0,totalMinutes:0,unknownDurationCount:0};
  39. const unresolved=Number(missing[0]?.total)||0;
  40. return {...result,unknownDurationCount:Number(result.unknownDurationCount)+unresolved,totalMinutes:unresolved?null:result.totalMinutes};
  41. }