review-rewards.sql 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. -- Applied as one transaction before publishing the app/operations gateways.
  2. BEGIN;
  3. LOCK TABLE "MemoryPracticeRecord" IN SHARE ROW EXCLUSIVE MODE;
  4. ALTER TABLE "MemoryPracticeRecord" ADD COLUMN IF NOT EXISTS "rewardTaskKey" text;
  5. ALTER TABLE "MemoryPracticeRecord" ADD COLUMN IF NOT EXISTS "rewardDuplicate" boolean;
  6. ALTER TABLE "PayrollLine" ADD COLUMN IF NOT EXISTS "reviewRewardId" text;
  7. ALTER TABLE "PayrollLine" ADD COLUMN IF NOT EXISTS "reviewTaskId" text;
  8. ALTER TABLE "PayrollLine" ADD COLUMN IF NOT EXISTS "plannedDate" text;
  9. ALTER TABLE "PayrollBatch" ADD COLUMN IF NOT EXISTS "reviewCount" double precision;
  10. ALTER TABLE "PayrollBatch" ADD COLUMN IF NOT EXISTS "reviewAmount" double precision;
  11. CREATE TABLE IF NOT EXISTS "ReviewReward" (
  12. "objectId" text PRIMARY KEY, "company" text NOT NULL, "taskId" text NOT NULL,
  13. "taskKey" text, "coachId" double precision NOT NULL DEFAULT 0, "coachName" text NOT NULL DEFAULT '',
  14. "studentId" double precision NOT NULL DEFAULT 0, "studentName" text NOT NULL DEFAULT '',
  15. "courseName" text NOT NULL DEFAULT '', "storeId" double precision NOT NULL DEFAULT 0, "storeName" text NOT NULL DEFAULT '',
  16. "plannedDate" text NOT NULL DEFAULT '', "completedAt" timestamptz, "incomeMonth" text NOT NULL DEFAULT '',
  17. "amount" numeric(12,2) NOT NULL DEFAULT 0, "status" text NOT NULL,
  18. "exception" text NOT NULL DEFAULT '', "createdAt" timestamptz NOT NULL DEFAULT now(),
  19. UNIQUE ("company","taskId"), CHECK ("status" IN ('earned','historical','exception')),
  20. CHECK (("status"='earned' AND "amount"=1 AND "completedAt" IS NOT NULL AND "coachId">0) OR ("status"<>'earned' AND "amount"=0))
  21. );
  22. CREATE UNIQUE INDEX IF NOT EXISTS review_reward_once_per_plan ON "ReviewReward" ("company","taskKey") WHERE "status"='earned';
  23. CREATE INDEX IF NOT EXISTS review_reward_coach_month ON "ReviewReward" ("company","coachId","incomeMonth");
  24. CREATE UNIQUE INDEX IF NOT EXISTS payroll_company_month_once ON "PayrollBatch" ("company","month");
  25. CREATE UNIQUE INDEX IF NOT EXISTS payroll_review_reward_once ON "PayrollLine" ("company","reviewRewardId") WHERE "kind"='review_reward' AND COALESCE("excluded",false)=false;
  26. CREATE OR REPLACE FUNCTION xiaoshu_review_key(student text, study text, day text) RETURNS text
  27. LANGUAGE sql IMMUTABLE AS $$
  28. SELECT CASE WHEN student ~ '^[1-9][0-9]*$' AND study ~ '^[1-9][0-9]*$' AND day ~ '^\d{4}-\d{2}-\d{2}$'
  29. THEN student||':'||study||':'||day ELSE NULL END
  30. $$;
  31. -- Preserve duplicate legacy rows, reserve their identity, and flag all copies for review.
  32. WITH ranked AS (
  33. SELECT "objectId", xiaoshu_review_key("yhid"::text,"xxjlid"::text,left("kywrq"::text,10)) k,
  34. row_number() OVER (PARTITION BY "company","yhid","xxjlid",left("kywrq"::text,10) ORDER BY "objectId") rn,
  35. count(*) OVER (PARTITION BY "company","yhid","xxjlid",left("kywrq"::text,10)) copies
  36. FROM "MemoryPracticeRecord"
  37. )
  38. UPDATE "MemoryPracticeRecord" m SET "rewardTaskKey"=CASE WHEN r.rn=1 THEN r.k END,
  39. "rewardDuplicate"=(r.copies>1 AND r.k IS NOT NULL)
  40. FROM ranked r WHERE m."objectId"=r."objectId" AND m."rewardDuplicate" IS NULL;
  41. CREATE UNIQUE INDEX IF NOT EXISTS memory_review_plan_once ON "MemoryPracticeRecord" ("company","rewardTaskKey") WHERE "rewardTaskKey" IS NOT NULL;
  42. CREATE OR REPLACE FUNCTION xiaoshu_record_review_reward(task "MemoryPracticeRecord", historical boolean) RETURNS void
  43. LANGUAGE plpgsql AS $$
  44. DECLARE coach record; student record; store record; course text; problem text := ''; finished timestamptz; reward_status text;
  45. BEGIN
  46. IF task."company" IS NULL THEN RAISE EXCEPTION '复习任务缺少帐套'; END IF;
  47. IF EXISTS (SELECT 1 FROM "ReviewReward" WHERE "company"=task."company" AND "taskId"=task."objectId") THEN RETURN; END IF;
  48. SELECT * INTO coach FROM "_User" WHERE "company"=task."company" AND COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID')=task."plid"::text LIMIT 1;
  49. SELECT * INTO student FROM "_User" WHERE "company"=task."company" AND COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID')=task."yhid"::text LIMIT 1;
  50. SELECT * INTO store FROM "_User" WHERE "company"=task."company" AND COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID')=coach."legacyUserData"->>'ParentUserID' LIMIT 1;
  51. SELECT "nodeName" INTO course FROM "Node" WHERE "company"=task."company" AND "nodeId"::text=task."kcid"::text LIMIT 1;
  52. BEGIN
  53. finished := CASE WHEN historical THEN NULLIF(task."wcsj"::text,'')::timestamp AT TIME ZONE 'Asia/Shanghai' ELSE statement_timestamp() END;
  54. EXCEPTION WHEN OTHERS THEN finished := NULL; END;
  55. IF task."rewardDuplicate" THEN problem := '来源复习任务重复';
  56. ELSIF task."rewardTaskKey" IS NULL OR NOT EXISTS (SELECT 1 FROM "CommonModel" WHERE "company"=task."company" AND "modelId"::text='56' AND "generalId"::text=task."xxjlid"::text) THEN problem := '缺少有效来源学习记录或计划日期';
  57. ELSIF coach."objectId" IS NULL OR COALESCE(task."plid"::text,'0') !~ '^[1-9][0-9]*$' THEN problem := '缺少有效复习老师';
  58. ELSIF student."objectId" IS NULL THEN problem := '缺少有效学员';
  59. ELSIF finished IS NULL THEN problem := '缺少有效完成时间'; END IF;
  60. reward_status := CASE WHEN problem<>'' THEN 'exception' WHEN historical THEN 'historical' ELSE 'earned' END;
  61. INSERT INTO "ReviewReward" ("objectId","company","taskId","taskKey","coachId","coachName","studentId","studentName","courseName","storeId","storeName","plannedDate","completedAt","incomeMonth","amount","status","exception")
  62. VALUES (md5(task."company"||':review:'||task."objectId"),task."company",task."objectId",task."rewardTaskKey",
  63. CASE WHEN task."plid"::text ~ '^[1-9][0-9]*$' THEN task."plid"::double precision ELSE 0 END,
  64. COALESCE(NULLIF(coach."realName",''),NULLIF(coach."nickname",''),coach."legacyUserData"->>'HoneyName',coach."username",'未识别老师'),
  65. CASE WHEN task."yhid"::text ~ '^[1-9][0-9]*$' THEN task."yhid"::double precision ELSE 0 END,
  66. COALESCE(NULLIF(student."realName",''),NULLIF(student."nickname",''),student."legacyUserData"->>'HoneyName',student."username",'未识别学员'),COALESCE(course,''),
  67. CASE WHEN coach."legacyUserData"->>'ParentUserID' ~ '^[1-9][0-9]*$' THEN (coach."legacyUserData"->>'ParentUserID')::double precision ELSE 0 END,
  68. COALESCE(NULLIF(store."realName",''),NULLIF(store."nickname",''),store."legacyUserData"->>'HoneyName',store."username",'未绑定门店'),
  69. COALESCE(left(task."kywrq"::text,10),''),finished,COALESCE(to_char(finished AT TIME ZONE 'Asia/Shanghai','YYYY-MM'),''),CASE WHEN reward_status='earned' THEN 1 ELSE 0 END,reward_status,problem)
  70. ON CONFLICT ("company","taskId") DO NOTHING;
  71. END $$;
  72. -- Backfill creates zero-value, non-payable evidence; rerunning migration cannot award it.
  73. -- Use one set-based statement so the full history is scanned once instead of issuing
  74. -- several lookup queries for every completed task.
  75. CREATE OR REPLACE FUNCTION xiaoshu_safe_shanghai_timestamp(value text) RETURNS timestamptz
  76. LANGUAGE plpgsql IMMUTABLE AS $$
  77. BEGIN
  78. RETURN NULLIF(value,'')::timestamp AT TIME ZONE 'Asia/Shanghai';
  79. EXCEPTION WHEN OTHERS THEN RETURN NULL;
  80. END $$;
  81. WITH users AS MATERIALIZED (
  82. SELECT DISTINCT ON ("company",COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID'))
  83. "company",COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID') AS legacy_id,
  84. "objectId","realName","nickname","username","legacyUserData"
  85. FROM "_User"
  86. WHERE COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID') IS NOT NULL
  87. ORDER BY "company",COALESCE("legacyUserId"::text,"legacyUserData"->>'UserID'),"objectId"
  88. ), studies AS MATERIALIZED (
  89. SELECT DISTINCT "company","generalId"::text AS general_id
  90. FROM "CommonModel" WHERE "modelId"::text='56'
  91. ), courses AS MATERIALIZED (
  92. SELECT DISTINCT ON ("company","nodeId"::text) "company","nodeId"::text AS node_id,"nodeName"
  93. FROM "Node" ORDER BY "company","nodeId"::text,"nodeName"
  94. ), facts AS (
  95. SELECT task.*,xiaoshu_safe_shanghai_timestamp(task."wcsj"::text) AS finished,
  96. coach."objectId" AS coach_object_id,coach."realName" AS coach_real_name,coach."nickname" AS coach_nickname,
  97. coach."username" AS coach_username,coach."legacyUserData" AS coach_legacy,
  98. student."objectId" AS student_object_id,student."realName" AS student_real_name,student."nickname" AS student_nickname,
  99. student."username" AS student_username,student."legacyUserData" AS student_legacy,
  100. store."realName" AS store_real_name,store."nickname" AS store_nickname,store."username" AS store_username,
  101. store."legacyUserData" AS store_legacy,study.general_id AS study_general_id,course."nodeName" AS course_name
  102. FROM "MemoryPracticeRecord" task
  103. LEFT JOIN users coach ON coach."company"=task."company" AND coach.legacy_id=task."plid"::text
  104. LEFT JOIN users student ON student."company"=task."company" AND student.legacy_id=task."yhid"::text
  105. LEFT JOIN users store ON store."company"=task."company" AND store.legacy_id=coach."legacyUserData"->>'ParentUserID'
  106. LEFT JOIN studies study ON study."company"=task."company" AND study.general_id=task."xxjlid"::text
  107. LEFT JOIN courses course ON course."company"=task."company" AND course.node_id=task."kcid"::text
  108. WHERE task."fxzt"::text='1' AND task."company" IS NOT NULL
  109. ), prepared AS (
  110. SELECT facts.*,
  111. CASE WHEN "rewardDuplicate" THEN '来源复习任务重复'
  112. WHEN "rewardTaskKey" IS NULL OR study_general_id IS NULL THEN '缺少有效来源学习记录或计划日期'
  113. WHEN coach_object_id IS NULL OR COALESCE("plid"::text,'0') !~ '^[1-9][0-9]*$' THEN '缺少有效复习老师'
  114. WHEN student_object_id IS NULL THEN '缺少有效学员'
  115. WHEN finished IS NULL THEN '缺少有效完成时间' ELSE '' END AS problem
  116. FROM facts
  117. )
  118. INSERT INTO "ReviewReward" ("objectId","company","taskId","taskKey","coachId","coachName","studentId","studentName","courseName","storeId","storeName","plannedDate","completedAt","incomeMonth","amount","status","exception")
  119. SELECT md5("company"||':review:'||"objectId"),"company","objectId","rewardTaskKey",
  120. CASE WHEN "plid"::text ~ '^[1-9][0-9]*$' THEN "plid"::double precision ELSE 0 END,
  121. COALESCE(NULLIF(coach_real_name,''),NULLIF(coach_nickname,''),coach_legacy->>'HoneyName',coach_username,'未识别老师'),
  122. CASE WHEN "yhid"::text ~ '^[1-9][0-9]*$' THEN "yhid"::double precision ELSE 0 END,
  123. COALESCE(NULLIF(student_real_name,''),NULLIF(student_nickname,''),student_legacy->>'HoneyName',student_username,'未识别学员'),COALESCE(course_name,''),
  124. CASE WHEN coach_legacy->>'ParentUserID' ~ '^[1-9][0-9]*$' THEN (coach_legacy->>'ParentUserID')::double precision ELSE 0 END,
  125. COALESCE(NULLIF(store_real_name,''),NULLIF(store_nickname,''),store_legacy->>'HoneyName',store_username,'未绑定门店'),
  126. COALESCE(left("kywrq"::text,10),''),finished,COALESCE(to_char(finished AT TIME ZONE 'Asia/Shanghai','YYYY-MM'),''),0,
  127. CASE WHEN problem='' THEN 'historical' ELSE 'exception' END,problem
  128. FROM prepared
  129. ON CONFLICT ("company","taskId") DO NOTHING;
  130. CREATE OR REPLACE FUNCTION xiaoshu_review_before_write() RETURNS trigger LANGUAGE plpgsql AS $$
  131. BEGIN
  132. IF TG_OP='UPDATE' THEN
  133. IF OLD."company" IS DISTINCT FROM NEW."company" OR OLD."objectId" IS DISTINCT FROM NEW."objectId" THEN RAISE EXCEPTION '不能修改复习任务身份'; END IF;
  134. IF OLD."rewardTaskKey" IS NOT NULL OR OLD."rewardDuplicate" OR OLD."fxzt"::text='1' THEN
  135. IF ROW(OLD."yhid",OLD."xxjlid",OLD."kywrq") IS DISTINCT FROM ROW(NEW."yhid",NEW."xxjlid",NEW."kywrq") THEN RAISE EXCEPTION '不能修改复习任务来源'; END IF;
  136. NEW."rewardTaskKey" := OLD."rewardTaskKey"; NEW."rewardDuplicate" := OLD."rewardDuplicate";
  137. ELSE
  138. NEW."rewardTaskKey" := xiaoshu_review_key(NEW."yhid"::text,NEW."xxjlid"::text,left(NEW."kywrq"::text,10)); NEW."rewardDuplicate" := false;
  139. END IF;
  140. IF OLD."fxzt"::text='1' THEN
  141. NEW."fxzt" := OLD."fxzt"; NEW."wcsj" := OLD."wcsj"; NEW."plid" := OLD."plid";
  142. RETURN NEW;
  143. END IF;
  144. IF NEW."fxzt"::text='1' AND NOT ((COALESCE(NEW."sourceKey",'') LIKE 'legacy:model:60:%' OR COALESCE(NEW."sourceKey",'') LIKE 'legacy-sync:memory-records:addon:%') AND NULLIF(NEW."wcsj"::text,'') IS NOT NULL) THEN
  145. NEW."wcsj" := to_char(statement_timestamp() AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD HH24:MI:SS');
  146. END IF;
  147. ELSE
  148. NEW."rewardTaskKey" := xiaoshu_review_key(NEW."yhid"::text,NEW."xxjlid"::text,left(NEW."kywrq"::text,10)); NEW."rewardDuplicate" := false;
  149. END IF;
  150. RETURN NEW;
  151. END $$;
  152. CREATE OR REPLACE FUNCTION xiaoshu_review_after_write() RETURNS trigger LANGUAGE plpgsql AS $$
  153. BEGIN
  154. IF NEW."fxzt"::text='1' THEN
  155. IF TG_OP='INSERT' THEN PERFORM xiaoshu_record_review_reward(NEW,true);
  156. ELSE PERFORM xiaoshu_record_review_reward(NEW,OLD."fxzt"::text='1' OR COALESCE(NEW."sourceKey",'') LIKE 'legacy:model:60:%' OR COALESCE(NEW."sourceKey",'') LIKE 'legacy-sync:memory-records:addon:%'); END IF;
  157. END IF;
  158. RETURN NEW;
  159. END $$;
  160. DROP TRIGGER IF EXISTS review_reward_before_write ON "MemoryPracticeRecord";
  161. CREATE TRIGGER review_reward_before_write BEFORE INSERT OR UPDATE ON "MemoryPracticeRecord" FOR EACH ROW EXECUTE FUNCTION xiaoshu_review_before_write();
  162. DROP TRIGGER IF EXISTS review_reward_after_write ON "MemoryPracticeRecord";
  163. CREATE TRIGGER review_reward_after_write AFTER INSERT OR UPDATE ON "MemoryPracticeRecord" FOR EACH ROW EXECUTE FUNCTION xiaoshu_review_after_write();
  164. -- The compatibility endpoint calls this, so an unapplied migration fails closed.
  165. CREATE OR REPLACE FUNCTION xiaoshu_complete_review(tenant text, task_id text) RETURNS void LANGUAGE plpgsql AS $$
  166. BEGIN
  167. UPDATE "MemoryPracticeRecord" SET "fxzt"='1',"updatedAt"=statement_timestamp() WHERE "company"=tenant AND "objectId"=task_id;
  168. IF NOT FOUND THEN RAISE EXCEPTION '复习任务不存在'; END IF;
  169. END $$;
  170. CREATE OR REPLACE FUNCTION xiaoshu_reward_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
  171. BEGIN
  172. IF TG_OP='UPDATE' AND current_setting('xiaoshu.review_reward_confirm',true)='yes' AND OLD."status"='historical' AND NEW."status"='earned' AND NEW."amount"=1 AND (to_jsonb(NEW)-'status'-'amount'-'updatedAt')=(to_jsonb(OLD)-'status'-'amount'-'updatedAt') THEN RETURN NEW; END IF;
  173. RAISE EXCEPTION '抗遗忘工资凭据不可改写或删除,请使用工资调整';
  174. END $$;
  175. DROP TRIGGER IF EXISTS review_reward_immutable ON "ReviewReward";
  176. CREATE TRIGGER review_reward_immutable BEFORE UPDATE OR DELETE ON "ReviewReward" FOR EACH ROW EXECUTE FUNCTION xiaoshu_reward_immutable();
  177. CREATE OR REPLACE FUNCTION xiaoshu_payroll_reward_guard() RETURNS trigger LANGUAGE plpgsql AS $$
  178. DECLARE batch record; reward record; tenant text; batch_id text;
  179. BEGIN
  180. tenant := CASE WHEN TG_OP='DELETE' THEN OLD."company" ELSE NEW."company" END;
  181. batch_id := CASE WHEN TG_OP='DELETE' THEN OLD."batchId" ELSE NEW."batchId" END;
  182. IF TG_OP='UPDATE' AND ROW(OLD."company",OLD."batchId",OLD."kind",OLD."reviewRewardId") IS DISTINCT FROM ROW(NEW."company",NEW."batchId",NEW."kind",NEW."reviewRewardId") THEN RAISE EXCEPTION '不能修改工资明细归属'; END IF;
  183. SELECT * INTO batch FROM "PayrollBatch" WHERE "company"=tenant AND "objectId"=batch_id FOR UPDATE;
  184. IF batch."objectId" IS NULL OR batch."status"<>'draft' THEN RAISE EXCEPTION '工资单已锁定或不存在'; END IF;
  185. IF TG_OP='DELETE' THEN RETURN OLD; END IF;
  186. IF NEW."kind"='review_reward' THEN
  187. SELECT * INTO reward FROM "ReviewReward" WHERE "company"=tenant AND "objectId"=NEW."reviewRewardId";
  188. IF reward."status" IS DISTINCT FROM 'earned' OR reward."incomeMonth">batch."month" THEN RAISE EXCEPTION '抗遗忘工资不可结算'; END IF;
  189. IF NEW."excluded" THEN RAISE EXCEPTION '有效抗遗忘工资不能排除,请使用工资调整'; END IF;
  190. NEW."coachId" := reward."coachId"; NEW."coachName" := reward."coachName";
  191. NEW."studentName" := reward."studentName"; NEW."courseName" := reward."courseName";
  192. NEW."storeId" := reward."storeId"; NEW."storeName" := reward."storeName";
  193. NEW."reviewTaskId" := reward."taskId"; NEW."plannedDate" := reward."plannedDate";
  194. NEW."amount" := reward."amount"; NEW."rateAmount" := reward."amount"; NEW."durationHours" := 0;
  195. NEW."originalMonth" := reward."incomeMonth"; NEW."settlementMonth" := batch."month"; NEW."lessonAt" := reward."completedAt";
  196. NEW."exception" := ''; NEW."classType" := 0; NEW."courseCategoryName" := '抗遗忘工资';
  197. END IF;
  198. RETURN NEW;
  199. END $$;
  200. DROP TRIGGER IF EXISTS payroll_reward_guard ON "PayrollLine";
  201. CREATE TRIGGER payroll_reward_guard BEFORE INSERT OR UPDATE OR DELETE ON "PayrollLine" FOR EACH ROW EXECUTE FUNCTION xiaoshu_payroll_reward_guard();
  202. CREATE OR REPLACE FUNCTION xiaoshu_payroll_totals_guard() RETURNS trigger LANGUAGE plpgsql AS $$
  203. BEGIN
  204. IF OLD."status" IN ('reviewed','paid') THEN
  205. IF NEW."status" IS DISTINCT FROM OLD."status" AND NOT (OLD."status"='reviewed' AND NEW."status"='paid') THEN RAISE EXCEPTION '已审核工资不可重新打开'; END IF;
  206. IF ROW(NEW."baseAmount",NEW."reviewAmount",NEW."adjustmentAmount",NEW."payableAmount",NEW."month") IS DISTINCT FROM ROW(OLD."baseAmount",OLD."reviewAmount",OLD."adjustmentAmount",OLD."payableAmount",OLD."month") THEN RAISE EXCEPTION '已审核工资金额不可修改'; END IF;
  207. RETURN NEW;
  208. END IF;
  209. SELECT COALESCE(sum("amount") FILTER (WHERE "kind"='lesson'),0),COALESCE(sum("amount") FILTER (WHERE "kind"='review_reward'),0),COALESCE(sum("amount") FILTER (WHERE "kind"='adjustment'),0),count(*) FILTER (WHERE "kind"='review_reward'),count(*) FILTER (WHERE "kind"='lesson'),COALESCE(sum("durationHours") FILTER (WHERE "kind"='lesson'),0)
  210. INTO NEW."baseAmount",NEW."reviewAmount",NEW."adjustmentAmount",NEW."reviewCount",NEW."lessonCount",NEW."totalHours"
  211. FROM "PayrollLine" WHERE "company"=NEW."company" AND "batchId"=NEW."objectId" AND COALESCE("excluded",false)=false AND COALESCE("exception",'')='';
  212. NEW."payableAmount" := NEW."baseAmount"+NEW."reviewAmount"+NEW."adjustmentAmount";
  213. RETURN NEW;
  214. END $$;
  215. DROP TRIGGER IF EXISTS payroll_totals_guard ON "PayrollBatch";
  216. CREATE TRIGGER payroll_totals_guard BEFORE UPDATE ON "PayrollBatch" FOR EACH ROW EXECUTE FUNCTION xiaoshu_payroll_totals_guard();
  217. CREATE OR REPLACE FUNCTION xiaoshu_payroll_delete_guard() RETURNS trigger LANGUAGE plpgsql AS $$
  218. BEGIN
  219. IF OLD."status" IN ('reviewed','paid') OR EXISTS (SELECT 1 FROM "PayrollLine" WHERE "company"=OLD."company" AND "batchId"=OLD."objectId") THEN RAISE EXCEPTION '工资批次已锁定或仍有明细,不能删除'; END IF;
  220. RETURN OLD;
  221. END $$;
  222. DROP TRIGGER IF EXISTS payroll_delete_guard ON "PayrollBatch";
  223. CREATE TRIGGER payroll_delete_guard BEFORE DELETE ON "PayrollBatch" FOR EACH ROW EXECUTE FUNCTION xiaoshu_payroll_delete_guard();
  224. COMMIT;