mobile-teaching.sql 6.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. -- Atomic, retry-safe teaching writes. No historical teaching or payroll data is rewritten.
  2. CREATE TABLE IF NOT EXISTS "MobileTeachingCommand" (
  3. "company" text NOT NULL, "actorId" text NOT NULL, "requestKey" text NOT NULL,
  4. "fingerprint" text NOT NULL, "result" jsonb NOT NULL,
  5. "createdAt" timestamptz NOT NULL DEFAULT now(),
  6. PRIMARY KEY ("company","actorId","requestKey")
  7. );
  8. CREATE OR REPLACE FUNCTION xs_mobile_teaching_write(p_company text,p_actor text,p_key text,p_fingerprint text,p_operations jsonb)
  9. RETURNS jsonb LANGUAGE plpgsql AS $$
  10. DECLARE op jsonb; data jsonb; criteria jsonb; previous jsonb; saved jsonb; output jsonb := '[]';
  11. tab text; field text; columns_sql text; values_sql text; where_sql text; typ text; counter text;
  12. existing jsonb; prior "MobileTeachingCommand"%ROWTYPE;
  13. BEGIN
  14. IF length(p_key)<10 OR length(p_key)>160 OR jsonb_array_length(p_operations)>105 THEN RAISE EXCEPTION 'Invalid teaching command'; END IF;
  15. -- Serialize the learner's commands as well as duplicate request keys; nested writes share one transaction.
  16. PERFORM pg_advisory_xact_lock(hashtextextended(p_company||':'||p_actor,0));
  17. SELECT * INTO prior FROM "MobileTeachingCommand" WHERE "company"=p_company AND "actorId"=p_actor AND "requestKey"=p_key;
  18. IF FOUND THEN
  19. IF prior."fingerprint"<>p_fingerprint THEN RAISE EXCEPTION 'Request key reused with different content'; END IF;
  20. RETURN prior."result";
  21. END IF;
  22. FOR op IN SELECT value FROM jsonb_array_elements(p_operations) LOOP
  23. tab := op->>'table';
  24. IF tab NOT IN ('PracticeRecord','DailyStudyRecord','MemoryPracticeRecord','CommonModel','UnitLearningRecord','AssessmentProfile','SurveyItem','SurveyLog','DesignAnswer','LessonCompletionReport','LessonRecord') THEN RAISE EXCEPTION 'Teaching table not allowed'; END IF;
  25. data := op->'data'; criteria := op->'match';
  26. IF criteria IS NULL OR criteria='{}' THEN RAISE EXCEPTION 'Missing business key'; END IF;
  27. IF op ? 'reference' THEN
  28. FOR field,counter IN SELECT key,value#>>'{}' FROM jsonb_each(op->'reference') LOOP
  29. data := jsonb_set(data,ARRAY[field],previous->counter);
  30. IF criteria ? field THEN criteria:=jsonb_set(criteria,ARRAY[field],previous->counter); END IF;
  31. END LOOP;
  32. END IF;
  33. where_sql := '';
  34. FOR field IN SELECT jsonb_object_keys(criteria) LOOP
  35. SELECT format_type(a.atttypid,a.atttypmod) INTO typ FROM pg_attribute a WHERE a.attrelid=to_regclass(format('%I',tab)) AND a.attname=field AND NOT a.attisdropped;
  36. IF typ IS NULL THEN RAISE EXCEPTION 'Unknown match field %',field; END IF;
  37. where_sql:=where_sql||CASE WHEN where_sql='' THEN '' ELSE ' AND ' END||format('%I = ($1->>%L)::%s',field,field,typ);
  38. END LOOP;
  39. EXECUTE format('SELECT to_jsonb(t) FROM %I t WHERE %s ORDER BY "createdAt", "objectId" LIMIT 1 FOR UPDATE',tab,where_sql) INTO existing USING criteria;
  40. IF existing IS NOT NULL THEN
  41. data := data || jsonb_build_object('objectId',existing->'objectId','createdAt',existing->'createdAt');
  42. -- Stable legacy IDs must survive retries and concurrent upserts.
  43. FOREACH field IN ARRAY ARRAY['id','generalId','itemId'] LOOP
  44. IF existing ? field THEN data:=jsonb_set(data,ARRAY[field],existing->field); END IF;
  45. END LOOP;
  46. END IF;
  47. FOR field IN SELECT jsonb_array_elements_text(COALESCE(op->'increment','[]')) LOOP
  48. data:=jsonb_set(data,ARRAY[field],to_jsonb(COALESCE((existing->>field)::numeric,0)+COALESCE((data->>field)::numeric,0)));
  49. END LOOP;
  50. FOR field IN SELECT jsonb_array_elements_text(COALESCE(op->'union','[]')) LOOP
  51. data:=jsonb_set(data,ARRAY[field],(SELECT COALESCE(jsonb_agg(DISTINCT value),'[]') FROM jsonb_array_elements(COALESCE(existing->field,'[]')||COALESCE(data->field,'[]'))));
  52. END LOOP;
  53. IF tab='UnitLearningRecord' THEN data:=jsonb_set(data,'{learnedCount}',to_jsonb(jsonb_array_length(data->'wordIds'))); END IF;
  54. data:=data||jsonb_build_object('updatedAt',now());
  55. columns_sql:=''; values_sql:='';
  56. FOR field IN SELECT jsonb_object_keys(data) LOOP
  57. IF NOT EXISTS(SELECT 1 FROM pg_attribute WHERE attrelid=to_regclass(format('%I',tab)) AND attname=field AND NOT attisdropped) THEN RAISE EXCEPTION 'Unknown teaching field %.%',tab,field; END IF;
  58. columns_sql:=columns_sql||CASE WHEN columns_sql='' THEN '' ELSE ',' END||format('%I',field);
  59. values_sql:=values_sql||CASE WHEN values_sql='' THEN '' ELSE ',' END||format('r.%I',field);
  60. END LOOP;
  61. IF existing IS NULL THEN
  62. EXECUTE format('INSERT INTO %I (%s) SELECT %s FROM jsonb_populate_record(NULL::%I,$1) r RETURNING to_jsonb(%I.*)',tab,columns_sql,values_sql,tab,tab) INTO saved USING data;
  63. ELSE
  64. EXECUTE format('UPDATE %I t SET (%s)=(SELECT %s FROM jsonb_populate_record(NULL::%I,$1) r) WHERE t."objectId"=$2 RETURNING to_jsonb(t)',tab,columns_sql,values_sql,tab) INTO saved USING data,existing->>'objectId';
  65. END IF;
  66. previous:=saved; output:=output||jsonb_build_array(saved);
  67. END LOOP;
  68. INSERT INTO "MobileTeachingCommand" VALUES(p_company,p_actor,p_key,p_fingerprint,output,now());
  69. RETURN output;
  70. END $$;
  71. REVOKE ALL ON FUNCTION xs_mobile_teaching_write(text,text,text,text,jsonb) FROM PUBLIC;
  72. -- Post-lesson acknowledgement never changes credits or pay snapshots.
  73. CREATE OR REPLACE FUNCTION xs_mobile_confirm_appointment(co text,general_id text,target_status integer,actor_id numeric,administrator boolean)
  74. RETURNS jsonb LANGUAGE plpgsql AS $$
  75. DECLARE a record; old_status integer;
  76. BEGIN
  77. IF target_status NOT IN (20,30) THEN RAISE EXCEPTION '不支持的确认状态';END IF;
  78. 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=general_id FOR UPDATE OF ca;
  79. IF NOT FOUND OR a.common_status=-2 THEN RAISE EXCEPTION '预约不存在或已取消'; END IF;
  80. IF NOT administrator AND actor_id::text IS DISTINCT FROM (CASE WHEN target_status=20 THEN a."szyh"::text ELSE a."pl"::text END) THEN RAISE EXCEPTION '无权确认该预约';END IF;
  81. old_status:=a."dszt"::integer;
  82. IF old_status>=target_status THEN RETURN jsonb_build_object('GeneralID',general_id,'status',old_status,'unchanged',true);END IF;
  83. IF (target_status=20 AND old_status<>11) OR (target_status=30 AND old_status<>20) THEN RAISE EXCEPTION '预约状态已变更,请刷新';END IF;
  84. UPDATE "CourseAppointment" SET "dszt"=target_status::text,"updatedAt"=now() WHERE "objectId"=a."objectId";
  85. RETURN jsonb_build_object('GeneralID',general_id,'previousStatus',old_status,'status',target_status,'unchanged',false);
  86. END $$;
  87. REVOKE ALL ON FUNCTION xs_mobile_confirm_appointment(text,text,integer,numeric,boolean) FROM PUBLIC;