|
|
@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS "LegacyReviewSyncState" (
|
|
|
"lastError" text NOT NULL DEFAULT '',"updatedAt" timestamptz NOT NULL DEFAULT now(),
|
|
|
PRIMARY KEY(company,"taskId")
|
|
|
);
|
|
|
+ALTER TABLE "LegacyReviewSyncState" ADD COLUMN IF NOT EXISTS "legacyFeeProof" jsonb;
|
|
|
CREATE INDEX IF NOT EXISTS legacy_review_income_pending ON "LegacyReviewSyncState"(company,"taskId") WHERE "pendingIncome";
|
|
|
CREATE TABLE IF NOT EXISTS "LegacyReviewSyncAudit" (
|
|
|
"objectId" text PRIMARY KEY,company text NOT NULL,"taskId" text NOT NULL,
|
|
|
@@ -34,7 +35,7 @@ BEGIN
|
|
|
SELECT EXISTS(SELECT 1 FROM "ReviewRewardIncome" r WHERE r.company=co AND r."taskId"=task_id AND r."incomeMonth">='2026-09' AND r.status IN ('historical','exception')) INTO pending;
|
|
|
INSERT INTO "LegacyReviewSyncState"(company,"taskId","assignmentVersion",fingerprint,"pendingIncome")
|
|
|
VALUES(co,task_id,COALESCE((m->>'assignmentVersion')::numeric,0),md5(m::text),pending)
|
|
|
- ON CONFLICT(company,"taskId") DO UPDATE SET "assignmentVersion"=EXCLUDED."assignmentVersion",fingerprint=EXCLUDED.fingerprint,"pendingIncome"=EXCLUDED."pendingIncome","lastError"='',"updatedAt"=now();
|
|
|
+ ON CONFLICT(company,"taskId") DO UPDATE SET "assignmentVersion"=EXCLUDED."assignmentVersion",fingerprint=EXCLUDED.fingerprint,"pendingIncome"=EXCLUDED."pendingIncome","legacyFeeProof"=NULL,"lastError"='',"updatedAt"=now();
|
|
|
INSERT INTO "LegacyDirectBaseline"(company,model,gid,fingerprint) VALUES(co,60,(c->>'generalId')::numeric,md5(jsonb_build_array(c,m)::text))
|
|
|
ON CONFLICT(company,model,gid) DO UPDATE SET fingerprint=EXCLUDED.fingerprint,"updatedAt"=now();
|
|
|
END $$;
|
|
|
@@ -45,12 +46,17 @@ CREATE OR REPLACE FUNCTION xs_legacy_review_observe(co text, source_rows jsonb)
|
|
|
DECLARE row_data record; n integer:=0;
|
|
|
BEGIN
|
|
|
IF jsonb_typeof(source_rows)<>'array' OR jsonb_array_length(source_rows)>1000 THEN RAISE EXCEPTION '无效旧复习来源页'; END IF;
|
|
|
- FOR row_data IN SELECT m."objectId",m.fxzt,md5(to_jsonb(m)::text) AS fingerprint FROM jsonb_array_elements(source_rows) s
|
|
|
+ FOR row_data IN SELECT m."objectId",m.fxzt,COALESCE(m."assignmentVersion",0) AS version,md5(to_jsonb(m)::text) AS fingerprint,
|
|
|
+ CASE WHEN jsonb_typeof(s->'reviewTrialProof')='object' THEN jsonb_build_object('trial',s->'reviewTrialProof')
|
|
|
+ WHEN s->>'reviewFeeVerified'='true' THEN '{"positive":true}'::jsonb END AS proof
|
|
|
+ FROM jsonb_array_elements(source_rows) s
|
|
|
JOIN "CommonModel" c ON c.company=co AND c."modelId"=60 AND c."generalId"=(s->>'GeneralID')::numeric
|
|
|
JOIN "MemoryPracticeRecord" m ON m.company=c.company AND m.id=c."itemId"
|
|
|
LEFT JOIN "LegacyDirectBaseline" b ON b.company=c.company AND b.model=60 AND b.gid=c."generalId"
|
|
|
LEFT JOIN "LegacyReviewSyncState" st ON st.company=m.company AND st."taskId"=m."objectId"
|
|
|
- WHERE COALESCE(m."assignmentVersion",0)=0 AND m."sourceKey" LIKE 'legacy:model:60:general:%'
|
|
|
+ WHERE (COALESCE(m."assignmentVersion",0)=0 OR
|
|
|
+ (m.fxzt='1' AND st."pendingIncome" AND st."assignmentVersion"=m."assignmentVersion" AND st.fingerprint=md5(to_jsonb(m)::text)))
|
|
|
+ AND m."sourceKey" LIKE 'legacy:model:60:general:%'
|
|
|
AND NOT COALESCE(c."teachingManaged",false) AND COALESCE(c.status,99)<>-2
|
|
|
AND (b.fingerprint IS NULL OR b.fingerprint=md5(jsonb_build_array(to_jsonb(c),to_jsonb(m))::text)
|
|
|
OR (m."assignmentVersion" IS NULL AND b.fingerprint=md5(jsonb_build_array(to_jsonb(c),to_jsonb(m)-'assignmentVersion')::text)))
|
|
|
@@ -59,13 +65,14 @@ BEGIN
|
|
|
AND ((m.fxzt='0' AND st."taskId" IS NULL AND NOT EXISTS(
|
|
|
SELECT 1 FROM "ReviewReward" r WHERE r.company=m.company AND r."taskId"=m."objectId"))
|
|
|
OR (m.fxzt='1' AND COALESCE(c.status,99)=99
|
|
|
- AND s->>'reviewFeeVerified'='true'
|
|
|
+ AND (s->>'reviewFeeVerified'='true' OR jsonb_typeof(s->'reviewTrialProof')='object')
|
|
|
AND m."sourceKey"='legacy:model:60:general:'||c."generalId"::text AND c."sourceKey"=m."sourceKey"
|
|
|
- AND (st."taskId" IS NULL OR (NOT st."pendingIncome" AND st."assignmentVersion"=0 AND st.fingerprint=md5(to_jsonb(m)::text)))
|
|
|
+ AND (st."taskId" IS NULL OR (st."assignmentVersion"=COALESCE(m."assignmentVersion",0) AND st.fingerprint=md5(to_jsonb(m)::text)))
|
|
|
AND EXISTS(SELECT 1 FROM "ReviewReward" r
|
|
|
JOIN "ReviewRewardIncome" v ON v.company=r.company AND v."objectId"=r."objectId"
|
|
|
WHERE r.company=m.company AND r."taskId"=m."objectId" AND r.amount=0
|
|
|
- AND v.status IN ('historical','exception') AND v.amount=0 AND v."incomeMonth">='2026-09'
|
|
|
+ AND v.status IN ('historical','exception') AND v.amount=0
|
|
|
+ AND (v."incomeMonth">='2026-09' OR jsonb_typeof(s->'reviewTrialProof')='object')
|
|
|
AND r."completionRole"='historical_unknown' AND NULLIF(r."completedBy",'') IS NULL
|
|
|
AND r."studentId"::text=m.yhid::text AND r."coachId"::text=m.plid::text AND r."plannedDate"=LEFT(m.kywrq,10)
|
|
|
AND r."completedAt"=xiaoshu_safe_shanghai_timestamp(m.wcsj) AND r."completedAt"<=now()
|
|
|
@@ -76,10 +83,11 @@ BEGIN
|
|
|
PERFORM xs_legacy_review_track(co,row_data."objectId");
|
|
|
n:=n+1;
|
|
|
ELSE
|
|
|
- INSERT INTO "LegacyReviewSyncState"(company,"taskId","assignmentVersion",fingerprint,"pendingIncome")
|
|
|
- VALUES(co,row_data."objectId",0,row_data.fingerprint,true)
|
|
|
- ON CONFLICT(company,"taskId") DO UPDATE SET "pendingIncome"=true,"updatedAt"=now()
|
|
|
- WHERE NOT "LegacyReviewSyncState"."pendingIncome" AND "LegacyReviewSyncState"."assignmentVersion"=0
|
|
|
+ INSERT INTO "LegacyReviewSyncState"(company,"taskId","assignmentVersion",fingerprint,"pendingIncome","legacyFeeProof")
|
|
|
+ VALUES(co,row_data."objectId",row_data.version,row_data.fingerprint,true,row_data.proof)
|
|
|
+ ON CONFLICT(company,"taskId") DO UPDATE SET "pendingIncome"=true,"legacyFeeProof"=EXCLUDED."legacyFeeProof","updatedAt"=now()
|
|
|
+ WHERE (NOT "LegacyReviewSyncState"."pendingIncome" OR "LegacyReviewSyncState"."legacyFeeProof" IS NULL)
|
|
|
+ AND "LegacyReviewSyncState"."assignmentVersion"=EXCLUDED."assignmentVersion"
|
|
|
AND "LegacyReviewSyncState".fingerprint=EXCLUDED.fingerprint;
|
|
|
IF FOUND THEN n:=n+1;END IF;
|
|
|
END IF;
|
|
|
@@ -122,6 +130,18 @@ BEGIN
|
|
|
EXCEPTION WHEN OTHERS THEN PERFORM set_config('xiaoshu.review_transfer',prior,true);RAISE;
|
|
|
END $$;
|
|
|
|
|
|
+-- These projected course types require a verified old fee before newly completed
|
|
|
+-- tasks can enter the existing positive-payment path.
|
|
|
+CREATE OR REPLACE FUNCTION xs_legacy_review_needs_fee(co text, task_id text) RETURNS boolean LANGUAGE sql STABLE AS $$
|
|
|
+ SELECT EXISTS(
|
|
|
+ SELECT 1 FROM xs_review_task_link link
|
|
|
+ JOIN "CommonModel" c ON c.company=link.company AND c."modelId"=54 AND c."generalId"::text=link."appointmentId"
|
|
|
+ JOIN "CourseAppointment" a ON a.company=c.company AND a.id=c."itemId" AND a.szyh::text=link."studentId"
|
|
|
+ WHERE link.company=co AND link."taskId"=task_id AND a.dslx::text='3'
|
|
|
+ )
|
|
|
+$$;
|
|
|
+REVOKE ALL ON FUNCTION xs_legacy_review_needs_fee(text,text) FROM PUBLIC;
|
|
|
+
|
|
|
CREATE OR REPLACE FUNCTION xs_legacy_review_income(co text, actor text, batch_limit integer DEFAULT 100, after_task text DEFAULT '') RETURNS jsonb LANGUAGE plpgsql AS $$
|
|
|
DECLARE item record; m "MemoryPracticeRecord"; r "ReviewReward"; best record; expired record; outcome text; err text; n integer:=0; earned integer:=0; excluded integer:=0; amount numeric:=0; pending integer; errors jsonb:='[]'; cursor_id text:=after_task; more boolean; saved_n integer; saved_earned integer; saved_excluded integer; saved_amount numeric;
|
|
|
BEGIN
|
|
|
@@ -136,7 +156,16 @@ BEGIN
|
|
|
SELECT * INTO r FROM "ReviewReward" WHERE company=co AND "taskId"=item."taskId" FOR UPDATE;
|
|
|
IF m."objectId" IS NULL OR r."objectId" IS NULL OR item.fingerprint<>md5(to_jsonb(m)::text) OR COALESCE(m."assignmentVersion",0)<>item."assignmentVersion" THEN RAISE EXCEPTION '来源同步后任务已改变'; END IF;
|
|
|
IF EXISTS(SELECT 1 FROM "ReviewRewardIncome" WHERE company=co AND "objectId"=r."objectId" AND status IN ('earned','ineligible')) THEN
|
|
|
- UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=item."taskId";n:=n+1;CONTINUE;
|
|
|
+ UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"legacyFeeProof"=NULL,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=item."taskId";n:=n+1;CONTINUE;
|
|
|
+ END IF;
|
|
|
+ IF item."legacyFeeProof" ? 'trial' THEN
|
|
|
+ PERFORM xs_review_resolve(co,actor,jsonb_build_array(jsonb_build_object('rewardId',r."objectId",'reasonCode','legacy_trial_no_fee','legacyTrialProof',item."legacyFeeProof"->'trial')),
|
|
|
+ '旧系统体验课复习不计费。原预约、学习记录和旧费用明细已自动核实,旧系统本条费用为 ¥0。',true);
|
|
|
+ UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"legacyFeeProof"=NULL,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=item."taskId";
|
|
|
+ excluded:=excluded+1;n:=n+1;CONTINUE;
|
|
|
+ END IF;
|
|
|
+ IF item."legacyFeeProof"->>'positive' IS DISTINCT FROM 'true' AND xs_legacy_review_needs_fee(co,item."taskId") THEN
|
|
|
+ RAISE EXCEPTION '原课次为旧系统体验课类型,仍需核实旧费用明细';
|
|
|
END IF;
|
|
|
IF r."incomeMonth"<'2026-09' OR r."completedAt">now() OR r."completionRole" IS DISTINCT FROM 'historical_unknown' OR NULLIF(r."completedBy",'') IS NOT NULL THEN RAISE EXCEPTION '不符合已确认的历史核定范围'; END IF;
|
|
|
IF EXISTS(SELECT 1 FROM "_User" u WHERE u.company=co AND u."legacyUserId"=r."coachId" AND concat_ws('',u."realName",u.nickname,u.username) LIKE '%自习%') THEN RAISE EXCEPTION '自习账号需单独核对,不能自动计薪'; END IF;
|
|
|
@@ -152,7 +181,7 @@ BEGIN
|
|
|
AND t."sourceKey" LIKE 'legacy:model:60:general:%'
|
|
|
LOOP
|
|
|
PERFORM xs_review_resolve(co,actor,jsonb_build_array(jsonb_build_object('rewardId',expired."objectId",'reasonCode','overdue')),'旧来源重复复习核对:先排除已核实逾期副本,保留计划当天完成副本的计薪资格。',true);
|
|
|
- UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=expired."taskId";
|
|
|
+ UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"legacyFeeProof"=NULL,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=expired."taskId";
|
|
|
excluded:=excluded+1;n:=n+1;
|
|
|
END LOOP;
|
|
|
END IF;
|
|
|
@@ -172,7 +201,7 @@ BEGIN
|
|
|
'旧系统来源自动核对:按已确认历史绑定老师、计划当天完成和来源课时时长口径;保留实际完成身份未知,排除逾期和重复。',true);
|
|
|
SELECT * INTO best FROM "ReviewRewardIncome" WHERE company=co AND "objectId"=r."objectId";
|
|
|
IF best.status='earned' THEN earned:=earned+1;amount:=amount+best.amount;ELSE excluded:=excluded+1;END IF;
|
|
|
- UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=item."taskId";
|
|
|
+ UPDATE "LegacyReviewSyncState" SET "pendingIncome"=false,"legacyFeeProof"=NULL,"lastError"='',"updatedAt"=now() WHERE company=co AND "taskId"=item."taskId";
|
|
|
n:=n+1;
|
|
|
EXCEPTION WHEN OTHERS THEN
|
|
|
GET STACKED DIAGNOSTICS err=MESSAGE_TEXT;
|