Files
danyang-match/backend/src/test/resources/schema-h2.sql
T
WorkBuddy d9c08d85f9 feat(privacy): 键集以小程序为准 —— V047 补列 + 接通 /account/privacy-settings 桩
产品裁定:隐私设置键集**以小程序为准**,用迁移补齐后端缺失列。

V047 新增 allow_activity_invitation(默认 TRUE)与 show_last_online(默认 FALSE)。
默认值**不对称**,与小程序 data 初值严格一致 —— showLastOnline 写成 TRUE 等于默认公开用户在线状态。
既有 6 列保留不动:UserProfileService 的资料详情 DTO 仍在全量返回它们(第 74-79 行),
那是仍在生效的独立读取出口。本迁移只做加法,不做替换。

AccountLifecycleService 的两个 TODO 桩改为委托 PrivacyService 真实读写。
这是**用户真正走的路径**(小程序调 /account/privacy-settings,而 /me/* 与 /profile/* 无人调用),
此前「保存成功」只是回 {updated:true} 而从不落库。

入参校验:已知键类型错误必须显式抛错,未知键宽容忽略 ——
若静默丢弃,用户会看到「保存成功」而设置从未生效,这类缺陷最难排查。
另容错 Boolean 的字符串形式("true"/"false")。

test(privacy): PrivacySettingsUpsertGuardTest 扩至 8 用例,其中 2 个是契约守卫

契约守卫**直接解析小程序源文件**的 PUT payload 与 data 块得出权威键集/默认值,
不抄契约副本(沿用 EntityMigrationParityTest 的纪律)—— 小程序改键即变红。
① 键集:后端返回键必须与小程序 PUT payload 键完全一致(不多不少)
② 默认值:后端默认值必须与小程序 data 初值一致(含 showLastOnline=false 这条不对称项)
③ 桩已接通:经 /account/privacy-settings 写入必须真落库且可读回
④ 已知键传错类型必须报错,不得静默丢弃
其余 4 个为 upsert 行为守卫(连续两次更新不炸且不产生第二行 / 部分更新不重置 / 默认值 / 读取无副作用)。

负向注入:把桩退回「不落库」→ 恰 2 红(③ 与 ④),其余 6 个正确未红。已还原并复跑全绿。

test(deployment): MigrationValidationTest 基线 46 → 47

新增迁移必须同步更新 LATEST_MIGRATION,否则全量回归报「版本号超出当前基线」。
该守卫同时断言「迁移文件总数 == 基线」(不允许缺号),是有效守卫 ——
正确做法是更新常量,不要为了让测试变绿而改断言方式。
2026-09-19 17:11:46 +08:00

636 lines
38 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
DROP ALL OBJECTS;
CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY, wechat_openid VARCHAR(128) NOT NULL UNIQUE,
wechat_unionid VARCHAR(128), phone VARCHAR(32), account_status VARCHAR(32),
relationship_status VARCHAR(32), recommendation_enabled BOOLEAN,
last_login_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE user_relations (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), target_user_id VARCHAR(36), relation_type VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
UNIQUE (user_id, target_user_id, relation_type)
);
-- 与 V002__add_user_and_verification_tables.sql 的 user_profiles 对齐(列名/语义一致),
-- 供 DiscoverService 搜索(INV-004 搜索不可见)等需要真实资料的用例使用。
CREATE TABLE user_profiles (
user_id VARCHAR(36) PRIMARY KEY, display_name VARCHAR(64), gender VARCHAR(16),
birth_date DATE, height_cm INT, region_code VARCHAR(32), region_name VARCHAR(128),
marital_status VARCHAR(32), children_status VARCHAR(32), education_code VARCHAR(32),
occupation_code VARCHAR(32), work_region_code VARCHAR(32), smoking_status VARCHAR(32),
drinking_status VARCHAR(32), long_term_danyang VARCHAR(32), future_danyang VARCHAR(32),
relationship_goal VARCHAR(64), marriage_plan VARCHAR(32), self_intro CLOB,
profile_completion INT DEFAULT 0, profile_status VARCHAR(32) DEFAULT 'in_progress',
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE matches (
id VARCHAR(36) PRIMARY KEY, user_a_id VARCHAR(36), user_b_id VARCHAR(36), status VARCHAR(32), source VARCHAR(32),
source_id VARCHAR(64), last_activity_at TIMESTAMP WITH TIME ZONE, unmatched_by VARCHAR(36), episode INT,
matched_at TIMESTAMP WITH TIME ZONE, unmatched_at TIMESTAMP WITH TIME ZONE, successful_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- 与 V003__add_review_and_relation_tables.sql:91 的生产约束保持一致。
-- 测试库若缺此约束,形如 source='liked' 的非法取值会被静默放过,只在 PostgreSQL 上炸。
CONSTRAINT chk_match_source CHECK (source IN ('user_like', 'activity_mutual', 'matchmaker'))
);
-- Phase 16:活动互选二阶段同意表(与 V024 + V037 + V041 生产 DDL 对齐)。
-- 此前完全缺失 → 互选相关路径零覆盖(只能写假测试)。
-- 注意 deleted_at 必须为可空 TIMESTAMP(V037 已为生产表补该列;BaseEntity 逻辑删除依赖它)。
CREATE TABLE activity_mutual_connection_consents (
id VARCHAR(36) PRIMARY KEY,
activity_id VARCHAR(36) NOT NULL,
user_a_id VARCHAR(36) NOT NULL,
user_b_id VARCHAR(36) NOT NULL,
user_a_status VARCHAR(32) NOT NULL DEFAULT 'pending',
user_b_status VARCHAR(32) NOT NULL DEFAULT 'pending',
both_confirmed_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE,
match_id VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
UNIQUE (activity_id, user_a_id, user_b_id),
CONSTRAINT chk_consent_status CHECK (
user_a_status IN ('pending', 'accepted', 'declined')
AND user_b_status IN ('pending', 'accepted', 'declined')
)
);
CREATE TABLE relationship_success_requests (
id VARCHAR(36) PRIMARY KEY, match_id VARCHAR(36), initiated_by VARCHAR(36), status VARCHAR(32), confirmed_by VARCHAR(36),
resolved_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE conversations (
id VARCHAR(36) PRIMARY KEY, match_id VARCHAR(36) UNIQUE, status VARCHAR(32), last_message_id VARCHAR(36),
last_message_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- 复合主键 (conversation_id, user_id),与 V004/V008 真实表结构一致,无 id 列
CREATE TABLE conversation_members (
conversation_id VARCHAR(36), user_id VARCHAR(36), last_read_message_id VARCHAR(36),
unread_count INT, can_send BOOLEAN, joined_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (conversation_id, user_id)
);
CREATE TABLE messages (
id VARCHAR(36) PRIMARY KEY, conversation_id VARCHAR(36), sender_id VARCHAR(36), client_message_id VARCHAR(128),
message_type VARCHAR(32), content CLOB, file_id VARCHAR(36), status VARCHAR(32), risk_level VARCHAR(16),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- 与 V004__add_messaging_and_recommendation_tables.sql:71 的生产唯一索引对齐:
-- CREATE UNIQUE INDEX uk_message_client_id ON messages(sender_id, client_message_id)
-- WHERE client_message_id IS NOT NULL;
-- 生产带 WHERE(部分索引),H2 不支持过滤索引;但 H2 的唯一索引对 NULL 采取
-- SQL 标准语义(多个 NULL 互不冲突),因此省略 WHERE 后语义等价,
-- 保证「未带幂等键的消息」(如系统 RiskNotice)仍可多条存在。
-- 这条索引是"正常会话不重复投递"在并发下的真正保障:service 侧的
-- SELECT-then-INSERT 在并发重放时会有竞态,只有 DB 唯一索引能兜住。
CREATE UNIQUE INDEX uk_message_client_id ON messages(sender_id, client_message_id);
CREATE TABLE audit_logs (
id VARCHAR(36) PRIMARY KEY, operator_id VARCHAR(36), organization_id VARCHAR(36), action VARCHAR(128),
resource_type VARCHAR(64), resource_id VARCHAR(128), before_data CLOB, after_data CLOB, ip VARCHAR(64),
user_agent VARCHAR(500), created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE notifications (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), type VARCHAR(32), title VARCHAR(200), content VARCHAR(2000),
business_type VARCHAR(32), business_id VARCHAR(128), read_status VARCHAR(16), read_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- 对齐生产 V036 uk_notification_business:NOT NULL 业务行去重;H2 不支持 partial index,
-- 但标准 UNIQUE 对 NULL 的语义(多行 NULL 允许共存)与生产 partial index 等价。
CREATE UNIQUE INDEX uk_notification_business ON notifications(user_id, type, business_type, business_id, content);
-- 对齐生产 V004/V020 chk_notification_type:缺少此约束会导致写入非法 type 时测试静默通过,
-- 而真实 PostgreSQL 直接拒绝 —— Phase 11 十种通知曾因此长期带病运行(D3)。
ALTER TABLE notifications ADD CONSTRAINT chk_notification_type CHECK (
type IN ('review', 'match', 'matchmaker', 'activity', 'risk', 'account', 'system',
'appointment', 'counseling', 'premarital', 'local_eligibility', 'service_location')
);
CREATE TABLE recommendation_records (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), target_user_id VARCHAR(36), source VARCHAR(32), score NUMERIC(5,2),
level VARCHAR(16), reasons CLOB, shown_at TIMESTAMP WITH TIME ZONE, action VARCHAR(32), action_at TIMESTAMP WITH TIME ZONE,
rule_version INT, created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE activity_registrations (
id VARCHAR(36) PRIMARY KEY, activity_id VARCHAR(36), user_id VARCHAR(36), status VARCHAR(32),
registered_at TIMESTAMP WITH TIME ZONE, reviewed_by VARCHAR(36), reviewed_at TIMESTAMP WITH TIME ZONE,
reject_reason VARCHAR(500), checkin_status VARCHAR(32), checkin_at TIMESTAMP WITH TIME ZONE,
emergency_contact_name VARCHAR(100), emergency_contact_phone_enc VARCHAR(500), source VARCHAR(64),
request_source VARCHAR(64), created_by_operator_id VARCHAR(36), user_confirmed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- Phase 15(V010):生产约束此前测试库完全没有 —— 约束漂移盲区(与 Phase 11/13/14 同族根因)。
-- 缺 UNIQUE(activity_id, user_id) 使「取消/拒绝后重新报名」撞唯一键的必崩缺陷在测试期完全不可见;
-- 缺两个 CHECK 使非法 status / checkin_status 只在 PostgreSQL 上炸。
CONSTRAINT uk_activity_registration UNIQUE (activity_id, user_id),
CONSTRAINT chk_reg_status CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
CONSTRAINT chk_checkin_status CHECK (checkin_status IN ('not_checked_in', 'checked_in'))
);
CREATE TABLE service_appointments (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), service_location_id VARCHAR(36), service_type VARCHAR(64),
preferred_date DATE, preferred_time_slot VARCHAR(32), user_note CLOB, status VARCHAR(32),
assigned_operator_id VARCHAR(36), confirmed_start_at TIMESTAMP WITH TIME ZONE,
confirmed_end_at TIMESTAMP WITH TIME ZONE, cancelled_by VARCHAR(36), cancel_reason VARCHAR(500),
source VARCHAR(64), request_source VARCHAR(64), created_by_operator_id VARCHAR(36),
user_confirmed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- Phase 19:生产 V021 的两条 CHECK 此前在测试库完全缺失 → 非法 status/source
-- 在测试期不可见,上到 PostgreSQL 才撞约束返回 500(Phase 11/13 同族约束漂移根因)。
CONSTRAINT chk_appointment_status CHECK (status IN (
'submitted', 'pending_confirmation', 'confirmed', 'completed', 'cancelled'
)),
CONSTRAINT chk_source_type CHECK (source IN (
'user_self', 'matchmaker_referral', 'activity_followup', 'service_station', 'operator_assisted'
))
);
CREATE TABLE counseling_services (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), service_type VARCHAR(64), request_source VARCHAR(64),
user_request_summary CLOB, organization_id VARCHAR(36), assigned_operator_id VARCHAR(36), status VARCHAR(32),
preferred_service_mode VARCHAR(32), preferred_time VARCHAR(128), scheduled_start_at TIMESTAMP WITH TIME ZONE,
scheduled_end_at TIMESTAMP WITH TIME ZONE, consent_version VARCHAR(32), consented_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE, closed_reason VARCHAR(500), companion_user_id VARCHAR(36),
created_by_operator_id VARCHAR(36), user_confirmed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
idempotency_key VARCHAR(128),
-- 生产约束(V022 + V039):此前测试库完全没有 CHECK → 约束漂移盲区(Phase 11/13 同族根因)
CONSTRAINT chk_counseling_service_type CHECK (service_type IN (
'relationship_guidance', 'emotional_support', 'premarital_guidance', 'anti_fraud_guidance'
)),
CONSTRAINT chk_counseling_status CHECK (status IN (
'pending', 'assigned', 'scheduled', 'in_progress', 'completed', 'cancelled', 'closed'
)),
CONSTRAINT chk_request_source CHECK (request_source IN (
'user_self', 'matchmaker_referral', 'activity_followup', 'service_station', 'operator_assisted'
)),
-- V039 幂等索引:H2 无 partial index,用标准 UNIQUE 表达(PG/H2 均允许多行 NULL 共存,语义等价)
CONSTRAINT uk_counseling_idem UNIQUE (user_id, idempotency_key)
);
-- V022 / V039: counseling_followups 表(跟进正文属 INV-013 敏感数据)
CREATE TABLE counseling_followups (
id VARCHAR(36) PRIMARY KEY, counseling_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL,
followup_type VARCHAR(32), content CLOB NOT NULL, result_code VARCHAR(64),
next_followup_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE
);
-- V026: service_programs 专项公益服务项目
-- Phase 19:此前仅在 TEST_SCHEMA_OPTIONAL_TABLES 白名单(= 无人测),
-- 导致 ServiceProgramService 全链路(创建/编辑/激活/成员审批)零覆盖。已补入并移出白名单。
CREATE TABLE service_programs (
id VARCHAR(36) PRIMARY KEY, name VARCHAR(128), description CLOB, cover_file_id VARCHAR(36),
organizer_org_id VARCHAR(36), start_at TIMESTAMP WITH TIME ZONE, end_at TIMESTAMP WITH TIME ZONE,
status VARCHAR(32), created_by VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- V026 生产约束
CONSTRAINT chk_program_status CHECK (status IN ('draft', 'active', 'ended', 'cancelled'))
);
-- V028: service_location_operators 服务站运营人员数据范围(复合主键,无 id 列)
-- Phase 19:此前在可选白名单且测试库缺失 → ServiceAppointmentMapper.canAccessByOperator /
-- findAccessibleByOperator 的 LEFT JOIN 在测试库直接 BadSqlGrammar,「预约数据范围」路径零覆盖。
CREATE TABLE service_location_operators (
service_location_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL, created_by VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (service_location_id, operator_id)
);
CREATE TABLE service_program_members (
id VARCHAR(36) PRIMARY KEY, program_id VARCHAR(36), user_id VARCHAR(36), status VARCHAR(32),
applied_at TIMESTAMP WITH TIME ZONE, approved_at TIMESTAMP WITH TIME ZONE, approved_by VARCHAR(36),
rejected_reason VARCHAR(500), created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- 对齐生产 V026:UNIQUE(program_id, user_id)。缺此约束时"被拒/退出后重新申请"
-- 撞唯一键 500 的必崩缺陷在测试期完全不可见(Phase 15 同族根因)。
CONSTRAINT uk_program_member UNIQUE (program_id, user_id),
-- 对齐生产 V026 chk_program_member_status:withdrawn 表示用户退出 / 运营移除
CONSTRAINT chk_program_member_status CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn'))
);
CREATE TABLE user_local_eligibilities (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) UNIQUE, eligibility_type VARCHAR(32),
declared_region_code VARCHAR(32), verification_status VARCHAR(32), review_task_id VARCHAR(36),
verified_at TIMESTAMP WITH TIME ZONE, verified_by VARCHAR(36), created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- 对齐生产 V017:两条 CHECK 此前在测试库完全缺失(与 Phase 19 D9/D23 同族约束漂移)。
-- 后果实证:AccountLifecycleService.invalidateLocalEligibility 曾把 user_identities 域的
-- 'failed' 写进本表的 verification_status —— 在测试库"合法"、在真实 PostgreSQL 上
-- 必然 CHECK 违约并使 closeAccount 整个事务回滚(注销 100% 不可用)。
CONSTRAINT chk_elig_type CHECK (eligibility_type IN (
'household_registration', 'residence', 'employment', 'other'
)),
CONSTRAINT chk_elig_verify_status CHECK (verification_status IN (
'declared', 'needs_evidence', 'pending_review', 'verified', 'rejected'
))
);
-- ============================================================================
-- 以下表为 Phase 12 复验补齐(测试保真度盲区):
-- 生产迁移里存在、而测试库此前**完全没有**,导致相关端到端路径无法测试。
-- 例:GET /api/v1/home 的 getHome() 会调用 UserIdentityMapper.findByUserId,
-- 测试库缺 user_identities → BadSqlGrammar("Table USER_IDENTITIES not found")
-- 整个首页接口在测试环境下不可用,任何首页回归都无法验证。
-- 列定义对齐对应迁移,仅保留实体实际读写的列。
-- ============================================================================
-- V002: user_identities 实名认证表
CREATE TABLE user_identities (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), real_name_enc CLOB, id_number_enc CLOB,
id_number_hash VARCHAR(128), gender VARCHAR(16), birth_date DATE,
verification_status VARCHAR(32), provider VARCHAR(64), provider_request_id VARCHAR(128),
verified_at TIMESTAMP WITH TIME ZONE, failed_reason_code VARCHAR(64),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- V003: review_tasks 审核任务(首页 buildUserState 需查询审核状态)
CREATE TABLE review_tasks (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), review_type VARCHAR(32), status VARCHAR(32),
assigned_operator_id VARCHAR(36), submitted_at TIMESTAMP WITH TIME ZONE,
started_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE,
final_comment VARCHAR(1000), version INT,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- V017: local_eligibility_evidences 本地资格佐证材料
CREATE TABLE local_eligibility_evidences (
id VARCHAR(36) PRIMARY KEY, local_eligibility_id VARCHAR(36), file_key VARCHAR(256),
file_id VARCHAR(36), evidence_type VARCHAR(64), status VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- V001 + V034 + V035: sensitive_access_logs 敏感数据访问审计
-- INV-011 的「可核查」落点正是本表(access_result = success / denied)。
-- 本表此前在测试库**完全不存在**,而 ReviewService.logEvidenceAccess 一定会写它 ——
-- 后果:任何「审计真的落库了吗」的断言都无从编写,INV-011 只能退化成常量断言;
-- 生产上新接入的审计路径在测试期零覆盖。与 Phase 12 复验记录的
-- user_identities / review_tasks 缺失同族(表漂移盲区),此处补齐。
-- 列定义对齐:V001 建表(id/operator_id/user_id/data_type/reason/ip/created_at)
-- + V034(access_target/access_result)
-- + V035(BaseEntity 的 updated_at / deleted_at)。
-- 不建 FK:生产 REFERENCES operators(id) / users(id),而测试库无 operators 表;
-- 本库 review_tasks / local_eligibility_evidences 同样只保留列、不建 FK。
CREATE TABLE sensitive_access_logs (
id VARCHAR(36) PRIMARY KEY, operator_id VARCHAR(36), user_id VARCHAR(36),
data_type VARCHAR(64), reason VARCHAR(500), ip VARCHAR(64),
access_target VARCHAR(128), access_result VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
);
-- V019: home_banners 首页 Banner
CREATE TABLE home_banners (
id VARCHAR(36) PRIMARY KEY, title VARCHAR(200), subtitle VARCHAR(300), image_file_id VARCHAR(36),
button_text VARCHAR(64), target_type VARCHAR(64), target_value VARCHAR(200),
audience_scope VARCHAR(64), sort_order INT, start_at TIMESTAMP WITH TIME ZONE,
end_at TIMESTAMP WITH TIME ZONE, status VARCHAR(32), created_by VARCHAR(36), updated_by VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- Phase 19:生产 V019 的三条 CHECK 此前在测试库完全缺失(Phase 11/13 同族约束漂移)。
-- target_type 白名单同时是"禁外链"的服务端落点:外链类型不在白名单内。
CONSTRAINT chk_banner_target_type CHECK (target_type IN (
'activity', 'matchmaker', 'counseling', 'premarital', 'service_location', 'article', 'internal_page', 'none'
)),
CONSTRAINT chk_banner_audience_scope CHECK (audience_scope IN (
'all', 'onboarding', 'searching', 'getting_to_know', 'successful'
)),
CONSTRAINT chk_banner_status CHECK (status IN (
'draft', 'scheduled', 'active', 'inactive', 'expired'
))
);
-- V019: service_locations 善缘服务站
CREATE TABLE service_locations (
id VARCHAR(36) PRIMARY KEY, name VARCHAR(128), short_name VARCHAR(64), address CLOB,
latitude DECIMAL(10, 7), longitude DECIMAL(10, 7), contact_phone_enc CLOB,
contact_phone_display VARCHAR(32), opening_hours_text CLOB, service_description CLOB,
cover_file_id VARCHAR(36), primary_organization_id VARCHAR(36), notice_text CLOB,
status VARCHAR(32), created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
-- Phase 19:生产 V019 的服务站状态 CHECK 此前在测试库缺失(同族约束漂移)
CONSTRAINT chk_service_location_status CHECK (status IN ('active', 'inactive', 'closed'))
);
-- V042: service_location_announcements 善缘服务站「临时公告」(Phase 19 / U50)
-- Phase 19:该表此前**完全缺失于测试库**,而公告是本轮为新链路所建 —— 若不补表,
-- 公告 Service 一旦被任何测试调用即 BadSqlGrammar(表不存在),且
-- EntityMigrationParityTest 的生产表缺失守卫会直接变红(本轮即由此发现)。
-- 对齐 V042:去外键(H2 无 operators 表),保留 chk_announcement_status 白名单
-- (「非法状态只在 PostgreSQL 上炸」的必崩盲区同族根因)。
CREATE TABLE service_location_announcements (
id VARCHAR(36) PRIMARY KEY, service_location_id VARCHAR(36) NOT NULL,
title VARCHAR(200) NOT NULL, content CLOB, status VARCHAR(32) NOT NULL DEFAULT 'draft',
published_at TIMESTAMP WITH TIME ZONE, expires_at TIMESTAMP WITH TIME ZONE, sort_order INT NOT NULL DEFAULT 0,
created_by VARCHAR(36), updated_by VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_announcement_status CHECK (status IN ('draft', 'published', 'archived'))
);
-- V026: content_articles 婚恋课堂文章
-- V026: content_categories 内容分类(运营后台「内容分类」tab 的唯一落点)
-- Phase 19:此前该表完全缺失于测试库,整条分类路径零覆盖(与 Phase 12 缺 user_identities
-- 导致首页零覆盖同族)。补表同时补生产 chk_category_status,避免非法状态在测试期不可见。
CREATE TABLE content_categories (
id VARCHAR(36) PRIMARY KEY, name VARCHAR(128) NOT NULL, code VARCHAR(64) NOT NULL,
description CLOB, sort_order INT, status VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- 对齐生产 V026:UNIQUE(code)。缺此约束时"编码重复"在测试期静默通过,生产 500。
CONSTRAINT uk_content_category_code UNIQUE (code),
CONSTRAINT chk_category_status CHECK (status IN ('active', 'inactive'))
);
CREATE TABLE content_articles (
id VARCHAR(36) PRIMARY KEY, category_id VARCHAR(36), title VARCHAR(200), summary VARCHAR(500),
content CLOB, cover_file_id VARCHAR(36), author_operator_id VARCHAR(36), status VARCHAR(32),
publish_at TIMESTAMP WITH TIME ZONE, view_count INT, sort_order INT,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- 对齐生产 V026 chk_article_status:此前测试库缺该 CHECK,非法状态只在 PostgreSQL 上炸
-- (Phase 11/13/14/15 同族约束漂移根因)。
CONSTRAINT chk_article_status CHECK (status IN ('draft', 'scheduled', 'published', 'offline'))
);
-- V010: activities 公益活动(首页近期活动预览)
CREATE TABLE activities (
id VARCHAR(36) PRIMARY KEY, title VARCHAR(200), cover_file_id VARCHAR(36), description CLOB,
organizer_org_id VARCHAR(36), coorganizer_text VARCHAR(500), start_at TIMESTAMP WITH TIME ZONE,
end_at TIMESTAMP WITH TIME ZONE, location_name VARCHAR(200), location_address VARCHAR(500),
male_limit INT, female_limit INT, total_limit INT, min_age SMALLINT, max_age SMALLINT,
registration_start_at TIMESTAMP WITH TIME ZONE, registration_end_at TIMESTAMP WITH TIME ZONE,
need_review BOOLEAN, status VARCHAR(32), created_by VARCHAR(36),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
-- V023(Phase 14):多类型活动扩展列。此前测试库完全缺失,
-- 导致 Phase 14 全部新增逻辑零覆盖,且非法 activity_type 在测试期不可见
-- (上到 PostgreSQL 才撞 chk_activity_type 返回 500)。
activity_type VARCHAR(64) NOT NULL DEFAULT 'matchmaking',
registration_policy CLOB,
target_group VARCHAR(200),
industry_scope VARCHAR(200),
post_event_interest_enabled BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT chk_activity_type CHECK (activity_type IN (
'matchmaking', 'interest_social', 'relationship_education',
'premarital_guidance', 'marriage_custom_culture', 'anti_fraud'
))
);
-- ============================================================================
-- Phase 14(V001 + V023):活动组织与容量分组
-- 此前测试库完全缺失 organizations / activity_organizations / activity_capacity_groups,
-- 且 activities 缺 V023 全部新列 —— Phase 14 新增逻辑零覆盖。
-- 约束与生产 V023 对齐(chk_org_role 白名单)。
-- ============================================================================
-- V001: organizations 组织表(activity_organizations 的外键目标,且组织名用于活动详情展示)
CREATE TABLE organizations (
id VARCHAR(36) PRIMARY KEY, name VARCHAR(200), type VARCHAR(32),
contact_name VARCHAR(100), contact_phone_enc VARCHAR(500), status VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- V023: activity_organizations 活动多组织关联表
CREATE TABLE activity_organizations (
id VARCHAR(36) PRIMARY KEY, activity_id VARCHAR(36) NOT NULL, organization_id VARCHAR(36) NOT NULL,
role VARCHAR(32) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT uk_activity_org UNIQUE (activity_id, organization_id),
CONSTRAINT chk_org_role CHECK (role IN ('host', 'organizer', 'co_organizer', 'partner', 'venue_partner'))
);
-- V023: activity_capacity_groups 活动容量分组表
CREATE TABLE activity_capacity_groups (
id VARCHAR(36) PRIMARY KEY, activity_id VARCHAR(36) NOT NULL,
group_code VARCHAR(32) NOT NULL, rule_definition CLOB,
capacity INT NOT NULL DEFAULT 0, approved_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT uk_capacity_group UNIQUE (activity_id, group_code)
);
-- V040: activity_staff_assignments 活动工作人员指派表(Phase 15 签到数据范围)
-- 主办/协办组织关系不授予操作权限(INV-020),签到必须来自显式指派。
CREATE TABLE activity_staff_assignments (
activity_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL,
assignment_role VARCHAR(32) NOT NULL DEFAULT 'checkin_staff',
created_by VARCHAR(36), created_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (activity_id, operator_id),
CONSTRAINT chk_activity_staff_role CHECK (assignment_role IN ('checkin_staff', 'activity_manager'))
);
-- ============================================================================
-- Phase 13(V009 + V021 + V037 + V038):红娘服务三表
-- 此前测试库完全缺失,导致 matchmaker 模块零真实行为覆盖、只能写假测试。
-- 约束与生产 V038 对齐(status 白名单已含 liked/passed/confirmed_mutual)。
-- ============================================================================
-- V009: matchmaker_services 红娘服务表
CREATE TABLE matchmaker_services (
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, service_type VARCHAR(32) NOT NULL,
description VARCHAR(1000), status VARCHAR(32) NOT NULL DEFAULT 'submitted',
organization_id VARCHAR(36), assigned_operator_id VARCHAR(36),
assigned_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE,
referral_target_type VARCHAR(32), referral_target_id VARCHAR(36),
referral_source_type VARCHAR(32) NOT NULL DEFAULT 'manual',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_matchmaker_service_type CHECK (service_type IN (
'recommend_partner', 'review_preference', 'contact_intention',
'join_activity', 'no_match_help', 'chat_help', 'other',
'recommendation',
'referral_counseling', 'referral_activity', 'referral_station'
)),
CONSTRAINT chk_matchmaker_status CHECK (status IN (
'submitted', 'assigned', 'in_service', 'waiting_user', 'completed', 'cancelled'
))
);
-- V009: matchmaker_recommendations 红娘推荐表
CREATE TABLE matchmaker_recommendations (
id VARCHAR(36) PRIMARY KEY, service_id VARCHAR(36) NOT NULL, user_id VARCHAR(36) NOT NULL,
target_user_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL, reason VARCHAR(1000),
status VARCHAR(32) NOT NULL DEFAULT 'pending_view',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
viewed_at TIMESTAMP WITH TIME ZONE, responded_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_rec_status CHECK (status IN (
'pending_view', 'viewed', 'responded',
'liked', 'passed', 'confirmed_mutual'
))
);
-- V009: matchmaker_followups 红娘跟进表
CREATE TABLE matchmaker_followups (
id VARCHAR(36) PRIMARY KEY, service_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL,
followup_type VARCHAR(32), content VARCHAR(2000) NOT NULL, result_code VARCHAR(64),
next_followup_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Phase 17:举报与风控三表(与 V012__add_risk_and_report_tables.sql 生产 DDL 对齐)。
-- 此前完全缺失 → ReportService / RiskReferralService 整条真实 DB 路径零覆盖,
-- 且 ReportServiceTest 只能写 assertTrue(true) 假绿。已补入并移出 TEST_SCHEMA_OPTIONAL_TABLES 白名单。
-- 注意:H2 无 operators 表,故去掉生产 DDL 中的 REFERENCES operators(id) 外键;
-- 保留 status / action_type 的 CHECK 约束,防止非法取值在测试期被静默放过(同 Phase 14/15/16 原则)。
CREATE TABLE reports (
id VARCHAR(36) PRIMARY KEY,
reporter_user_id VARCHAR(36) NOT NULL,
reported_user_id VARCHAR(36),
reason_type VARCHAR(64) NOT NULL,
description CLOB,
evidence_file_ids CLOB,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
process_result VARCHAR(64),
processed_by VARCHAR(36),
processed_at TIMESTAMP WITH TIME ZONE,
process_note CLOB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_report_status CHECK (status IN ('pending', 'processing', 'resolved', 'rejected', 'dismissed'))
);
CREATE TABLE risk_events (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
event_type VARCHAR(64) NOT NULL,
score INT NOT NULL DEFAULT 0,
source VARCHAR(32) NOT NULL DEFAULT 'auto',
source_id VARCHAR(128),
note CLOB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE risk_actions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
action_type VARCHAR(64) NOT NULL,
reason VARCHAR(256) NOT NULL,
reason_detail CLOB,
created_by VARCHAR(36) NOT NULL,
effective_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE,
status VARCHAR(32) NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_risk_action_type CHECK (action_type IN ('warning', 'feature_limit', 'temporary_suspend', 'ban')),
CONSTRAINT chk_risk_action_status CHECK (status IN ('active', 'resolved', 'expired'))
);
-- Phase 18: appeals 表(申诉)—— 此前完全缺失于测试库(只列在 EntityMigrationParityTest
-- 的可选白名单里 = 无人测),导致申诉整条真实 DB 路径零覆盖,AppealServiceTest 只能写
-- assertTrue(true) 假绿。对齐 V013__add_appeals_table.sql:去外键(H2 无 operators 表),
-- 保留 chk_appeal_status / chk_appeal_type 两个白名单(「只在 PG 上炸」的必崩盲区)。
CREATE TABLE appeals (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
appeal_type VARCHAR(64) NOT NULL,
related_action_id VARCHAR(36),
reason CLOB NOT NULL,
evidence CLOB,
contact_method VARCHAR(256),
status VARCHAR(32) NOT NULL DEFAULT 'pending',
decision_note CLOB,
reviewed_by VARCHAR(36),
reviewed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_appeal_status CHECK (status IN ('pending', 'reviewing', 'approved', 'rejected', 'escalated')),
CONSTRAINT chk_appeal_type CHECK (appeal_type IN ('ban_appeal', 'suspend_appeal'))
);
-- Phase 24/幂等检修: idempotency_records(通用 Idempotency-Key 落地表)
-- 对齐 V045__add_idempotency_records.sql。此前该表不存在于测试库,
-- 任何走幂等拦截器的写操作在 H2 下都会报表不存在 —— 与 appeals 当初
-- 整条真实 DB 路径零覆盖是同一类盲区。
CREATE TABLE idempotency_records (
id VARCHAR(36) PRIMARY KEY,
actor_id VARCHAR(64) NOT NULL,
idempotency_key VARCHAR(255) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
response_status INTEGER,
response_body CLOB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT chk_idempotency_state CHECK (state IN ('PROCESSING', 'COMPLETED'))
);
-- ---------------------------------------------------------------------------
-- Phase 23 补齐(2026-09-19):以下 4 张表此前在测试库完全不存在。
-- 发现经过:MapperBindingGuardTest 报出 5 个「声明了却无 @Select」的方法,
-- 其中 4 个方法对应的表正是这 4 张 —— 表不存在 + 语句未绑定 = 双重盲区,
-- 任何测试都走不到这些代码路径,缺陷被"空数据 + 缺表"共同掩盖。
-- 保真口径:**按 V002 + V037 之后的最终状态**建模,不是按 V002 的初始建表。
-- V037(align_entity_columns_with_migrations)已给这 4 张表补上 BaseEntity 公共列
-- (id / created_at / updated_at / deleted_at)与匹配权重列 ——
-- 若只看 V002 会误判为「表主键是 user_id、无 id 列」,进而写出错误的 SQL。
-- 尤需注意 deleted_at:4 张表**全部**已有软删列,故相关查询必须带 deleted_at IS NULL。
-- ---------------------------------------------------------------------------
-- V002 + V037: user_preferences 择偶偏好
-- V002 初始:user_id 为主键,无 id / deleted_at / 权重列
-- V037 补齐:id + deleted_at + 11 个匹配权重列
CREATE TABLE user_preferences (
user_id VARCHAR(36) PRIMARY KEY, id VARCHAR(36),
min_age INT, max_age INT, age_priority VARCHAR(16),
min_height_cm INT, max_height_cm INT, height_priority VARCHAR(16),
region_scope CLOB, region_priority VARCHAR(16),
marital_statuses CLOB, marital_priority VARCHAR(16),
min_education_code VARCHAR(32), education_priority VARCHAR(16),
children_acceptance VARCHAR(32), children_priority VARCHAR(16),
smoking_preference VARCHAR(32), drinking_preference VARCHAR(32),
settlement_preference VARCHAR(32), marriage_plan_preference VARCHAR(32),
age_weight DOUBLE PRECISION DEFAULT 0.15, region_weight DOUBLE PRECISION DEFAULT 0.20,
marital_weight DOUBLE PRECISION DEFAULT 0.10, children_weight DOUBLE PRECISION DEFAULT 0.05,
education_weight DOUBLE PRECISION DEFAULT 0.10, height_weight DOUBLE PRECISION DEFAULT 0.05,
marriage_plan_weight DOUBLE PRECISION DEFAULT 0.15, lifestyle_weight DOUBLE PRECISION DEFAULT 0.10,
settlement_weight DOUBLE PRECISION DEFAULT 0.05, interest_weight DOUBLE PRECISION DEFAULT 0.10,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_pref_priority CHECK (age_priority IN ('required', 'preferred'))
);
-- V002 + V037: user_privacy_settings 隐私设置
-- V037 补齐:id + deleted_at(V002 时无此两列)
CREATE TABLE user_privacy_settings (
user_id VARCHAR(36) PRIMARY KEY, id VARCHAR(36),
visible_in_recommendations BOOLEAN DEFAULT TRUE, visible_in_search BOOLEAN DEFAULT TRUE,
show_occupation BOOLEAN DEFAULT TRUE, show_lifestyle_photos BOOLEAN DEFAULT TRUE,
receive_activity_recommendations BOOLEAN DEFAULT TRUE,
accept_matchmaker_recommendations BOOLEAN DEFAULT TRUE,
-- V047:小程序 privacy-settings 契约列。注意 show_last_online 默认 FALSE(不公开在线状态)
allow_activity_invitation BOOLEAN DEFAULT TRUE,
show_last_online BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- V003 + V037: review_reasons 审核原因字典
-- 注意主键是 code(业务以 code 引用),V037 额外补 id 列以承接 BaseEntity 语义。
-- V037 补:id + created_at + updated_at + deleted_at
CREATE TABLE review_reasons (
code VARCHAR(64) PRIMARY KEY, id VARCHAR(36), name VARCHAR(128),
category VARCHAR(32), enabled BOOLEAN DEFAULT TRUE, sort_order INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE
);
-- V003 + V037: review_items 审核项
-- V037 补:deleted_at
CREATE TABLE review_items (
id VARCHAR(36) PRIMARY KEY, review_task_id VARCHAR(36),
item_type VARCHAR(32), status VARCHAR(32) DEFAULT 'pending',
reason_code VARCHAR(64), comment VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_item_type CHECK (item_type IN ('avatar', 'lifestyle_photos', 'basic_profile', 'self_intro', 'relationship_profile')),
CONSTRAINT chk_item_status CHECK (status IN ('pending', 'approved', 'need_edit', 'abnormal'))
);
CREATE UNIQUE INDEX uk_idempotency_scope
ON idempotency_records (actor_id, endpoint, idempotency_key);