@taopao/memoir 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -2
- package/assets/migrations-pg/0100_baseline.sql +1115 -0
- package/assets/migrations-pg/0101_functions.sql +93 -0
- package/assets/migrations-pg/0102_views.sql +2425 -0
- package/assets/migrations-pg/0103_constraints.sql +147 -0
- package/assets/migrations-pg/0104_protection.sql +33 -0
- package/assets/migrations-pg/0105_bocom_credit_card.sql +43 -0
- package/assets/migrations-pg/0106_psbc_debit_card.sql +87 -0
- package/assets/migrations-pg/0107_tailong_debit_card.sql +87 -0
- package/assets/migrations-pg/0108_ccb_debit_card.sql +87 -0
- package/assets/migrations-pg/0109_ccb_credit_card.sql +63 -0
- package/assets/migrations-pg/0110_icbc_debit_card.sql +87 -0
- package/assets/migrations-pg/0111_icbc_credit_card.sql +66 -0
- package/assets/migrations-pg/0112_boc_debit_card.sql +88 -0
- package/assets/migrations-pg/0113_jsb_debit_card.sql +88 -0
- package/assets/migrations-pg/0114_cmbc_debit_card.sql +88 -0
- package/assets/migrations-pg/0115_hzbank_debit_card.sql +88 -0
- package/assets/migrations-pg/0116_unified_bank_documents.sql +197 -0
- package/assets/migrations-pg/0117_agent_submissions.sql +50 -0
- package/assets/migrations-pg/0118_agent_reversions_analysis.sql +34 -0
- package/assets/migrations-pg/0119_agent_extension_registry.sql +72 -0
- package/assets/migrations-pg/0120_agent_record_sources.sql +22 -0
- package/assets/migrations-pg/0121_agent_extension_deprecation.sql +30 -0
- package/assets/migrations-pg/0122_agent_extension_controlled_revert.sql +107 -0
- package/assets/migrations-pg/0123_agent_extension_versions.sql +54 -0
- package/assets/migrations-pg/0124_agent_extension_retirement_groups.sql +26 -0
- package/assets/migrations-pg/0125_agent_candidate_reviews.sql +30 -0
- package/assets/migrations-pg/0126_agent_module_lifecycle.sql +93 -0
- package/assets/migrations-pg/0127_agent_tasks.sql +64 -0
- package/assets/migrations-pg/0128_agent_task_leases.sql +80 -0
- package/assets/migrations-pg/0129_agent_task_plans.sql +66 -0
- package/assets/migrations-pg/0130_agent_task_submissions.sql +72 -0
- package/assets/migrations-pg/0131_agent_task_completions.sql +93 -0
- package/assets/migrations-pg/0132_agent_task_changes.sql +113 -0
- package/assets/migrations-pg/0133_agent_imported_source_snapshots.sql +152 -0
- package/assets/migrations-pg/0134_agent_analysis_datasets.sql +6 -0
- package/assets/migrations-pg/0135_agent_maintenance_tasks.sql +139 -0
- package/assets/migrations-pg/0136_agent_task_structures.sql +66 -0
- package/assets/migrations-pg/0137_agent_personal_identity.sql +149 -0
- package/assets/migrations-pg/0138_agent_work_records.sql +24 -0
- package/assets/migrations-pg/0139_shared_files.sql +43 -0
- package/assets/migrations-pg/0140_shared_script_runs.sql +36 -0
- package/assets/migrations-pg/0141_payment_bank_links.sql +96 -0
- package/assets/migrations-pg/0142_consumption_order_types_and_places.sql +287 -0
- package/assets/migrations-pg/0143_skill_direct_write.sql +330 -0
- package/assets/migrations-pg/0144_skill_schema_archive.sql +73 -0
- package/assets/migrations-pg/0145_skill_ingest_wiki.sql +91 -0
- package/assets/migrations-pg/0146_skill_load.sql +17 -0
- package/assets/migrations-pg/0147_drop_agent_schema.sql +147 -0
- package/assets/migrations-pg/0148_ingest_lanes.sql +19 -0
- package/assets/migrations-pg/0149_account_ownership.sql +233 -0
- package/assets/migrations-pg/0150_bank_card_exact_link.sql +24 -0
- package/assets/migrations-pg/0151_account_ownership_enforce.sql +35 -0
- package/assets/migrations-pg/0152_account_ownership_views.sql +750 -0
- package/assets/migrations-pg/0153_web_frontend.sql +34 -0
- package/assets/migrations-pg/0154_audio_record_owners.sql +103 -0
- package/assets/migrations-pg/0155_ingest_knowledge_tables.sql +171 -0
- package/assets/migrations-pg/0156_batch_playbook_note.sql +17 -0
- package/assets/migrations-pg/0157_last_contact.sql +166 -0
- package/assets/migrations-pg/0158_refund_match_hash_join.sql +91 -0
- package/assets/migrations-pg/0159_fact_occurred_time.sql +88 -0
- package/assets/migrations-pg/0160_agent_notes.sql +118 -0
- package/assets/migrations-pg/0161_life_history.sql +314 -0
- package/assets/migrations-pg/0162_person_card.sql +427 -0
- package/assets/migrations-pg/0163_parking_drop_personal_defaults.sql +12 -0
- package/assets/native/darwin-arm64.node +0 -0
- package/assets/playbooks/audio.md +83 -0
- package/assets/playbooks/chat.md +37 -0
- package/assets/playbooks/common.md +66 -0
- package/assets/playbooks/intimate.md +19 -0
- package/assets/playbooks/journal.md +17 -0
- package/assets/playbooks/orders.md +15 -0
- package/assets/playbooks/payments.md +16 -0
- package/assets/playbooks/self.md +29 -0
- package/bin/install-codex-skill.mjs +77 -0
- package/bin/memoir.mjs +5 -0
- package/dist/cli.js +34062 -0
- package/package.json +40 -4
- package/release-manifest.json +432 -0
- package/skill/SKILL.md +120 -0
- package/skill/references//345/256/211/350/243/205/344/275/215/347/275/256.md +21 -0
- package/skill/references//346/225/260/346/215/256/345/217/243/345/276/204.md +36 -0
- package/skill/scripts/memoir.mjs +82 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
-- 稳定本人/设备与授权历史;只新增映射,不回写历史 actor、审计、批次或任务键。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
IF NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入停用时不能扩展身份入口'; END IF;
|
|
5
|
+
END $$;
|
|
6
|
+
CREATE TABLE agent.self_identity (
|
|
7
|
+
singleton boolean PRIMARY KEY CHECK(singleton), id text NOT NULL UNIQUE CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$')
|
|
8
|
+
);
|
|
9
|
+
CREATE TABLE agent.devices (
|
|
10
|
+
device_id text PRIMARY KEY CHECK(device_id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
11
|
+
person_id text NOT NULL REFERENCES agent.self_identity(id),name text NOT NULL CHECK(length(name) BETWEEN 1 AND 100)
|
|
12
|
+
);
|
|
13
|
+
CREATE TABLE agent.actor_owners (
|
|
14
|
+
actor text PRIMARY KEY CHECK(actor ~ '^agent:(local:[0-9]+|person:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$'),
|
|
15
|
+
person_id text NOT NULL REFERENCES agent.self_identity(id),registered_device_id text NOT NULL REFERENCES agent.devices(device_id),reason text NOT NULL CHECK(length(reason) BETWEEN 1 AND 2000)
|
|
16
|
+
);
|
|
17
|
+
CREATE TABLE agent.agreements (
|
|
18
|
+
id text NOT NULL,version integer NOT NULL CHECK(version>0),person_id text NOT NULL REFERENCES agent.self_identity(id),
|
|
19
|
+
topic text NOT NULL CHECK(length(topic) BETWEEN 1 AND 200),user_words text NOT NULL CHECK(length(user_words) BETWEEN 1 AND 2000),scope text NOT NULL CHECK(length(scope) BETWEEN 1 AND 2000),
|
|
20
|
+
state text NOT NULL CHECK(state IN ('active','cancelled')),actor text NOT NULL, idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
21
|
+
reason text NOT NULL CHECK(length(reason) BETWEEN 1 AND 2000),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
22
|
+
PRIMARY KEY(id,version),UNIQUE(person_id,idempotency_key)
|
|
23
|
+
);
|
|
24
|
+
CREATE INDEX ix_agreements_person_topic ON agent.agreements(person_id,topic,id,version DESC);
|
|
25
|
+
ALTER TABLE public.audit_log ADD COLUMN person_id text;
|
|
26
|
+
ALTER TABLE public.audit_log ADD COLUMN device_id text;
|
|
27
|
+
ALTER TABLE public.audit_log ADD COLUMN agent_name text;
|
|
28
|
+
ALTER TABLE public.audit_log ADD COLUMN agent_id text;
|
|
29
|
+
-- 旧审计保持原值;新审计分别记录逻辑 actor、本人和本次设备,不伪造旧操作来源。
|
|
30
|
+
CREATE OR REPLACE FUNCTION public.vault_audit() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
31
|
+
DECLARE item jsonb; row_key text; key_value jsonb; old_value text; new_value text;
|
|
32
|
+
BEGIN
|
|
33
|
+
item:=CASE WHEN TG_OP='DELETE' THEN to_jsonb(OLD) ELSE to_jsonb(NEW) END;
|
|
34
|
+
SELECT string_agg(item ->> key, ':' ORDER BY ord),jsonb_object_agg(key,item -> key)
|
|
35
|
+
INTO row_key,key_value FROM unnest(TG_ARGV) WITH ORDINALITY AS keys(key,ord);
|
|
36
|
+
IF TG_OP<>'INSERT' THEN old_value:=to_jsonb(OLD)::text; END IF;
|
|
37
|
+
IF TG_OP<>'DELETE' THEN new_value:=to_jsonb(NEW)::text; END IF;
|
|
38
|
+
INSERT INTO public.audit_log(at,actor,reason,table_name,row_key,op,before_json,after_json,change_set,object_schema,primary_key_json,person_id,device_id,agent_name,agent_id)
|
|
39
|
+
VALUES(to_char(clock_timestamp() AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD HH24:MI:SS'),
|
|
40
|
+
current_setting('vault.actor'),nullif(current_setting('vault.reason',true),''),TG_TABLE_NAME,row_key,lower(TG_OP),old_value,new_value,
|
|
41
|
+
nullif(current_setting('vault.change_set',true),''),TG_TABLE_SCHEMA,key_value::text,
|
|
42
|
+
nullif(current_setting('vault.person_id',true),''),nullif(current_setting('vault.device_id',true),''),nullif(current_setting('vault.agent_name',true),''),nullif(current_setting('vault.agent_id',true),''));
|
|
43
|
+
RETURN NULL;
|
|
44
|
+
END $$;
|
|
45
|
+
-- 只替换 actor 格式 CHECK;既有外键和内容/尝试/幂等约束原样保留。
|
|
46
|
+
DO $$ DECLARE constraint_row record; BEGIN
|
|
47
|
+
FOR constraint_row IN SELECT n.nspname,c.relname,k.conname,pg_get_constraintdef(k.oid) AS definition
|
|
48
|
+
FROM pg_constraint k JOIN pg_class c ON c.oid=k.conrelid JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
49
|
+
WHERE n.nspname='agent' AND k.contype='c' AND pg_get_constraintdef(k.oid) LIKE '%^agent:local:[0-9]+$%' LOOP
|
|
50
|
+
EXECUTE format('ALTER TABLE %I.%I DROP CONSTRAINT %I',constraint_row.nspname,constraint_row.relname,constraint_row.conname);
|
|
51
|
+
EXECUTE format('ALTER TABLE %I.%I ADD CONSTRAINT %I %s',constraint_row.nspname,constraint_row.relname,constraint_row.conname,
|
|
52
|
+
replace(constraint_row.definition,'^agent:local:[0-9]+$','^agent:(local:[0-9]+|person:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$'));
|
|
53
|
+
END LOOP;
|
|
54
|
+
END $$;
|
|
55
|
+
CREATE OR REPLACE FUNCTION public.vault_task_registration_guard() RETURNS trigger
|
|
56
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
57
|
+
DECLARE session_actor text:=nullif(current_setting('vault.actor',true),'');
|
|
58
|
+
BEGIN
|
|
59
|
+
IF session_actor='migration' THEN RETURN NEW; END IF;
|
|
60
|
+
-- 历史任务接续仍使用原 actor,但当前设备须属于已登记本人;不改写原计划和稳定业务键。
|
|
61
|
+
IF session_actor IS NULL OR NEW.actor IS DISTINCT FROM session_actor OR NOT EXISTS(
|
|
62
|
+
SELECT 1 FROM agent.actor_owners a JOIN agent.devices d ON d.person_id=a.person_id
|
|
63
|
+
WHERE a.actor=session_actor AND a.person_id=current_setting('vault.person_id',true) AND d.device_id=current_setting('vault.device_id',true)
|
|
64
|
+
) THEN RAISE EXCEPTION '任务登记身份与可信本人/设备写会话不一致'; END IF;
|
|
65
|
+
NEW.created_at:=to_char(clock_timestamp() AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"');
|
|
66
|
+
RETURN NEW;
|
|
67
|
+
END $$;
|
|
68
|
+
DO $$ DECLARE name text; key text; g record; BEGIN
|
|
69
|
+
FOREACH name IN ARRAY ARRAY['self_identity','devices','actor_owners','agreements'] LOOP
|
|
70
|
+
key:=CASE name WHEN 'self_identity' THEN 'id' WHEN 'devices' THEN 'device_id' WHEN 'actor_owners' THEN 'actor' ELSE 'id' END;
|
|
71
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
72
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
73
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(%2$L)',name,key);
|
|
74
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
75
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
76
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
77
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
78
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
79
|
+
END LOOP;
|
|
80
|
+
END LOOP;
|
|
81
|
+
END $$;
|
|
82
|
+
|
|
83
|
+
-- 保留原审计链验证,授权比较扩展为显式归属映射。
|
|
84
|
+
CREATE OR REPLACE FUNCTION public.vault_lock_extension_revert(p_extension_id text,p_submission_id text) RETURNS void
|
|
85
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,pg_temp AS $$
|
|
86
|
+
DECLARE e record; role_oid oid; v_actor text:=nullif(current_setting('vault.actor',true),''); v_change_set text:=nullif(current_setting('vault.change_set',true),'');
|
|
87
|
+
BEGIN
|
|
88
|
+
IF v_actor IS NULL OR v_change_set IS NULL OR v_change_set=p_submission_id OR nullif(current_setting('vault.reason',true),'') IS NULL THEN RAISE EXCEPTION 'Agent 撤销会话无效'; END IF;
|
|
89
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
90
|
+
IF NOT EXISTS(SELECT 1 FROM agent.submissions s WHERE s.id=p_submission_id AND (s.actor=v_actor OR EXISTS(SELECT 1 FROM agent.actor_owners a JOIN agent.actor_owners b ON b.person_id=a.person_id WHERE a.actor=v_actor AND b.actor=s.actor))) OR EXISTS(SELECT 1 FROM agent.reversions r WHERE r.submission_id=p_submission_id) THEN RAISE EXCEPTION 'Agent 原提交不存在、身份不匹配或已撤销'; END IF;
|
|
91
|
+
SELECT * INTO e FROM agent.extensions WHERE id=p_extension_id FOR SHARE;
|
|
92
|
+
IF NOT FOUND OR e.schema_name NOT IN ('agent_ext','agent_archive') OR e.state NOT IN ('active','deprecated','retired') THEN RAISE EXCEPTION 'Agent 撤销扩展未登记'; END IF;
|
|
93
|
+
SELECT oid INTO role_oid FROM pg_roles WHERE rolname=current_user;
|
|
94
|
+
IF NOT EXISTS(SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname=e.schema_name AND c.relname=e.table_name AND c.relkind='r' AND c.relowner=role_oid) THEN RAISE EXCEPTION 'Agent 撤销函数与扩展属主不一致'; END IF;
|
|
95
|
+
EXECUTE format('LOCK TABLE %I.%I IN SHARE ROW EXCLUSIVE MODE',e.schema_name,e.table_name);
|
|
96
|
+
END $$;
|
|
97
|
+
|
|
98
|
+
-- 参数只能定位原审计行,不接受调用者提供待恢复字段、SQL 或操作者。
|
|
99
|
+
-- 核心依赖/隐私校验仍由同一事务中的业务撤销入口编排,不能单独调用此内部函数绕过协议。
|
|
100
|
+
CREATE OR REPLACE FUNCTION public.vault_restore_extension_revert(p_extension_id text,p_submission_id text,p_key_json text,p_last_audit bigint) RETURNS void
|
|
101
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,pg_temp AS $$
|
|
102
|
+
DECLARE e record; audit_row record; first_before jsonb; final_after jsonb; previous_after jsonb; key_value jsonb:=p_key_json::jsonb;
|
|
103
|
+
original_object text; current_object text; columns_now text[]; columns_audit text[]; pk_now text[]; pk_input text[];
|
|
104
|
+
chain_count integer:=0; last_id bigint; current_row jsonb; statement text; updates text; changed bigint; stamp text;
|
|
105
|
+
previous_marker text:=current_setting('vault.extension_revert',true); v_actor text:=nullif(current_setting('vault.actor',true),'');
|
|
106
|
+
BEGIN
|
|
107
|
+
PERFORM public.vault_lock_extension_revert(p_extension_id,p_submission_id);
|
|
108
|
+
SELECT * INTO e FROM agent.extensions WHERE id=p_extension_id;
|
|
109
|
+
IF e.state NOT IN ('deprecated','retired') OR e.version<>1 THEN RAISE EXCEPTION 'Agent 限定恢复仅用于结构兼容的已废弃或归档扩展'; END IF;
|
|
110
|
+
original_object:='agent_ext.'||(e.descriptor_json::jsonb->'domain'->>'key')||'_'||(e.descriptor_json::jsonb->>'entity');
|
|
111
|
+
current_object:=e.schema_name||'.'||e.table_name;
|
|
112
|
+
IF (e.state='deprecated' AND current_object IS DISTINCT FROM original_object)
|
|
113
|
+
OR (e.state='retired' AND current_object IS DISTINCT FROM 'agent_archive.ext_'||replace(e.id,'-',''))
|
|
114
|
+
OR NOT EXISTS(SELECT 1 FROM agent.extension_migrations m WHERE m.extension_id=e.id AND m.version=e.version AND m.checksum=e.checksum AND m.descriptor_json::jsonb=e.descriptor_json::jsonb)
|
|
115
|
+
THEN RAISE EXCEPTION 'Agent 扩展结构或位置历史不一致'; END IF;
|
|
116
|
+
IF jsonb_typeof(key_value)<>'object' OR key_value='{}'::jsonb OR EXISTS(SELECT 1 FROM jsonb_each(key_value) kv WHERE jsonb_typeof(kv.value) NOT IN ('string','number')) THEN RAISE EXCEPTION 'Agent 撤销主键无效'; END IF;
|
|
117
|
+
-- 字段名使用固定字符序比较,不能让数据库地区排序规则影响结构兼容判断。
|
|
118
|
+
SELECT array_agg(x ORDER BY x COLLATE "C") INTO pk_input FROM jsonb_object_keys(key_value) x;
|
|
119
|
+
SELECT array_agg(a.attname::text ORDER BY k.ord) INTO pk_now FROM pg_index i CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ord) JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attnum=k.attnum WHERE i.indrelid=to_regclass(format('%I.%I',e.schema_name,e.table_name)) AND i.indisprimary;
|
|
120
|
+
IF pk_now IS NULL OR to_jsonb(pk_now) IS DISTINCT FROM e.descriptor_json::jsonb->'primary_key'
|
|
121
|
+
OR (SELECT array_agg(x ORDER BY x COLLATE "C") FROM unnest(pk_now) x) IS DISTINCT FROM pk_input THEN RAISE EXCEPTION 'Agent 撤销主键结构已变化'; END IF;
|
|
122
|
+
-- 新旧物理位置共同定位同一稳定对象,保持原审计文本,不按冒号拆分复合键。
|
|
123
|
+
FOR audit_row IN SELECT l.* FROM public.audit_log l WHERE l.change_set=p_submission_id AND l.primary_key_json::jsonb=key_value AND coalesce(l.object_schema,'public')||'.'||l.table_name IN (original_object,current_object) ORDER BY l.id LIMIT 4001 LOOP
|
|
124
|
+
chain_count:=chain_count+1;
|
|
125
|
+
IF audit_row.after_json IS NULL OR audit_row.actor IS DISTINCT FROM (SELECT s.actor FROM agent.submissions s WHERE s.id=p_submission_id) OR (chain_count>1 AND previous_after IS DISTINCT FROM audit_row.before_json::jsonb) THEN RAISE EXCEPTION 'Agent 扩展审计链不完整'; END IF;
|
|
126
|
+
IF chain_count=1 THEN first_before:=audit_row.before_json::jsonb; END IF;
|
|
127
|
+
final_after:=audit_row.after_json::jsonb; previous_after:=final_after; last_id:=audit_row.id;
|
|
128
|
+
IF NOT (key_value<@final_after) OR (audit_row.before_json IS NOT NULL AND NOT (key_value<@audit_row.before_json::jsonb)) THEN RAISE EXCEPTION 'Agent 扩展审计主键不一致'; END IF;
|
|
129
|
+
END LOOP;
|
|
130
|
+
IF chain_count=0 OR chain_count>4000 OR last_id IS DISTINCT FROM p_last_audit THEN RAISE EXCEPTION 'Agent 扩展审计末端已变化'; END IF;
|
|
131
|
+
SELECT array_agg(attname::text ORDER BY attname::text COLLATE "C") INTO columns_now FROM pg_attribute WHERE attrelid=to_regclass(format('%I.%I',e.schema_name,e.table_name)) AND attnum>0 AND NOT attisdropped;
|
|
132
|
+
SELECT array_agg(x ORDER BY x COLLATE "C") INTO columns_audit FROM jsonb_object_keys(final_after) x;
|
|
133
|
+
IF columns_now IS DISTINCT FROM columns_audit OR NOT ('deleted_at'=ANY(columns_now)) OR (first_before IS NOT NULL AND (SELECT array_agg(x ORDER BY x COLLATE "C") FROM jsonb_object_keys(first_before) x) IS DISTINCT FROM columns_now) THEN RAISE EXCEPTION 'Agent 扩展审计字段与当前结构不兼容'; END IF;
|
|
134
|
+
EXECUTE format('SELECT to_jsonb(t) FROM %I.%I t WHERE $1 <@ to_jsonb(t) FOR UPDATE',e.schema_name,e.table_name) INTO current_row USING key_value;
|
|
135
|
+
IF current_row IS DISTINCT FROM final_after OR EXISTS(SELECT 1 FROM public.audit_log l WHERE l.id>last_id AND l.primary_key_json::jsonb=key_value AND coalesce(l.object_schema,'public')||'.'||l.table_name IN (original_object,current_object)) THEN RAISE EXCEPTION 'Agent 扩展行或后续审计已变化'; END IF;
|
|
136
|
+
stamp:=to_char(clock_timestamp() AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD HH24:MI:SS');
|
|
137
|
+
IF first_before IS NULL THEN updates:='deleted_at=$2';
|
|
138
|
+
ELSE
|
|
139
|
+
SELECT string_agg(format('%1$I=(jsonb_populate_record(NULL::%2$I.%3$I,$1)).%1$I',c,e.schema_name,e.table_name),',' ORDER BY c) INTO updates
|
|
140
|
+
FROM unnest(columns_now) c WHERE NOT (c=ANY(pk_now)) AND c NOT IN ('created_at','created_by','updated_at','updated_by');
|
|
141
|
+
END IF;
|
|
142
|
+
statement:=format('UPDATE %I.%I t SET %s,updated_at=$2,updated_by=$3 WHERE $4 <@ to_jsonb(t)',e.schema_name,e.table_name,updates);
|
|
143
|
+
PERFORM set_config('vault.extension_revert',e.id,true);
|
|
144
|
+
EXECUTE statement USING first_before,stamp,v_actor,key_value;
|
|
145
|
+
GET DIAGNOSTICS changed=ROW_COUNT;
|
|
146
|
+
PERFORM set_config('vault.extension_revert',coalesce(previous_marker,''),true);
|
|
147
|
+
IF changed<>1 THEN RAISE EXCEPTION 'Agent 扩展限定恢复未命中唯一记录'; END IF;
|
|
148
|
+
END $$;
|
|
149
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- 用户工作的持久业务进度;没有执行器、模型启动配置或租约令牌。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN IF NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入停用时不能扩展工作入口'; END IF; END $$;
|
|
4
|
+
CREATE TABLE agent.work_records (
|
|
5
|
+
id text NOT NULL,version integer NOT NULL CHECK(version>0),person_id text NOT NULL REFERENCES agent.self_identity(id),
|
|
6
|
+
state text NOT NULL CHECK(state IN ('open','finished')),payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),
|
|
7
|
+
actor text NOT NULL,idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
8
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),PRIMARY KEY(id,version),UNIQUE(person_id,idempotency_key)
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX ix_work_records_person ON agent.work_records(person_id,id,version DESC);
|
|
11
|
+
DO $$ DECLARE name text; key text; g record; BEGIN
|
|
12
|
+
FOREACH name IN ARRAY ARRAY['work_records'] LOOP
|
|
13
|
+
key:=CASE name WHEN 'self_identity' THEN 'id' WHEN 'devices' THEN 'device_id' WHEN 'actor_owners' THEN 'actor' ELSE 'id' END;
|
|
14
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
15
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
16
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(%2$L)',name,key);
|
|
17
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
18
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
19
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
20
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
21
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
22
|
+
END LOOP;
|
|
23
|
+
END LOOP;
|
|
24
|
+
END $$;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 原件字节属于稳定存储层;不依赖 Agent 包或可拔除 schema,不移动/删除本地原件。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
CREATE TABLE public.vault_file_blobs (
|
|
4
|
+
sha256 text PRIMARY KEY CHECK(sha256 ~ '^[a-f0-9]{64}$'),size_bytes integer NOT NULL CHECK(size_bytes BETWEEN 0 AND 52428800),content bytea NOT NULL,
|
|
5
|
+
CHECK(octet_length(content)=size_bytes),CHECK(encode(sha256(content),'hex')=sha256)
|
|
6
|
+
);
|
|
7
|
+
CREATE TABLE public.vault_files (
|
|
8
|
+
id text NOT NULL,version integer NOT NULL CHECK(version>0),kind text NOT NULL CHECK(kind IN ('original','script','artifact')),
|
|
9
|
+
name text NOT NULL CHECK(length(name) BETWEEN 1 AND 255),media_type text NOT NULL CHECK(length(media_type) BETWEEN 1 AND 200),
|
|
10
|
+
sha256 text NOT NULL REFERENCES public.vault_file_blobs(sha256),person_id text,actor text NOT NULL,
|
|
11
|
+
idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),reason text NOT NULL,
|
|
12
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),PRIMARY KEY(id,version),UNIQUE(actor,idempotency_key)
|
|
13
|
+
);
|
|
14
|
+
CREATE TABLE public.vault_file_imports (
|
|
15
|
+
batch_id text NOT NULL,file_sha256 text NOT NULL REFERENCES public.vault_file_blobs(sha256),
|
|
16
|
+
PRIMARY KEY(batch_id,file_sha256),FOREIGN KEY(batch_id,file_sha256) REFERENCES public.import_files(batch_id,file_sha256)
|
|
17
|
+
);
|
|
18
|
+
CREATE TABLE public.vault_file_migrations (
|
|
19
|
+
id text PRIMARY KEY,actor text NOT NULL,person_id text NOT NULL,idempotency_key text NOT NULL,fingerprint text NOT NULL,
|
|
20
|
+
manifest_json text NOT NULL CHECK(public.vault_json_valid(manifest_json)),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),UNIQUE(person_id,idempotency_key)
|
|
21
|
+
);
|
|
22
|
+
CREATE TABLE public.vault_file_migration_items (
|
|
23
|
+
id text PRIMARY KEY,migration_id text NOT NULL REFERENCES public.vault_file_migrations(id),ordinal integer NOT NULL CHECK(ordinal>0),sequence integer NOT NULL CHECK(sequence>0),UNIQUE(migration_id,ordinal,sequence),
|
|
24
|
+
state text NOT NULL CHECK(state IN ('shared','missing','oversize','conflict','failed')),file_id text,file_version integer,
|
|
25
|
+
detail_json text NOT NULL CHECK(public.vault_json_valid(detail_json)),created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
26
|
+
FOREIGN KEY(file_id,file_version) REFERENCES public.vault_files(id,version)
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX ix_file_migration_items ON public.vault_file_migration_items(migration_id,ordinal,sequence DESC);
|
|
29
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
30
|
+
FOREACH name IN ARRAY ARRAY['vault_file_blobs','vault_files','vault_file_imports','vault_file_migrations','vault_file_migration_items'] LOOP
|
|
31
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON public.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
32
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON public.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
33
|
+
-- blob 的字节不复制进审计 JSON(避免倍增和泄漏);元数据仍有独立逐行审计。
|
|
34
|
+
IF name<>'vault_file_blobs' THEN
|
|
35
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON public.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(%2$L)',name,CASE WHEN name='vault_file_imports' THEN 'batch_id' ELSE 'id' END);
|
|
36
|
+
END IF;
|
|
37
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON public.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
38
|
+
EXECUTE format('REVOKE ALL ON public.%I FROM PUBLIC',name);
|
|
39
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
40
|
+
EXECUTE format('GRANT %s ON public.%I TO %I',g.privilege_type,name,g.rolname);
|
|
41
|
+
END LOOP;
|
|
42
|
+
END LOOP;
|
|
43
|
+
END $$;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- 受信任脚本的运行预约/回执共享;没有模型启动器或自动执行入口。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN IF NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入停用时不能扩展脚本入口'; END IF; END $$;
|
|
4
|
+
CREATE TABLE agent.script_runs (
|
|
5
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{64}$'),actor text NOT NULL,fingerprint text NOT NULL,
|
|
6
|
+
payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
7
|
+
);
|
|
8
|
+
CREATE TABLE agent.script_run_results (
|
|
9
|
+
id text PRIMARY KEY REFERENCES agent.script_runs(id),actor text NOT NULL,
|
|
10
|
+
payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
11
|
+
);
|
|
12
|
+
DO $$ DECLARE name text; key text; g record; BEGIN
|
|
13
|
+
FOREACH name IN ARRAY ARRAY['script_runs','script_run_results'] LOOP
|
|
14
|
+
key:=CASE name WHEN 'self_identity' THEN 'id' WHEN 'devices' THEN 'device_id' WHEN 'actor_owners' THEN 'actor' ELSE 'id' END;
|
|
15
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
16
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
17
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(%2$L)',name,key);
|
|
18
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
19
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
20
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
21
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
22
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
23
|
+
END LOOP;
|
|
24
|
+
END LOOP;
|
|
25
|
+
END $$;
|
|
26
|
+
|
|
27
|
+
CREATE TABLE agent.scan_keys (id text PRIMARY KEY CHECK(id='source-scan-v1'),content bytea NOT NULL CHECK(octet_length(content)=32));
|
|
28
|
+
INSERT INTO agent.scan_keys(id,content) VALUES('source-scan-v1',decode(replace(gen_random_uuid()::text||gen_random_uuid()::text,'-',''),'hex'));
|
|
29
|
+
CREATE TRIGGER trg_scan_keys_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.scan_keys FOR EACH ROW EXECUTE FUNCTION public.vault_guard('append');
|
|
30
|
+
CREATE TRIGGER trg_scan_keys_truncate BEFORE TRUNCATE ON agent.scan_keys FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard('append');
|
|
31
|
+
REVOKE ALL ON agent.scan_keys FROM PUBLIC;
|
|
32
|
+
DO $$ DECLARE g record; BEGIN
|
|
33
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type='SELECT' LOOP
|
|
34
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);EXECUTE format('GRANT SELECT ON agent.scan_keys TO %I',g.rolname);
|
|
35
|
+
END LOOP;
|
|
36
|
+
END $$;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- 仅 PostgreSQL:复用 0116 的银行统一事实,不复制、改写原始账单,也不自动关联历史记录。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
|
|
4
|
+
-- 关联审核使用单调版本而非秒级时间戳,避免同一秒内先确认后撤回仍通过旧页面校验。
|
|
5
|
+
ALTER TABLE payment_order_links ADD COLUMN revision integer NOT NULL DEFAULT 1 CHECK(revision>0);
|
|
6
|
+
ALTER TABLE payment_order_links ADD COLUMN source_fingerprint text;
|
|
7
|
+
-- 只冻结关联身份/金额/时间/渠道;退款和隐藏继续走原净额/标签视图,不把退款变成一笔新消费。
|
|
8
|
+
CREATE FUNCTION vault_order_payment_fingerprint(pp text,pt text,op text,onum text)
|
|
9
|
+
RETURNS text LANGUAGE sql STABLE AS $$
|
|
10
|
+
SELECT encode(sha256(convert_to(jsonb_build_array(p.platform,p.txn_id,p.occurred_at,p.amount_cents,p.direction,
|
|
11
|
+
p.merchant_order_no,p.pay_method,o.platform,o.order_no,o.ordered_at,o.paid_at,o.total_paid_cents,o.pay_method,o.pay_channel,o.raw_json::jsonb->>'tradeNo')::text,'UTF8')),'hex')
|
|
12
|
+
FROM payments p CROSS JOIN orders o WHERE p.platform=pp AND p.txn_id=pt AND o.platform=op AND o.order_no=onum
|
|
13
|
+
$$;
|
|
14
|
+
-- 既有人工/规则关联保持原结论,只冻结迁移时的来源版本;后续来源变化退出可信集合等待重新核对。
|
|
15
|
+
UPDATE payment_order_links SET source_fingerprint=vault_order_payment_fingerprint(payment_platform,payment_txn_id,order_platform,order_no);
|
|
16
|
+
CREATE FUNCTION vault_payment_order_revision() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
17
|
+
BEGIN
|
|
18
|
+
IF TG_OP='UPDATE' THEN NEW.revision:=OLD.revision+1; END IF;
|
|
19
|
+
NEW.source_fingerprint:=vault_order_payment_fingerprint(NEW.payment_platform,NEW.payment_txn_id,NEW.order_platform,NEW.order_no);
|
|
20
|
+
RETURN NEW;
|
|
21
|
+
END $$;
|
|
22
|
+
CREATE TRIGGER trg_payment_order_links_revision BEFORE INSERT OR UPDATE ON payment_order_links FOR EACH ROW EXECUTE FUNCTION vault_payment_order_revision();
|
|
23
|
+
-- 保持原视图列名及顺序,不因新增审核列破坏已有分摊/净额视图的依赖。
|
|
24
|
+
CREATE OR REPLACE VIEW _v_trusted_payment_orders AS
|
|
25
|
+
SELECT payment_platform,payment_txn_id,order_platform,order_no,match_method,confidence,created_at,created_by,updated_at,updated_by,deleted_at
|
|
26
|
+
FROM payment_order_links
|
|
27
|
+
WHERE deleted_at IS NULL AND (confidence='high' OR match_method='manual')
|
|
28
|
+
AND source_fingerprint=vault_order_payment_fingerprint(payment_platform,payment_txn_id,order_platform,order_no);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE payment_bank_links (
|
|
31
|
+
id text PRIMARY KEY,
|
|
32
|
+
payment_platform text NOT NULL,payment_txn_id text NOT NULL,
|
|
33
|
+
debit_transaction_key text REFERENCES bank_card_transactions(transaction_key),
|
|
34
|
+
credit_transaction_key text REFERENCES credit_card_transactions(transaction_key),
|
|
35
|
+
method text NOT NULL CHECK(method IN ('bank_reference','card_time')),
|
|
36
|
+
state text NOT NULL CHECK(state IN ('suggested','confirmed','rejected')),
|
|
37
|
+
payment_fingerprint text NOT NULL CHECK(payment_fingerprint ~ '^[a-f0-9]{64}$'),bank_fingerprint text NOT NULL CHECK(bank_fingerprint ~ '^[a-f0-9]{64}$'),
|
|
38
|
+
reason text NOT NULL,revision integer NOT NULL DEFAULT 1 CHECK(revision>0),
|
|
39
|
+
created_at text NOT NULL,created_by text NOT NULL,updated_at text NOT NULL,updated_by text NOT NULL,
|
|
40
|
+
FOREIGN KEY(payment_platform,payment_txn_id) REFERENCES payments(platform,txn_id),
|
|
41
|
+
CHECK(num_nonnulls(debit_transaction_key,credit_transaction_key)=1)
|
|
42
|
+
);
|
|
43
|
+
-- 首版只确认逐笔全额的一对一资金来源;合并支付和混合付款保留待确认,禁止抢占已确认端点。
|
|
44
|
+
CREATE UNIQUE INDEX ix_payment_bank_pair_debit ON payment_bank_links(payment_platform,payment_txn_id,debit_transaction_key) WHERE debit_transaction_key IS NOT NULL;
|
|
45
|
+
CREATE UNIQUE INDEX ix_payment_bank_pair_credit ON payment_bank_links(payment_platform,payment_txn_id,credit_transaction_key) WHERE credit_transaction_key IS NOT NULL;
|
|
46
|
+
CREATE UNIQUE INDEX ix_payment_bank_confirmed_payment ON payment_bank_links(payment_platform,payment_txn_id) WHERE state='confirmed';
|
|
47
|
+
CREATE UNIQUE INDEX ix_payment_bank_confirmed_debit ON payment_bank_links(debit_transaction_key) WHERE state='confirmed';
|
|
48
|
+
CREATE UNIQUE INDEX ix_payment_bank_confirmed_credit ON payment_bank_links(credit_transaction_key) WHERE state='confirmed';
|
|
49
|
+
|
|
50
|
+
CREATE TABLE finance_bank_reviews (
|
|
51
|
+
id text PRIMARY KEY,
|
|
52
|
+
debit_transaction_key text REFERENCES bank_card_transactions(transaction_key),
|
|
53
|
+
credit_transaction_key text REFERENCES credit_card_transactions(transaction_key),
|
|
54
|
+
decision text NOT NULL CHECK(decision IN ('expense','excluded','unreviewed')),
|
|
55
|
+
bank_fingerprint text NOT NULL CHECK(bank_fingerprint ~ '^[a-f0-9]{64}$'),revision integer NOT NULL DEFAULT 1 CHECK(revision>0),
|
|
56
|
+
created_at text NOT NULL,created_by text NOT NULL,updated_at text NOT NULL,updated_by text NOT NULL,
|
|
57
|
+
CHECK(num_nonnulls(debit_transaction_key,credit_transaction_key)=1),
|
|
58
|
+
UNIQUE(debit_transaction_key),UNIQUE(credit_transaction_key)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- 借记卡扣款、信用卡消费使用同一只读投影;还款、退款、其他入账保留原交易类型,不反推消费。
|
|
62
|
+
CREATE VIEW _v_finance_bank_entries AS
|
|
63
|
+
SELECT 'debit'::text AS kind,t.transaction_key,t.source_kind,t.bank_card_id,
|
|
64
|
+
t.occurred_at,t.date_precision,t.currency,t.amount_cents,t.direction,
|
|
65
|
+
CASE WHEN t.direction='out' THEN 'outflow' ELSE 'inflow' END AS transaction_kind,
|
|
66
|
+
t.card_last_four,coalesce(c.bank_name,'') AS bank_name,
|
|
67
|
+
concat_ws(' ',t.counterparty,t.description,t.institution,t.bank_txn_id,t.transaction_card) AS description,
|
|
68
|
+
t.bank_txn_id AS reference,t.last_batch_id
|
|
69
|
+
FROM bank_card_transactions t LEFT JOIN bank_cards c ON c.id=t.bank_card_id AND c.deleted_at IS NULL
|
|
70
|
+
UNION ALL
|
|
71
|
+
SELECT 'credit',t.transaction_key,t.source_kind,t.bank_card_id,
|
|
72
|
+
coalesce(nullif(t.occurred_at,''),nullif(t.transaction_date,''),t.posting_date),
|
|
73
|
+
coalesce(t.date_precision,'day'),t.settlement_currency,abs(t.settlement_amount_cents),
|
|
74
|
+
CASE WHEN t.settlement_amount_cents>0 THEN 'out' ELSE 'in' END,t.transaction_kind,
|
|
75
|
+
t.card_last_four,coalesce(c.bank_name,''),
|
|
76
|
+
concat_ws(' ',t.description,t.counterparty,t.payment_channel,t.remark),'' AS reference,t.last_batch_id
|
|
77
|
+
FROM credit_card_transactions t LEFT JOIN bank_cards c ON c.id=t.bank_card_id AND c.deleted_at IS NULL;
|
|
78
|
+
|
|
79
|
+
CREATE INDEX ix_finance_payment_amount_date ON payments(amount_cents,occurred_at);
|
|
80
|
+
|
|
81
|
+
-- 新知识表与既有人工关联表一样,要求写会话、逐行审计、版本失效与最小角色权限。
|
|
82
|
+
DO $$ DECLARE tbl text; g record; BEGIN
|
|
83
|
+
FOREACH tbl IN ARRAY ARRAY['payment_bank_links','finance_bank_reviews'] LOOP
|
|
84
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON %1$I FOR EACH ROW EXECUTE FUNCTION vault_guard(''session'')',tbl);
|
|
85
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON %1$I FOR EACH STATEMENT EXECUTE FUNCTION vault_guard(''session'')',tbl);
|
|
86
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON %1$I FOR EACH ROW EXECUTE FUNCTION vault_audit(''id'')',tbl);
|
|
87
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON %1$I FOR EACH STATEMENT EXECUTE FUNCTION vault_touch()',tbl);
|
|
88
|
+
REVOKE ALL ON payment_bank_links,finance_bank_reviews FROM PUBLIC;
|
|
89
|
+
FOR g IN SELECT DISTINCT grantee,privilege_type FROM information_schema.role_table_grants WHERE table_schema='public' AND table_name='payment_order_links' AND grantee<>current_user AND privilege_type IN ('SELECT','INSERT','UPDATE') LOOP
|
|
90
|
+
EXECUTE format('GRANT %s ON %I TO %s',g.privilege_type,tbl,CASE WHEN g.grantee='PUBLIC' THEN 'PUBLIC' ELSE quote_ident(g.grantee) END);
|
|
91
|
+
END LOOP;
|
|
92
|
+
END LOOP;
|
|
93
|
+
FOR g IN SELECT DISTINCT grantee FROM information_schema.role_table_grants WHERE table_schema='public' AND table_name='bank_card_transactions' AND privilege_type='SELECT' AND grantee<>current_user LOOP
|
|
94
|
+
EXECUTE format('GRANT SELECT ON _v_finance_bank_entries TO %s',CASE WHEN g.grantee='PUBLIC' THEN 'PUBLIC' ELSE quote_ident(g.grantee) END);
|
|
95
|
+
END LOOP;
|
|
96
|
+
END $$;
|