建立5178独立项目基线
verify / booking-verify (push) Has been cancelled

This commit is contained in:
鲨鱼辣椒 committed 2026-09-08 15:03:45 +08:00
commit 31849411a8
1578 files changed
+290463

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# PostgreSQL target schema: `th_hotel_booking`
This package is the isolated PostgreSQL V0.1 target model for the booking-email workflow. The current Spring runtime remains on its existing MySQL/H2 migration line; these scripts must not be added to that Flyway location or executed against `public`.
## Scope
- Owns only schema `th_hotel_booking` and objects qualified with that schema.
- Covers source mail/revision evidence, deterministic processing runs, normalized facts, order tasks/cards, task relations and confirmation history.
- Keeps explicit `retention_until` values with a three-month default. No deletion scheduler is included in V0.1.
- Does not contain PMS/Opera execution tables or commands.
- Does not create roles, extensions, databases, objects in `public`, or objects in another project schema.
## Safe execution order
1. Connect to the explicitly approved database with an interactive password prompt or a secret environment injected outside the repository.
2. Run `preflight_read_only.sql` and retain its output.
3. Confirm `can_create_schema=true` and inspect any existing `th_hotel_booking` objects. If an unexpected object exists, stop.
4. Run `V001__bootstrap_th_hotel_booking.sql` with `ON_ERROR_STOP=1` in one transaction.
5. Run `postflight_read_only.sql` and compare non-project schema counts with the preflight output.
Example (password is prompted and never placed in the command):
```sh
psql -X -W -h <host> -p <port> -U <user> -d <database> -v ON_ERROR_STOP=1 -f database/postgresql/preflight_read_only.sql
psql -X -W -h <host> -p <port> -U <user> -d <database> -v ON_ERROR_STOP=1 -1 -f database/postgresql/V001__bootstrap_th_hotel_booking.sql
psql -X -W -h <host> -p <port> -U <user> -d <database> -v ON_ERROR_STOP=1 -f database/postgresql/postflight_read_only.sql
```
## Primary supported queries
- Find one actual email and all processing revisions by `(hotel_id, provider, external_message_id)`.
- Read a conversation in received order through `external_conversation_id`.
- List open/review-required tasks and cards by hotel, type and status.
- Follow linked Trace/Allotment relationships through `task_relation`.
- Reconstruct who confirmed or overrode a card and when.
- Find records whose three-month retention window has expired without deleting them automatically.
@@ -0,0 +1,265 @@
\set ON_ERROR_STOP on
CREATE SCHEMA IF NOT EXISTS th_hotel_booking AUTHORIZATION CURRENT_USER;
CREATE TABLE IF NOT EXISTS th_hotel_booking.schema_migration (
version VARCHAR(32) PRIMARY KEY,
description VARCHAR(255) NOT NULL,
installed_by VARCHAR(128) NOT NULL DEFAULT CURRENT_USER,
installed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS th_hotel_booking.source_message (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
hotel_id VARCHAR(64) NOT NULL,
provider VARCHAR(64) NOT NULL,
channel VARCHAR(32) NOT NULL,
external_message_id VARCHAR(256) NOT NULL,
external_conversation_id VARCHAR(256) NOT NULL,
message_sha256 CHAR(64) NOT NULL,
subject TEXT,
sender_summary TEXT,
source_sent_at TIMESTAMPTZ,
received_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
source_schema_version VARCHAR(64) NOT NULL,
original_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_source_actual_message
UNIQUE (hotel_id, provider, external_message_id),
CONSTRAINT ck_booking_source_sha256
CHECK (message_sha256 ~ '^[0-9a-f]{64}$'),
CONSTRAINT ck_booking_source_payload_object
CHECK (jsonb_typeof(original_payload) = 'object')
);
CREATE INDEX IF NOT EXISTS idx_booking_source_conversation
ON th_hotel_booking.source_message (hotel_id, external_conversation_id, received_at, id);
CREATE INDEX IF NOT EXISTS idx_booking_source_retention
ON th_hotel_booking.source_message (retention_until);
CREATE TABLE IF NOT EXISTS th_hotel_booking.source_attachment (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
source_message_id BIGINT NOT NULL
REFERENCES th_hotel_booking.source_message (id) ON DELETE CASCADE,
attachment_ordinal INTEGER NOT NULL,
file_name TEXT NOT NULL,
content_type VARCHAR(255),
size_bytes BIGINT NOT NULL,
content_sha256 CHAR(64) NOT NULL,
object_reference TEXT,
is_current_material BOOLEAN NOT NULL DEFAULT TRUE,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_source_attachment_ordinal
UNIQUE (source_message_id, attachment_ordinal),
CONSTRAINT ck_booking_attachment_ordinal_positive
CHECK (attachment_ordinal > 0),
CONSTRAINT ck_booking_attachment_size_nonnegative
CHECK (size_bytes >= 0),
CONSTRAINT ck_booking_attachment_sha256
CHECK (content_sha256 ~ '^[0-9a-f]{64}$')
);
CREATE INDEX IF NOT EXISTS idx_booking_attachment_source
ON th_hotel_booking.source_attachment (source_message_id);
CREATE TABLE IF NOT EXISTS th_hotel_booking.processing_run (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
source_message_id BIGINT NOT NULL
REFERENCES th_hotel_booking.source_message (id) ON DELETE CASCADE,
revision_number INTEGER NOT NULL,
run_status VARCHAR(32) NOT NULL,
catalog_version VARCHAR(64),
parser_version VARCHAR(64),
agent_profile_version VARCHAR(128),
input_fingerprint CHAR(64) NOT NULL,
warning_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_processing_revision
UNIQUE (source_message_id, revision_number),
CONSTRAINT ck_booking_processing_revision_positive
CHECK (revision_number > 0),
CONSTRAINT ck_booking_processing_status
CHECK (run_status IN ('RECEIVED', 'PARSING', 'AGENT_REVIEW', 'VALIDATED', 'TASKS_CREATED', 'FAILED')),
CONSTRAINT ck_booking_processing_fingerprint
CHECK (input_fingerprint ~ '^[0-9a-f]{64}$'),
CONSTRAINT ck_booking_processing_warnings_array
CHECK (jsonb_typeof(warning_payload) = 'array')
);
CREATE INDEX IF NOT EXISTS idx_booking_processing_source
ON th_hotel_booking.processing_run (source_message_id, revision_number DESC);
CREATE INDEX IF NOT EXISTS idx_booking_processing_status
ON th_hotel_booking.processing_run (run_status, started_at DESC);
CREATE TABLE IF NOT EXISTS th_hotel_booking.booking_fact (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
processing_run_id BIGINT NOT NULL
REFERENCES th_hotel_booking.processing_run (id) ON DELETE CASCADE,
source_event_index INTEGER NOT NULL,
order_ref VARCHAR(160) NOT NULL,
event_type VARCHAR(48) NOT NULL,
booking_type VARCHAR(16),
names_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
group_code VARCHAR(128),
group_name TEXT,
arrival_date DATE,
departure_date DATE,
room_items_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
trace_items_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
recognition_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
warning_payload JSONB NOT NULL DEFAULT '[]'::jsonb,
requires_review BOOLEAN NOT NULL DEFAULT TRUE,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_fact_event_index
UNIQUE (processing_run_id, source_event_index),
CONSTRAINT ck_booking_fact_event_index_nonnegative
CHECK (source_event_index >= 0),
CONSTRAINT ck_booking_fact_event_type
CHECK (event_type IN ('NEW_BOOKING', 'UPDATE_BOOKING', 'CANCEL_BOOKING', 'TRACE_RESERVATION_NOTES', 'ROOMING_LIST', 'PAYMENT')),
CONSTRAINT ck_booking_fact_booking_type
CHECK (booking_type IS NULL OR booking_type IN ('FIT', 'GROUP')),
CONSTRAINT ck_booking_fact_names_array
CHECK (jsonb_typeof(names_payload) = 'array'),
CONSTRAINT ck_booking_fact_rooms_array
CHECK (jsonb_typeof(room_items_payload) = 'array'),
CONSTRAINT ck_booking_fact_traces_array
CHECK (jsonb_typeof(trace_items_payload) = 'array'),
CONSTRAINT ck_booking_fact_recognition_object
CHECK (jsonb_typeof(recognition_payload) = 'object'),
CONSTRAINT ck_booking_fact_warnings_array
CHECK (jsonb_typeof(warning_payload) = 'array'),
CONSTRAINT ck_booking_fact_stay_dates
CHECK (arrival_date IS NULL OR departure_date IS NULL OR departure_date > arrival_date)
);
CREATE INDEX IF NOT EXISTS idx_booking_fact_run_order
ON th_hotel_booking.booking_fact (processing_run_id, order_ref, source_event_index);
CREATE INDEX IF NOT EXISTS idx_booking_fact_type_review
ON th_hotel_booking.booking_fact (event_type, requires_review);
CREATE TABLE IF NOT EXISTS th_hotel_booking.order_task (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
hotel_id VARCHAR(64) NOT NULL,
source_message_id BIGINT NOT NULL
REFERENCES th_hotel_booking.source_message (id) ON DELETE CASCADE,
processing_run_id BIGINT NOT NULL
REFERENCES th_hotel_booking.processing_run (id) ON DELETE CASCADE,
order_ref VARCHAR(160) NOT NULL,
idempotency_key VARCHAR(320) NOT NULL,
task_status VARCHAR(32) NOT NULL,
target_booking_type VARCHAR(16),
target_locator_type VARCHAR(32),
target_locator_value VARCHAR(256),
target_resolution_status VARCHAR(32) NOT NULL DEFAULT 'UNRESOLVED',
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_order_task_idempotency
UNIQUE (hotel_id, idempotency_key),
CONSTRAINT ck_booking_order_task_status
CHECK (task_status IN ('OPEN', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED')),
CONSTRAINT ck_booking_order_task_booking_type
CHECK (target_booking_type IS NULL OR target_booking_type IN ('FIT', 'GROUP')),
CONSTRAINT ck_booking_order_task_resolution
CHECK (target_resolution_status IN ('UNRESOLVED', 'RESOLVED', 'CONFLICT'))
);
CREATE INDEX IF NOT EXISTS idx_booking_order_task_queue
ON th_hotel_booking.order_task (hotel_id, task_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_booking_order_task_source
ON th_hotel_booking.order_task (source_message_id, processing_run_id);
CREATE TABLE IF NOT EXISTS th_hotel_booking.task_card (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_task_id BIGINT NOT NULL
REFERENCES th_hotel_booking.order_task (id) ON DELETE CASCADE,
source_event_index INTEGER,
card_type VARCHAR(64) NOT NULL,
event_type VARCHAR(48),
task_subtype VARCHAR(96),
card_status VARCHAR(32) NOT NULL,
review_status VARCHAR(32),
display_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
confirmed_payload JSONB,
manual_review_payload JSONB,
row_version BIGINT NOT NULL DEFAULT 0,
confirmed_by VARCHAR(128),
confirmed_at TIMESTAMPTZ,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_task_card_source
UNIQUE (order_task_id, card_type, source_event_index),
CONSTRAINT ck_booking_task_card_status
CHECK (card_status IN ('READONLY', 'PENDING_CONFIRM', 'REVIEW_REQUIRED', 'CONFIRMED', 'CANCELLED')),
CONSTRAINT ck_booking_task_card_review
CHECK (review_status IS NULL OR review_status IN ('PENDING', 'RESOLVED')),
CONSTRAINT ck_booking_task_card_display_object
CHECK (jsonb_typeof(display_payload) = 'object'),
CONSTRAINT ck_booking_task_card_confirmed_object
CHECK (confirmed_payload IS NULL OR jsonb_typeof(confirmed_payload) = 'object'),
CONSTRAINT ck_booking_task_card_manual_review_object
CHECK (manual_review_payload IS NULL OR jsonb_typeof(manual_review_payload) = 'object'),
CONSTRAINT ck_booking_task_card_version_nonnegative
CHECK (row_version >= 0)
);
CREATE INDEX IF NOT EXISTS idx_booking_task_card_work_queue
ON th_hotel_booking.task_card (card_status, review_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_booking_task_card_order
ON th_hotel_booking.task_card (order_task_id, id);
CREATE TABLE IF NOT EXISTS th_hotel_booking.task_relation (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
source_order_task_id BIGINT NOT NULL
REFERENCES th_hotel_booking.order_task (id) ON DELETE CASCADE,
target_order_task_id BIGINT NOT NULL
REFERENCES th_hotel_booking.order_task (id) ON DELETE CASCADE,
relationship_type VARCHAR(96) NOT NULL,
relationship_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_booking_task_relation
UNIQUE (source_order_task_id, target_order_task_id, relationship_type),
CONSTRAINT ck_booking_task_relation_not_self
CHECK (source_order_task_id <> target_order_task_id),
CONSTRAINT ck_booking_task_relation_payload_object
CHECK (jsonb_typeof(relationship_payload) = 'object')
);
CREATE INDEX IF NOT EXISTS idx_booking_task_relation_target
ON th_hotel_booking.task_relation (target_order_task_id, relationship_type);
CREATE TABLE IF NOT EXISTS th_hotel_booking.confirmation_action (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_task_id BIGINT NOT NULL
REFERENCES th_hotel_booking.order_task (id) ON DELETE CASCADE,
task_card_id BIGINT NOT NULL
REFERENCES th_hotel_booking.task_card (id) ON DELETE CASCADE,
action_type VARCHAR(32) NOT NULL,
field_overrides JSONB NOT NULL DEFAULT '[]'::jsonb,
reason TEXT,
actor_id VARCHAR(128) NOT NULL,
acted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
retention_until TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '3 months'),
CONSTRAINT ck_booking_confirmation_action
CHECK (action_type IN ('CONFIRM', 'RESOLVE_REVIEW', 'REOPEN')),
CONSTRAINT ck_booking_confirmation_overrides_array
CHECK (jsonb_typeof(field_overrides) = 'array')
);
CREATE INDEX IF NOT EXISTS idx_booking_confirmation_card_time
ON th_hotel_booking.confirmation_action (task_card_id, acted_at DESC);
CREATE INDEX IF NOT EXISTS idx_booking_confirmation_actor_time
ON th_hotel_booking.confirmation_action (actor_id, acted_at DESC);
INSERT INTO th_hotel_booking.schema_migration (version, description)
VALUES ('001', 'bootstrap booking email to confirmation target model')
ON CONFLICT (version) DO NOTHING;
@@ -0,0 +1,48 @@
\set ON_ERROR_STOP on
BEGIN READ ONLY;
SELECT
current_database() AS current_database,
current_user AS current_user,
n.nspname AS schema_name,
pg_get_userbyid(n.nspowner) AS schema_owner,
has_schema_privilege(current_user, n.oid, 'USAGE') AS can_use,
has_schema_privilege(current_user, n.oid, 'CREATE') AS can_create_objects
FROM pg_namespace n
WHERE n.nspname = 'th_hotel_booking';
SELECT version, description, installed_by, installed_at
FROM th_hotel_booking.schema_migration
ORDER BY version;
SELECT
c.relkind,
c.relname AS object_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'th_hotel_booking'
ORDER BY c.relkind, c.relname;
SELECT
n.nspname AS schema_name,
c.relkind,
count(*) AS object_count
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT LIKE 'pg_%'
AND n.nspname <> 'information_schema'
GROUP BY n.nspname, c.relkind
ORDER BY n.nspname, c.relkind;
SELECT
conrelid::regclass::text AS constrained_table,
confrelid::regclass::text AS referenced_table,
conname
FROM pg_constraint
WHERE contype = 'f'
AND connamespace = 'th_hotel_booking'::regnamespace
AND confrelid::regclass::text NOT LIKE 'th_hotel_booking.%'
ORDER BY constrained_table, conname;
COMMIT;
@@ -0,0 +1,41 @@
\set ON_ERROR_STOP on
BEGIN READ ONLY;
SELECT
current_database() AS current_database,
current_user AS current_user,
inet_server_addr()::text AS server_address,
inet_server_port() AS server_port,
has_database_privilege(current_user, current_database(), 'CREATE') AS can_create_schema;
SELECT
n.nspname AS schema_name,
pg_get_userbyid(n.nspowner) AS schema_owner,
has_schema_privilege(current_user, n.oid, 'USAGE') AS can_use,
has_schema_privilege(current_user, n.oid, 'CREATE') AS can_create_objects
FROM pg_namespace n
WHERE n.nspname = 'th_hotel_booking';
SELECT
n.nspname AS schema_name,
c.relkind,
count(*) AS object_count
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT LIKE 'pg_%'
AND n.nspname <> 'information_schema'
GROUP BY n.nspname, c.relkind
ORDER BY n.nspname, c.relkind;
SELECT
n.nspname AS schema_name,
c.relname AS object_name,
c.relkind
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'th_hotel_booking'
ORDER BY c.relkind, c.relname;
COMMIT;
+128
View File
@@ -0,0 +1,128 @@
\set ON_ERROR_STOP on
BEGIN;
INSERT INTO th_hotel_booking.source_message (
hotel_id,
provider,
channel,
external_message_id,
external_conversation_id,
message_sha256,
subject,
source_schema_version
) VALUES (
'HOTEL-SMOKE',
'SMOKE_TEST',
'EMAIL',
'smoke-message-v001',
'smoke-conversation-v001',
repeat('a', 64),
'Transactional schema smoke test',
'smoke-v1'
) RETURNING id \gset smoke_source_
INSERT INTO th_hotel_booking.processing_run (
source_message_id,
revision_number,
run_status,
catalog_version,
parser_version,
input_fingerprint
) VALUES (
:smoke_source_id,
1,
'TASKS_CREATED',
'booking-catalog-v20260808',
'booking-email-parser-v0.1',
repeat('b', 64)
) RETURNING id \gset smoke_run_
INSERT INTO th_hotel_booking.booking_fact (
processing_run_id,
source_event_index,
order_ref,
event_type,
booking_type,
names_payload,
room_items_payload,
recognition_payload
) VALUES (
:smoke_run_id,
0,
'SMOKE-ORDER-1',
'NEW_BOOKING',
'GROUP',
'["SMOKE-NAME"]'::jsonb,
'[{"room_type_code":"RM3","room_count":5}]'::jsonb,
'{"selection_status":"STRICT_CURRENT_CANDIDATE"}'::jsonb
);
INSERT INTO th_hotel_booking.order_task (
hotel_id,
source_message_id,
processing_run_id,
order_ref,
idempotency_key,
task_status,
target_booking_type
) VALUES (
'HOTEL-SMOKE',
:smoke_source_id,
:smoke_run_id,
'SMOKE-ORDER-1',
'smoke-idempotency-v001',
'OPEN',
'GROUP'
) RETURNING id \gset smoke_task_
INSERT INTO th_hotel_booking.task_card (
order_task_id,
source_event_index,
card_type,
event_type,
task_subtype,
card_status,
review_status,
display_payload
) VALUES (
:smoke_task_id,
0,
'ROOM_INFORMATION',
'NEW_BOOKING',
'new_group_block',
'REVIEW_REQUIRED',
'PENDING',
'{"room_information":{"booking_type":"GROUP"}}'::jsonb
) RETURNING id \gset smoke_card_
INSERT INTO th_hotel_booking.confirmation_action (
order_task_id,
task_card_id,
action_type,
field_overrides,
reason,
actor_id
) VALUES (
:smoke_task_id,
:smoke_card_id,
'RESOLVE_REVIEW',
'[{"field_pointer":"/room_information/final_values/group_code","value":"SMOKE-GROUP"}]'::jsonb,
'Rollback-only schema smoke test',
'codex-smoke'
);
SELECT
count(*) AS joined_smoke_rows
FROM th_hotel_booking.source_message sm
JOIN th_hotel_booking.processing_run pr ON pr.source_message_id = sm.id
JOIN th_hotel_booking.order_task ot ON ot.processing_run_id = pr.id
JOIN th_hotel_booking.task_card tc ON tc.order_task_id = ot.id
JOIN th_hotel_booking.confirmation_action ca ON ca.task_card_id = tc.id
WHERE sm.id = :smoke_source_id;
ROLLBACK;
SELECT count(*) AS persisted_smoke_rows
FROM th_hotel_booking.source_message
WHERE external_message_id = 'smoke-message-v001';