| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- -- One fresh read snapshot; no result cache. Deploy after payroll-source-index and cutoff dependencies.
- -- Call inside REPEATABLE READ READ ONLY because downstream legacy read helpers may be VOLATILE.
- -- Capture the exact date parser used for conservative transport pruning. A later
- -- parser change fails closed rather than assuming it still agrees with JavaScript.
- DO $install$
- DECLARE expected text:=md5(pg_get_functiondef('xs_payroll_time(text)'::regprocedure));
- conflicts_expected text:=md5(pg_get_functiondef('xs_payroll_cutoff_conflicts(text,text)'::regprocedure));
- BEGIN
- IF to_regprocedure('xs_payroll_read_snapshot_authority_ready()') IS NOT NULL THEN RETURN; END IF;
- EXECUTE format('CREATE OR REPLACE FUNCTION xs_payroll_read_snapshot_authority_ready() RETURNS boolean LANGUAGE sql STABLE AS %L',
- 'SELECT xs_payroll_authority_ready() AND md5(pg_get_functiondef(''xs_payroll_time(text)''::regprocedure))='||quote_literal(expected)||
- ' AND md5(pg_get_functiondef(''xs_payroll_cutoff_conflicts(text,text)''::regprocedure))='||quote_literal(conflicts_expected));
- END $install$;
- CREATE OR REPLACE FUNCTION public.xs_payroll_cutoff_from_version(co text, mon text, provided_version text)
- RETURNS jsonb
- LANGUAGE plpgsql STABLE
- SET jit TO 'off'
- SET work_mem TO '64MB'
- SET max_parallel_workers_per_gather TO '2'
- AS $function$
- DECLARE run "LegacyDirectRun"; ev record; table_name text; matches integer; complete boolean:=true; conflicts jsonb; version text; approval "PayrollCutoffApproval"; data_version text; source_version text; pending integer; datasets jsonb:='[]'; ready boolean;
- BEGIN
- IF NOT xs_payroll_read_snapshot_authority_ready() THEN RETURN NULL;END IF;
- IF mon !~ '^\d{4}-(0[1-9]|1[0-2])$' THEN RAISE EXCEPTION '工资月份无效'; END IF;
- SELECT * INTO run FROM "LegacyDirectRun" WHERE company=co AND status IN ('completed','completed-with-preserved-conflicts') ORDER BY "finishedAt" DESC LIMIT 1;
- IF NOT FOUND THEN complete:=false; END IF;
- IF (SELECT count(*) FROM "LegacyDirectEvidence" WHERE company=co AND "runId"=run."runId" AND model IN (54,59,60))<>3 THEN complete:=false; END IF;
- FOR ev IN SELECT * FROM "LegacyDirectEvidence" WHERE company=co AND "runId"=run."runId" AND model IN (54,59,60) LOOP
- table_name:=CASE ev.model WHEN 54 THEN 'CourseAppointment' WHEN 59 THEN 'LessonRecord' ELSE 'MemoryPracticeRecord' END;
- matches:=xs_payroll_evidence_matches(co,ev.model,ev.ids);
- complete:=complete AND matches=ev."sourceCount" AND ev."sourceChecksum"=ev."targetChecksum" AND jsonb_array_length(ev.ids)=ev."sourceCount" AND ev."sourceChecksum"=md5((SELECT COALESCE(string_agg(v,E'\n' ORDER BY v::numeric),'') FROM jsonb_array_elements_text(ev.ids) v));
- datasets:=datasets||jsonb_build_array(jsonb_build_object('model',ev.model,'source',ev."sourceCount",'matched',matches,'checksum',ev."sourceChecksum"));
- END LOOP;
- SELECT COALESCE(jsonb_agg(c ORDER BY c->>'id'),'[]') INTO conflicts FROM xs_payroll_cutoff_conflicts_bulk(co,mon) c;
- SELECT count(*) INTO pending FROM jsonb_array_elements(conflicts) c WHERE NOT (c->>'resolved')::boolean;
- -- Native row serialization covers every column without constructing a JSON object per row.
- data_version:=xs_payroll_data_version(co);
- source_version:=provided_version;
- version:=md5(jsonb_build_array(data_version,source_version,run."runId",run."finishedAt",datasets,conflicts)::text);
- SELECT * INTO approval FROM "PayrollCutoffApproval" WHERE company=co AND month=mon;
- ready:=complete AND pending=0 AND approval.version=version AND NOT EXISTS(SELECT 1 FROM "LegacyDirectRun" WHERE company=co AND status='running');
- RETURN jsonb_build_object('month',mon,'version',version,'sourceVersion',source_version,'runId',run."runId",'cutoffAt',run."finishedAt",'complete',complete,'pending',pending,'conflicts',conflicts,'datasets',datasets,'approved',COALESCE(ready,false),'monthClosed',COALESCE(run."finishedAt">=((mon||'-01')::date+interval '1 month') AT TIME ZONE 'Asia/Shanghai',false),'approval',to_jsonb(approval));
- END $function$;
- CREATE OR REPLACE FUNCTION xs_payroll_read_snapshot(co text, mon text, for_dashboard boolean DEFAULT false, include_scope_rows boolean DEFAULT false)
- RETURNS jsonb LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
- DECLARE result jsonb; cutoff jsonb;
- BEGIN
- IF NOT xs_payroll_read_snapshot_authority_ready() THEN RETURN NULL; END IF;
- IF mon !~ '^\d{4}-(0[1-9]|1[0-2])$' THEN RAISE EXCEPTION '工资月份无效'; END IF;
- WITH sources AS MATERIALIZED (
- SELECT row_data AS s,row_text,standard_key,scope_data,ordinal AS ord FROM xs_payroll_source_entries_scoped(co,mon,NULL,NULL,NOT for_dashboard)
- ), identity_state AS MATERIALIZED (
- SELECT EXISTS(SELECT 1 FROM sources GROUP BY standard_key HAVING count(*)>1) AS duplicates
- ), source_version AS MATERIALIZED (
- SELECT CASE WHEN for_dashboard THEN '' ELSE md5(
- COALESCE((SELECT string_agg(row_text,'|' ORDER BY row_text) FROM sources),'')||
- COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "TeachingCompensationRule" r WHERE r."company"=co),'')||
- COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CourseTeachingProfile" p WHERE p."company"=co),'')||
- COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CoachPayRate" p WHERE p."company"=co),'')||
- COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "ReviewRewardIncome" r WHERE r."company"=co AND r."status"='earned'),'')) END AS value
- ), candidates AS MATERIALIZED (
- SELECT s,ord FROM sources WHERE s IS NOT NULL
- ) SELECT jsonb_build_object(
- 'rows',COALESCE((SELECT jsonb_agg(s ORDER BY ord) FROM candidates),'[]'::jsonb),
- 'version',(SELECT value FROM source_version),
- 'total',(SELECT count(*) FROM sources),
- 'hasDuplicates',(SELECT duplicates FROM identity_state),
- 'scopeRows',CASE WHEN include_scope_rows THEN COALESCE((SELECT jsonb_agg(scope_data ORDER BY ord) FROM sources),'[]'::jsonb) ELSE NULL END
- ) INTO result;
- IF NOT for_dashboard THEN
- cutoff:=xs_payroll_cutoff_from_version(co,mon,result->>'version');
- IF cutoff IS NULL THEN RETURN NULL; END IF;
- -- Full conflicts/approval already participated in the cutoff hash; only sync metadata crosses the wire.
- cutoff:=jsonb_build_object('approved',cutoff->'approved','monthClosed',cutoff->'monthClosed','version',cutoff->'version',
- 'complete',cutoff->'complete','pending',cutoff->'pending','datasets',cutoff->'datasets',
- 'cutoffAt',cutoff->'cutoffAt','sourceVersion',cutoff->'sourceVersion');
- END IF;
- RETURN result||jsonb_build_object('cutoff',cutoff);
- END $$;
- -- Select only identity/display fields before JSON encoding. The catalog check
- -- preserves every supported alias even on schemas that omit optional columns.
- CREATE OR REPLACE FUNCTION xs_payroll_read_users(co text) RETURNS SETOF jsonb
- LANGUAGE plpgsql STABLE AS $$
- DECLARE columns text;
- BEGIN
- SELECT string_agg(quote_ident(attname),',' ORDER BY attnum) INTO columns FROM pg_attribute
- WHERE attrelid='"_User"'::regclass AND attnum>0 AND NOT attisdropped
- AND attname=ANY(ARRAY['objectId','legacyUserId','legacyUserData','legacyuserdata','realName','realname','nickname','nickName','nickname2','username','mobile','phone']);
- RETURN QUERY EXECUTE 'SELECT to_jsonb(u) FROM (SELECT '||columns||' FROM "_User" WHERE company=$1) u' USING co;
- END $$;
|