lesson-workflow.sql 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. -- Install after member-enrollment.sql and mobile-teaching.sql. No historical backfill.
  2. CREATE TABLE IF NOT EXISTS "LessonWorkflowCommand" (
  3. company text NOT NULL, actor text NOT NULL, "requestKey" text NOT NULL,
  4. fingerprint jsonb NOT NULL, result jsonb NOT NULL, "createdAt" timestamptz DEFAULT now(),
  5. PRIMARY KEY(company,actor,"requestKey")
  6. );
  7. CREATE TABLE IF NOT EXISTS "LessonReview" (
  8. company text NOT NULL, "appointmentGeneralId" text NOT NULL, "studentId" numeric NOT NULL,
  9. score integer NOT NULL CHECK(score BETWEEN 1 AND 5), comment text NOT NULL CHECK(length(comment)<=200),
  10. "submittedAt" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(company,"appointmentGeneralId")
  11. );
  12. CREATE TABLE IF NOT EXISTS "LessonWorkflowIssue" (
  13. company text NOT NULL, "appointmentGeneralId" text NOT NULL, message text NOT NULL,
  14. "updatedAt" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(company,"appointmentGeneralId")
  15. );
  16. ALTER TABLE "CourseAppointment" ADD COLUMN IF NOT EXISTS "teachingManaged" boolean;
  17. ALTER TABLE "CommonModel" ADD COLUMN IF NOT EXISTS "teachingManaged" boolean;
  18. ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "teachingManaged" boolean;
  19. ALTER TABLE "LessonCompletionReport" ADD COLUMN IF NOT EXISTS "coachComment" text;
  20. CREATE OR REPLACE FUNCTION xs_lesson_complete(co text,gid text,actor text,actor_uid numeric,expected timestamptz,request_key text,feedback jsonb)
  21. RETURNS jsonb LANGUAGE plpgsql AS $$
  22. DECLARE a "CourseAppointment"; c "CommonModel"; l "LessonRecord"; report "LessonCompletionReport";
  23. previous "LessonWorkflowCommand"; result jsonb; lesson_count int; cost jsonb; ledger_table text; evidence int;
  24. study record; study_words jsonb; study_course text; found_study boolean=false; words jsonb='[]'; review_date text; review_key text; review_id numeric; review_object text;
  25. total_reviews int; completed_reviews int; report_id text; was_charged boolean=false;
  26. BEGIN
  27. IF request_key IS NULL OR length(request_key)<10 OR length(request_key)>160 THEN RAISE EXCEPTION '缺少有效请求编号';END IF;
  28. IF NOT EXISTS(SELECT 1 FROM "_User" WHERE "objectId"=actor AND company=co AND "legacyUserId"=actor_uid AND "legacyGroupId"=3 AND NOT COALESCE("isDisabled",false)) THEN RAISE EXCEPTION '用户身份已失效';END IF;
  29. IF NULLIF(trim(feedback->>'contentSummary'),'') IS NULL OR length(feedback->>'contentSummary')>1000
  30. OR length(COALESCE(feedback->>'reviewStatus',''))>200
  31. OR length(COALESCE(feedback->>'trainingStatus',''))>200
  32. OR NULLIF(trim(feedback->>'trainingSummary'),'') IS NULL OR length(feedback->>'trainingSummary')>1000
  33. OR (NOT COALESCE((feedback->>'noHomework')::boolean,false) AND NULLIF(trim(feedback->>'homework'),'') IS NULL)
  34. THEN RAISE EXCEPTION '请完整填写学习内容、进度说明和作业';END IF;
  35. PERFORM pg_advisory_xact_lock(hashtext('xs-credit:'||co));
  36. SELECT * INTO c FROM "CommonModel" WHERE company=co AND "modelId"=54 AND "generalId"::text=gid;
  37. SELECT * INTO a FROM "CourseAppointment" WHERE company=co AND id=c."itemId";
  38. IF a."objectId" IS NULL OR c.status=-2 THEN RAISE EXCEPTION '预约不存在或已取消';END IF;
  39. IF a.pl::text IS DISTINCT FROM actor_uid::text THEN RAISE EXCEPTION '仅预约老师可以结课';END IF;
  40. -- Same lock order as credit transfers and xs_complete_appointment.
  41. PERFORM 1 FROM "_User" WHERE company=co AND "legacyUserId"::text=a.szyh::text FOR UPDATE;
  42. SELECT * INTO a FROM "CourseAppointment" WHERE "objectId"=a."objectId" FOR UPDATE;
  43. SELECT * INTO previous FROM "LessonWorkflowCommand" WHERE company=co AND "LessonWorkflowCommand".actor=xs_lesson_complete.actor AND "requestKey"=request_key;
  44. IF FOUND THEN
  45. IF previous.fingerprint<>jsonb_build_object('id',gid,'feedback',feedback) THEN RAISE EXCEPTION '请求编号已用于其他内容';END IF;
  46. RETURN previous.result;
  47. END IF;
  48. IF (SELECT count(*) FROM "LessonCompletionReport" WHERE company=co AND "appointmentGeneralId"::text=gid)>1 THEN RAISE EXCEPTION '历史凭据存在重复报告,请由后台核对';END IF;
  49. SELECT * INTO report FROM "LessonCompletionReport" WHERE company=co AND "appointmentGeneralId"::text=gid ORDER BY "submittedAt" DESC LIMIT 1;
  50. IF a.dszt='30' AND report."objectId" IS NOT NULL THEN RETURN jsonb_build_object('GeneralID',gid,'status',30,'unchanged',true,'feedbackId',report."objectId");END IF;
  51. IF expected IS NULL OR a."updatedAt" IS DISTINCT FROM expected THEN RAISE EXCEPTION '预约已更新,请刷新详情后重试';END IF;
  52. IF a.dszt NOT IN ('10','11','20') THEN RAISE EXCEPTION '请先开始课程';END IF;
  53. -- Versioned batches and completion serialize on the same xs-credit lock.
  54. IF to_regclass('"LessonStudyBatch"') IS NOT NULL THEN
  55. IF EXISTS(SELECT 1 FROM "LessonStudyBatch" WHERE company=co AND "orderId"=gid AND status='draft') THEN RAISE EXCEPTION '还有未确认的学习批次,请先确认掌握状态或放弃该批次'; END IF;
  56. IF EXISTS(SELECT 1 FROM "LessonStudyBatch" WHERE company=co AND "orderId"=gid AND status='confirmed') OR EXISTS(SELECT 1 FROM "DailyStudyRecord" WHERE company=co AND dsid::text=gid AND "userId"::text=a.szyh::text) THEN
  57. PERFORM xs_lesson_study_rebuild(co,gid);
  58. IF EXISTS(SELECT 1 FROM "LessonStudyBatch" WHERE company=co AND "orderId"=gid AND status='draft') THEN RAISE EXCEPTION '历史学习批次尚未确认,请先确认掌握状态'; END IF;
  59. UPDATE "DailyStudyRecord" SET fxrl=CASE WHEN COALESCE(learned,0)>0 THEN (SELECT string_agg(to_char((now() AT TIME ZONE 'Asia/Shanghai')::date+offset_days,'YYYYMMDD'),',' ORDER BY offset_days) FROM unnest(ARRAY[1,2,3,5,7,9,11,14,17,21]) offset_days) ELSE '' END WHERE company=co AND dsid::text=gid AND "recordScope"='lesson';
  60. END IF;
  61. END IF;
  62. SELECT count(*) INTO lesson_count FROM "LessonRecord" WHERE company=co AND yyds::text=gid;
  63. IF lesson_count>1 THEN RAISE EXCEPTION '历史凭据存在重复上课记录,请由后台核对';END IF;
  64. SELECT * INTO l FROM "LessonRecord" WHERE company=co AND yyds::text=gid LIMIT 1;
  65. IF l."objectId" IS NOT NULL THEN words=COALESCE(NULLIF(l.kzsj,'')::jsonb,'[]');END IF;
  66. IF l."objectId" IS NOT NULL OR a.dszt IN ('11','20') THEN
  67. IF l."objectId" IS NULL OR l.xymz::text IS DISTINCT FROM a.szyh::text OR l.jsmz::text IS DISTINCT FROM a.pl::text
  68. OR NOT EXISTS(SELECT 1 FROM "CommonModel" WHERE company=co AND "modelId"=59 AND "itemId"=l.id AND status=99)
  69. OR jsonb_array_length(COALESCE(a."creditCostsSnapshot"::jsonb,'[]'))=0
  70. OR (SELECT count(*) FROM "CourseCreditReservation" WHERE company=co AND "appointmentId"=c."objectId" AND state='consumed' AND "creditCosts"::jsonb=a."creditCostsSnapshot"::jsonb)<>1
  71. THEN RAISE EXCEPTION '历史凭据不完整,请由后台核对课时,不能自动补扣';END IF;
  72. FOR cost IN SELECT value FROM jsonb_array_elements(a."creditCostsSnapshot"::jsonb) LOOP
  73. ledger_table=CASE cost->>'account' WHEN 'Purse' THEN 'UserExpDomP' WHEN 'SilverCoin' THEN 'UserSIcon' WHEN 'UserExp' THEN 'UserExpHis' WHEN 'UserPoint' THEN 'UserUserPoint' END;
  74. IF ledger_table IS NULL THEN RAISE EXCEPTION '历史凭据的账户无效';END IF;
  75. EXECUTE format('SELECT count(*) FROM %I WHERE company=$1 AND "userId"::text=$2 AND "sourceKey"=$3 AND score=$4',ledger_table)
  76. INTO evidence USING co,a.szyh::text,'credit:complete:'||c."objectId"||':'||a.szyh||':'||(cost->>'account'),-(cost->>'amount')::numeric;
  77. IF evidence<>1 THEN RAISE EXCEPTION '历史凭据与课时扣减不一致,请由后台核对';END IF;
  78. END LOOP;
  79. ELSE
  80. -- A fresh completion must consume exactly the reservation created by scheduling.
  81. IF (SELECT count(*) FROM "CourseCreditReservation" WHERE company=co AND "appointmentId"=c."objectId" AND state='reserved' AND "creditCosts"::jsonb=a."creditCostsSnapshot"::jsonb)<>1
  82. THEN RAISE EXCEPTION '历史凭据缺少匹配的课时预留,请由后台核对';END IF;
  83. result=xs_complete_appointment(co,c."objectId",actor);
  84. was_charged=COALESCE((result->>'periodDeducted')::boolean,false);
  85. SELECT * INTO l FROM "LessonRecord" WHERE company=co AND "objectId"=result->>'lessonId';
  86. END IF;
  87. FOR study IN SELECT d.*,sc."generalId" AS study_general_id,b.kcid AS study_course_id FROM "DailyStudyRecord" d
  88. JOIN "CommonModel" sc ON sc.company=d.company AND sc."modelId"=56 AND sc."itemId"=d.id AND sc.status=99
  89. LEFT JOIN "CourseBinding" b ON b.company=d.company AND b."objectId"=to_jsonb(d)->>'bindingId' AND b.yhid::text=a.szyh::text
  90. WHERE d.company=co AND d."userId"::text=a.szyh::text AND d.dsid::text=gid AND COALESCE(to_jsonb(d)->>'recordScope','')<>'lesson-alias' ORDER BY d."updatedAt" DESC,d.id DESC LOOP
  91. IF NOT found_study THEN words='[]';found_study=true;END IF;
  92. study_words=COALESCE(NULLIF(study.xxqs,'')::jsonb,'[]');
  93. IF jsonb_typeof(study_words)<>'array' THEN RAISE EXCEPTION '课堂学习记录格式异常,请核对后重试';END IF;
  94. words=words||study_words;study_course=COALESCE(study.study_course_id,a.kcid);
  95. IF jsonb_array_length(study_words)>0 THEN
  96. FOR review_date IN SELECT DISTINCT value FROM unnest(string_to_array(COALESCE(study.fxrl,''),',')) value WHERE value ~ '^\d{8}$' LIMIT 15 LOOP
  97. review_key='cloud:e_order_update:'||gid||CASE WHEN NULLIF(to_jsonb(study)->>'bindingId','') IS NOT NULL AND (to_jsonb(study)->>'bindingId') IS DISTINCT FROM a."bindingId" THEN ':study:'||study.study_general_id ELSE '' END||':review:'||review_date;
  98. SELECT id,"objectId" INTO review_id,review_object FROM "MemoryPracticeRecord" WHERE company=co AND "sourceKey"=review_key LIMIT 1;
  99. IF review_object IS NULL THEN
  100. review_id=nextval('xiaoshu_business_id');review_object=xs_object_id();
  101. INSERT INTO "MemoryPracticeRecord" SELECT * FROM jsonb_populate_record(NULL::"MemoryPracticeRecord",jsonb_build_object(
  102. 'objectId',review_object,'company',co,'id',review_id,'createdAt',now(),'updatedAt',now(),'sourceKey',review_key,
  103. 'yhid',a.szyh,'plid',COALESCE(NULLIF(a.fxpl,''),a.pl),'orderId',gid,'xxjlid',study.study_general_id,'kcid',study_course,
  104. 'fxzt','0','kywrq',to_char(to_date(review_date,'YYYYMMDD'),'YYYY-MM-DD'),'kywsj',to_char(to_date(review_date,'YYYYMMDD'),'YYYY-MM-DD')||' '||COALESCE(a.sdsd,'00:00')));
  105. END IF;
  106. IF NOT EXISTS(SELECT 1 FROM "CommonModel" WHERE company=co AND "sourceKey"=review_key) THEN
  107. PERFORM xs_insert('CommonModel',jsonb_build_object('objectId',xs_object_id(),'company',co,'createdAt',now(),'updatedAt',now(),'sourceKey',review_key,'generalId',nextval('xiaoshu_business_id'),'itemId',review_id,'modelId',60,'nodeId',388,'tableName','ZL_C_gywjl','title',c.title||'@抗遗忘@'||review_date,'inputer',actor,'status',99));
  108. END IF;
  109. END LOOP;
  110. END IF;
  111. END LOOP;
  112. -- Combine all selected books for the lesson report without counting a word twice.
  113. SELECT COALESCE(jsonb_agg(value ORDER BY ord),'[]'::jsonb) INTO words FROM (
  114. SELECT DISTINCT ON (COALESCE(value->>'GeneralID',value::text)) value,ord
  115. FROM jsonb_array_elements(words) WITH ORDINALITY w(value,ord)
  116. ORDER BY COALESCE(value->>'GeneralID',value::text),ord
  117. ) unique_words;
  118. UPDATE "LessonRecord" SET kzsj=words::text,pldp=COALESCE(NULLIF(feedback->>'coachComment',''),NULLIF(feedback->>'notes',''),pldp),plpjsj=now()::text,"teachingManaged"=true,"updatedAt"=now() WHERE "objectId"=l."objectId";
  119. UPDATE "CourseAppointment" SET dszt='30',jssj=COALESCE(NULLIF(jssj,''),now()::text),"teachingManaged"=true,"updatedAt"=now() WHERE "objectId"=a."objectId";
  120. UPDATE "CommonModel" SET "teachingManaged"=true,"updatedAt"=now() WHERE company=co AND ("objectId"=c."objectId" OR ("modelId"=59 AND "itemId"=l.id));
  121. SELECT count(*),count(*) FILTER(WHERE fxzt='1') INTO total_reviews,completed_reviews FROM "MemoryPracticeRecord" WHERE company=co AND "orderId"::text=gid;
  122. report_id=COALESCE(report."objectId",xs_object_id());
  123. -- Existing partial reports are replaced in the same transaction, preserving their ID.
  124. DELETE FROM "LessonCompletionReport" WHERE "objectId"=report_id AND company=co;
  125. INSERT INTO "LessonCompletionReport" SELECT * FROM jsonb_populate_record(NULL::"LessonCompletionReport",feedback||jsonb_build_object(
  126. 'objectId',report_id,'company',co,'createdAt',COALESCE(report."createdAt",now()),'updatedAt',now(),'sourceKey','cloud:lesson-feedback:'||co||':'||gid,
  127. 'appointmentId',a."objectId",'appointmentGeneralId',gid,'lessonId',l."objectId",'studentId',a.szyh,'coachId',a.pl,
  128. 'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'deliveryMode',a."deliveryMode",'durationMinutes',a."durationMinutes",
  129. 'actualStartAt',NULLIF(a.kssj,'')::timestamptz,'actualEndAt',COALESCE(NULLIF(a.jssj,'')::timestamptz,now()),'creditCosts',a."creditCostsSnapshot",
  130. 'vocabularyCount',jsonb_array_length(words),'antiForgetSummary',jsonb_build_object('total',total_reviews,'completed',completed_reviews,'pending',total_reviews-completed_reviews),
  131. 'homework',CASE WHEN COALESCE((feedback->>'noHomework')::boolean,false) THEN '无作业' ELSE feedback->>'homework' END,'submittedBy',actor,'submittedAt',now()));
  132. DELETE FROM "LessonWorkflowIssue" WHERE company=co AND "appointmentGeneralId"=gid;
  133. result=jsonb_build_object('GeneralID',gid,'status',30,'lessonId',l."objectId",'feedbackId',report_id,'periodDeducted',was_charged);
  134. INSERT INTO "LessonWorkflowCommand" VALUES(co,actor,request_key,jsonb_build_object('id',gid,'feedback',feedback),result,now());
  135. RETURN result;
  136. END $$;
  137. REVOKE ALL ON FUNCTION xs_lesson_complete(text,text,text,numeric,timestamptz,text,jsonb) FROM PUBLIC;
  138. CREATE OR REPLACE FUNCTION xs_lesson_review(co text,gid text,actor text,actor_uid numeric,rating integer,review_comment text)
  139. RETURNS jsonb LANGUAGE plpgsql AS $$
  140. DECLARE a record; l record; existing "LessonReview";
  141. BEGIN
  142. IF rating IS NULL OR rating NOT BETWEEN 1 AND 5 OR review_comment IS NULL OR length(review_comment)>200 THEN RAISE EXCEPTION '评分应为1至5分,评价最多200字';END IF;
  143. IF NOT EXISTS(SELECT 1 FROM "_User" WHERE "objectId"=actor AND company=co AND "legacyUserId"=actor_uid AND NOT COALESCE("isDisabled",false)) THEN RAISE EXCEPTION '用户身份已失效';END IF;
  144. SELECT ca.*,c.status AS common_status INTO a FROM "CourseAppointment" ca JOIN "CommonModel" c ON c.company=ca.company AND c."modelId"=54 AND c."itemId"=ca.id WHERE c.company=co AND c."generalId"::text=gid FOR UPDATE OF ca;
  145. IF NOT FOUND OR a.common_status=-2 THEN RAISE EXCEPTION '预约不存在或已取消';END IF;
  146. IF a.szyh::text IS DISTINCT FROM actor_uid::text THEN RAISE EXCEPTION '仅预约学生可以评价';END IF;
  147. IF a.dszt NOT IN ('30','40') THEN RAISE EXCEPTION '课程尚未结课';END IF;
  148. SELECT * INTO existing FROM "LessonReview" WHERE company=co AND "appointmentGeneralId"=gid;
  149. IF FOUND THEN RETURN to_jsonb(existing)||jsonb_build_object('unchanged',true,'status',a.dszt::int);END IF;
  150. IF (SELECT count(*) FROM "LessonRecord" WHERE company=co AND yyds::text=gid)<>1 THEN RAISE EXCEPTION '上课记录缺失或重复,请由后台核对';END IF;
  151. SELECT * INTO l FROM "LessonRecord" WHERE company=co AND yyds::text=gid FOR UPDATE;
  152. IF COALESCE(l.pf::text,'') ~ '^[1-5](\.0+)?$' THEN RETURN jsonb_build_object('score',l.pf,'comment',l.pjnr,'unchanged',true,'status',a.dszt::int);END IF;
  153. INSERT INTO "LessonReview" VALUES(co,gid,actor_uid,rating,review_comment,now()) RETURNING * INTO existing;
  154. UPDATE "LessonRecord" SET pf=rating,pjnr=review_comment,"teachingManaged"=true,"updatedAt"=now() WHERE "objectId"=l."objectId";
  155. UPDATE "CourseAppointment" SET "teachingManaged"=true,"updatedAt"=now() WHERE "objectId"=a."objectId";
  156. UPDATE "CommonModel" SET "teachingManaged"=true,"updatedAt"=now() WHERE company=co AND (("modelId"=54 AND "itemId"=a.id) OR ("modelId"=59 AND "itemId"=l.id));
  157. RETURN to_jsonb(existing)||jsonb_build_object('status',a.dszt::int);
  158. END $$;
  159. REVOKE ALL ON FUNCTION xs_lesson_review(text,text,text,numeric,integer,text) FROM PUBLIC;
  160. -- Recheck assignment under the same lock as scheduling; the preceding HTTP read is not authority.
  161. CREATE OR REPLACE FUNCTION xs_start_appointment_for_actor(co text,aid text,actor text,actor_uid numeric)
  162. RETURNS void LANGUAGE plpgsql AS $$
  163. DECLARE assigned text;
  164. BEGIN
  165. PERFORM pg_advisory_xact_lock(hashtext('xs-credit:'||co));
  166. SELECT a.pl INTO assigned FROM "CourseAppointment" a JOIN "CommonModel" c ON c.company=a.company AND c."modelId"=54 AND c."itemId"=a.id WHERE c.company=co AND c."objectId"=aid;
  167. IF assigned IS DISTINCT FROM actor_uid::text OR NOT EXISTS(SELECT 1 FROM "_User" WHERE company=co AND "objectId"=actor AND "legacyUserId"=actor_uid AND "legacyGroupId"=3 AND NOT COALESCE("isDisabled",false)) THEN RAISE EXCEPTION '仅预约老师可以开始课程';END IF;
  168. PERFORM xs_start_appointment(co,aid);
  169. END $$;
  170. REVOKE ALL ON FUNCTION xs_start_appointment_for_actor(text,text,text,numeric) FROM PUBLIC;
  171. -- Protect completed lessons even if a legacy sync read races with the teacher command.
  172. CREATE OR REPLACE FUNCTION xs_lesson_no_regression() RETURNS trigger LANGUAGE plpgsql AS $$
  173. BEGIN
  174. IF OLD."teachingManaged" IS TRUE AND (
  175. (OLD.dszt IN ('30','40') AND NEW.dszt IS DISTINCT FROM OLD.dszt AND NOT (OLD.dszt='30' AND NEW.dszt='40'))
  176. OR (OLD.dszt='10' AND NEW.dszt NOT IN ('10','11','20','30','40'))
  177. OR NEW."teachingManaged" IS DISTINCT FROM true)
  178. THEN RAISE EXCEPTION '新系统课次不可由旧流程回退';END IF;
  179. RETURN NEW;
  180. END $$;
  181. DROP TRIGGER IF EXISTS lesson_no_regression ON "CourseAppointment";
  182. CREATE TRIGGER lesson_no_regression BEFORE UPDATE ON "CourseAppointment" FOR EACH ROW EXECUTE FUNCTION xs_lesson_no_regression();
  183. -- Administrators retain the existing correction operation with a required, transactional audit reason.
  184. CREATE OR REPLACE FUNCTION xs_admin_lesson_correction(co text,aid text,actor text,reason text)
  185. RETURNS jsonb LANGUAGE plpgsql AS $$
  186. DECLARE result jsonb;
  187. BEGIN
  188. IF NULLIF(trim(reason),'') IS NULL THEN RAISE EXCEPTION '请填写纠错原因';END IF;
  189. result=xs_complete_appointment(co,aid,actor);
  190. PERFORM xs_insert('EnrollmentCommand',jsonb_build_object('objectId',xs_object_id(),'company',co,'createdAt',now(),'updatedAt',now(),'idempotencyKey',xs_object_id(),'request',jsonb_build_object('actor',actor,'reason',reason,'appointmentId',aid,'operation','admin.lesson-correction'),'result',result));
  191. RETURN result;
  192. END $$;
  193. REVOKE ALL ON FUNCTION xs_admin_lesson_correction(text,text,text,text) FROM PUBLIC;
  194. -- Feedback-only amendment: never re-run completion, credit or learning projections.
  195. CREATE OR REPLACE FUNCTION xs_lesson_feedback_update(co text,gid text,actor text,actor_uid numeric,expected timestamptz,request_key text,feedback jsonb)
  196. RETURNS jsonb LANGUAGE plpgsql AS $$
  197. DECLARE a "CourseAppointment"; c "CommonModel"; report "LessonCompletionReport";
  198. previous "LessonWorkflowCommand"; result jsonb; old_feedback jsonb;
  199. BEGIN
  200. IF request_key IS NULL OR length(request_key)<10 OR length(request_key)>160 THEN RAISE EXCEPTION '缺少有效请求编号';END IF;
  201. IF NOT EXISTS(SELECT 1 FROM "_User" WHERE "objectId"=actor AND company=co AND "legacyUserId"=actor_uid AND "legacyGroupId"=3 AND NOT COALESCE("isDisabled",false)) THEN RAISE EXCEPTION '用户身份已失效';END IF;
  202. IF NULLIF(trim(feedback->>'contentSummary'),'') IS NULL OR NULLIF(trim(feedback->>'trainingSummary'),'') IS NULL
  203. OR (NOT COALESCE((feedback->>'noHomework')::boolean,false) AND NULLIF(trim(feedback->>'homework'),'') IS NULL)
  204. OR EXISTS(SELECT 1 FROM jsonb_each_text(feedback) f WHERE length(f.value)>CASE WHEN f.key IN ('reviewStatus','trainingStatus') THEN 200 ELSE 1000 END)
  205. THEN RAISE EXCEPTION '请完整填写学习内容、进度说明和作业,并检查文字长度';END IF;
  206. PERFORM pg_advisory_xact_lock(hashtext('xs-credit:'||co));
  207. SELECT * INTO c FROM "CommonModel" WHERE company=co AND "modelId"=54 AND "generalId"::text=gid FOR UPDATE;
  208. SELECT * INTO a FROM "CourseAppointment" WHERE company=co AND id=c."itemId" FOR UPDATE;
  209. IF a."objectId" IS NULL OR c.status=-2 THEN RAISE EXCEPTION '预约不存在或已取消';END IF;
  210. IF a.pl::text IS DISTINCT FROM actor_uid::text THEN RAISE EXCEPTION '仅预约老师可以修改报告';END IF;
  211. IF a.dszt NOT IN ('30','40') THEN RAISE EXCEPTION '请先完成课程再修改报告';END IF;
  212. SELECT * INTO previous FROM "LessonWorkflowCommand" WHERE company=co AND "LessonWorkflowCommand".actor=xs_lesson_feedback_update.actor AND "requestKey"=request_key;
  213. IF FOUND THEN
  214. IF previous.fingerprint<>jsonb_build_object('action','feedback_update','id',gid,'feedback',feedback) THEN RAISE EXCEPTION '请求编号已用于其他内容';END IF;
  215. RETURN previous.result - 'previousFeedback';
  216. END IF;
  217. IF (SELECT count(*) FROM "LessonCompletionReport" WHERE company=co AND "appointmentGeneralId"::text=gid)<>1 THEN RAISE EXCEPTION '报告不存在或不唯一,请联系管理员核对';END IF;
  218. SELECT * INTO report FROM "LessonCompletionReport" WHERE company=co AND "appointmentGeneralId"::text=gid FOR UPDATE;
  219. IF expected IS NULL OR report."updatedAt" IS DISTINCT FROM expected THEN RAISE EXCEPTION '报告已被更新,请关闭编辑并刷新详情后重试';END IF;
  220. SELECT jsonb_object_agg(key,value) INTO old_feedback FROM jsonb_each(to_jsonb(report)) WHERE key IN ('contentSummary','reviewStatus','reviewSummary','trainingStatus','trainingSummary','homework','noHomework','coachComment','notes');
  221. UPDATE "LessonCompletionReport" SET
  222. "contentSummary"=feedback->>'contentSummary',"reviewStatus"=feedback->>'reviewStatus',"reviewSummary"=feedback->>'reviewSummary',
  223. "trainingStatus"=feedback->>'trainingStatus',"trainingSummary"=feedback->>'trainingSummary',
  224. homework=CASE WHEN COALESCE((feedback->>'noHomework')::boolean,false) THEN '无作业' ELSE feedback->>'homework' END,
  225. "noHomework"=COALESCE((feedback->>'noHomework')::boolean,false),"coachComment"=feedback->>'coachComment',notes=feedback->>'notes',"updatedAt"=clock_timestamp()
  226. WHERE "objectId"=report."objectId";
  227. UPDATE "LessonRecord" SET pldp=COALESCE(feedback->>'coachComment',''),plpjsj=now()::text,"updatedAt"=now()
  228. WHERE company=co AND "objectId"=report."lessonId" AND yyds::text=gid;
  229. UPDATE "CourseAppointment" SET "updatedAt"=now() WHERE "objectId"=a."objectId";
  230. UPDATE "CommonModel" SET "updatedAt"=now() WHERE "objectId"=c."objectId";
  231. result=jsonb_build_object('GeneralID',gid,'feedbackId',report."objectId",'saved',true,'previousFeedback',old_feedback);
  232. INSERT INTO "LessonWorkflowCommand" VALUES(co,actor,request_key,jsonb_build_object('action','feedback_update','id',gid,'feedback',feedback),result,now());
  233. RETURN result - 'previousFeedback';
  234. END $$;
  235. REVOKE ALL ON FUNCTION xs_lesson_feedback_update(text,text,text,numeric,timestamptz,text,jsonb) FROM PUBLIC;