From 9ed536e206dfad799996ce290fb7eae6ce5c1aeb Mon Sep 17 00:00:00 2001 From: Windows Date: Fri, 11 Sep 2026 12:16:06 +0800 Subject: [PATCH] feat: migrate advisor database schema to qyqy base --- .../20260910_advisor_data_quality_backtest.py | 77 +++++++++++ ...0_advisor_governance_monitor_and_quotes.py | 106 +++++++++++++++ .../20260910_advisor_investment_goal.py | 69 ++++++++++ ...0260910_advisor_market_quote_resilience.py | 92 +++++++++++++ ...20260910_advisor_offsite_fund_reference.py | 48 +++++++ ...advisor_portfolio_projection_checkpoint.py | 35 +++++ ...10_advisor_product_asset_classification.py | 44 +++++++ ...10_advisor_product_governance_reference.py | 99 ++++++++++++++ ...60910_advisor_product_industry_exposure.py | 48 +++++++ ...0260910_advisor_product_metric_snapshot.py | 44 +++++++ .../20260910_advisor_product_price_history.py | 47 +++++++ ...0910_advisor_product_reference_snapshot.py | 43 ++++++ .../20260911_advisor_goal_conversation.py | 42 ++++++ ...20260911_advisor_profile_tag_governance.py | 122 ++++++++++++++++++ docs/21-投顾Agent迁移TODO.md | 63 +++++---- tests/unit/test_advisor_migration_contract.py | 64 +++++++++ tools/audit_schema.py | 22 ++++ 17 files changed, 1040 insertions(+), 25 deletions(-) create mode 100644 alembic/versions/20260910_advisor_data_quality_backtest.py create mode 100644 alembic/versions/20260910_advisor_governance_monitor_and_quotes.py create mode 100644 alembic/versions/20260910_advisor_investment_goal.py create mode 100644 alembic/versions/20260910_advisor_market_quote_resilience.py create mode 100644 alembic/versions/20260910_advisor_offsite_fund_reference.py create mode 100644 alembic/versions/20260910_advisor_portfolio_projection_checkpoint.py create mode 100644 alembic/versions/20260910_advisor_product_asset_classification.py create mode 100644 alembic/versions/20260910_advisor_product_governance_reference.py create mode 100644 alembic/versions/20260910_advisor_product_industry_exposure.py create mode 100644 alembic/versions/20260910_advisor_product_metric_snapshot.py create mode 100644 alembic/versions/20260910_advisor_product_price_history.py create mode 100644 alembic/versions/20260910_advisor_product_reference_snapshot.py create mode 100644 alembic/versions/20260911_advisor_goal_conversation.py create mode 100644 alembic/versions/20260911_advisor_profile_tag_governance.py create mode 100644 tests/unit/test_advisor_migration_contract.py diff --git a/alembic/versions/20260910_advisor_data_quality_backtest.py b/alembic/versions/20260910_advisor_data_quality_backtest.py new file mode 100644 index 0000000..1583406 --- /dev/null +++ b/alembic/versions/20260910_advisor_data_quality_backtest.py @@ -0,0 +1,77 @@ +"""add advisory market-data quality and allocation backtest records + +Compatibility proof: this revision creates only the additive +``advisor_product_data_quality_snapshot`` and ``advisor_allocation_backtest_run`` +tables. It does not alter, rename, delete, reuse, or retype a baseline table +or an existing field. +""" + +from alembic import op + +revision = "20260910_adv_quality_backtest" +down_revision = "20260910_adv_quote_resilience" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_data_quality_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + as_of_date DATE NOT NULL, + observation_count BIGINT UNSIGNED NOT NULL, + expected_trading_days BIGINT UNSIGNED NOT NULL, + price_coverage_pct DECIMAL(7,4) NOT NULL, + turnover_coverage_pct DECIMAL(7,4) NOT NULL, + max_abs_daily_return_pct DECIMAL(10,4) NULL, + status VARCHAR(16) NOT NULL, + reason_codes JSON NOT NULL, + rule_version VARCHAR(16) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_data_quality_snapshot (product_id, as_of_date), + KEY idx_advisor_product_data_quality_latest (product_id, status, as_of_date), + CONSTRAINT chk_advisor_product_data_quality_status + CHECK (status IN ('accepted', 'rejected')), + CONSTRAINT fk_advisor_product_data_quality_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_allocation_backtest_run ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + backtest_no VARCHAR(36) NOT NULL, + started_on DATE NOT NULL, + ended_on DATE NOT NULL, + profile_risk_level VARCHAR(8) NOT NULL, + return_target_lower_pct DECIMAL(7,4) NOT NULL, + max_drawdown_pct DECIMAL(7,4) NOT NULL, + liquidity_requirement VARCHAR(32) NOT NULL, + status VARCHAR(32) NOT NULL, + observation_count BIGINT UNSIGNED NOT NULL, + static_total_return_pct DECIMAL(12,4) NULL, + dynamic_total_return_pct DECIMAL(12,4) NULL, + static_max_drawdown_pct DECIMAL(12,4) NULL, + dynamic_max_drawdown_pct DECIMAL(12,4) NULL, + dynamic_rebalance_count BIGINT UNSIGNED NOT NULL, + liquidity_history_coverage_pct DECIMAL(7,4) NOT NULL, + limitations JSON NOT NULL, + strategy_version VARCHAR(32) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_allocation_backtest_run (backtest_no), + KEY idx_advisor_allocation_backtest_lookup (status, ended_on, created_at), + CONSTRAINT chk_advisor_allocation_backtest_status + CHECK (status IN ('ready', 'partial', 'data_quality_required', 'insufficient_history')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory quality and backtest records must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_governance_monitor_and_quotes.py b/alembic/versions/20260910_advisor_governance_monitor_and_quotes.py new file mode 100644 index 0000000..7209b36 --- /dev/null +++ b/alembic/versions/20260910_advisor_governance_monitor_and_quotes.py @@ -0,0 +1,106 @@ +"""add product governance monitoring and exchange quote snapshots + +Compatibility proof: this revision creates three additive advisory tables only. +No baseline table or existing baseline field is altered, renamed, deleted, +reused, or retyped. +""" + +from alembic import op + +revision = "20260910_adv_monitor_quotes" +down_revision = "20260910_adv_product_governance" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_governance_candidate_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + data_kind VARCHAR(16) NOT NULL, + sales_institution VARCHAR(128) NULL, + observed_at DATETIME(6) NOT NULL, + effective_from DATE NOT NULL, + risk_level VARCHAR(8) NULL, + payload JSON NOT NULL, + source_url VARCHAR(1024) NOT NULL, + document_title VARCHAR(256) NOT NULL, + document_published_at DATE NULL, + document_sha256 CHAR(64) NOT NULL, + source VARCHAR(64) NOT NULL, + review_status VARCHAR(16) NOT NULL, + reviewed_by BIGINT UNSIGNED NULL, + reviewed_at DATETIME(6) NULL, + review_comment VARCHAR(1000) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_governance_candidate + (product_id, data_kind, document_sha256), + KEY idx_advisor_product_governance_review + (review_status, data_kind, observed_at DESC, product_id), + CONSTRAINT chk_advisor_product_governance_candidate_kind + CHECK (data_kind IN ('suitability', 'contract')), + CONSTRAINT chk_advisor_product_governance_candidate_risk + CHECK (risk_level IS NULL OR risk_level IN ('R1', 'R2', 'R3', 'R4', 'R5')), + CONSTRAINT chk_advisor_product_governance_candidate_status + CHECK (review_status IN ('pending_review', 'approved', 'rejected')), + CONSTRAINT fk_advisor_product_governance_candidate_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_product_governance_sync_run ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + run_no VARCHAR(36) NOT NULL, + source VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + product_count BIGINT UNSIGNED NOT NULL, + change_count BIGINT UNSIGNED NOT NULL, + error_count BIGINT UNSIGNED NOT NULL, + detail JSON NOT NULL, + started_at DATETIME(6) NOT NULL, + completed_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_governance_sync_run (run_no), + KEY idx_advisor_product_governance_sync_run (status, started_at DESC), + CONSTRAINT chk_advisor_product_governance_sync_status + CHECK (status IN ('running', 'succeeded', 'failed')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_product_market_quote_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + observed_at DATETIME(6) NOT NULL, + last_price DECIMAL(18,6) NOT NULL, + previous_close DECIMAL(18,6) NULL, + change_pct DECIMAL(10,4) NULL, + volume DECIMAL(24,4) NULL, + turnover_amount DECIMAL(24,2) NULL, + quote_status VARCHAR(16) NOT NULL, + source VARCHAR(64) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + KEY idx_advisor_product_market_quote_latest (product_id, observed_at DESC), + CONSTRAINT chk_advisor_product_market_quote_price CHECK (last_price > 0), + CONSTRAINT chk_advisor_product_market_quote_status + CHECK (quote_status IN ('active', 'stale')), + CONSTRAINT fk_advisor_product_market_quote_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory governance and quote records must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_investment_goal.py b/alembic/versions/20260910_advisor_investment_goal.py new file mode 100644 index 0000000..4adbde1 --- /dev/null +++ b/alembic/versions/20260910_advisor_investment_goal.py @@ -0,0 +1,69 @@ +"""add advisory investment goal table + +Compatibility proof: this revision only creates ``advisor_investment_goal``. It does not +alter, rename, delete, or reuse any table or field defined by the immutable baseline. +``client_facing_content`` is referenced as its existing, unchanged content-review resource. +""" + +from alembic import op + +revision = "20260910_advisor_investment_goal" +down_revision = "20260910_drop_review_separation" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_investment_goal ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + goal_no VARCHAR(36) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + status VARCHAR(24) NOT NULL, + annualized_return_lower_pct DECIMAL(7,4) NOT NULL, + annualized_return_upper_pct DECIMAL(7,4) NOT NULL, + max_drawdown_pct DECIMAL(7,4) NOT NULL, + liquidity_requirement VARCHAR(32) NOT NULL, + investment_horizon_months INT UNSIGNED NOT NULL, + benchmark_name VARCHAR(128) NOT NULL, + notes TEXT NULL, + source VARCHAR(16) NOT NULL, + goal_book_content_id BIGINT UNSIGNED NOT NULL, + created_by BIGINT UNSIGNED NOT NULL, + confirmed_by BIGINT UNSIGNED NULL, + confirmed_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_investment_goal_no (goal_no), + UNIQUE KEY uk_advisor_investment_goal_book (goal_book_content_id), + KEY idx_advisor_investment_goal_customer_status (customer_id, status, created_at), + CONSTRAINT chk_advisor_investment_goal_status + CHECK (status IN ('pending_confirmation', 'confirmed', 'superseded')), + CONSTRAINT chk_advisor_investment_goal_return_range + CHECK (annualized_return_lower_pct <= annualized_return_upper_pct), + CONSTRAINT chk_advisor_investment_goal_return_lower + CHECK (annualized_return_lower_pct BETWEEN 0 AND 100), + CONSTRAINT chk_advisor_investment_goal_return_upper + CHECK (annualized_return_upper_pct BETWEEN 0 AND 100), + CONSTRAINT chk_advisor_investment_goal_drawdown + CHECK (max_drawdown_pct BETWEEN 0 AND 100), + CONSTRAINT chk_advisor_investment_goal_horizon + CHECK (investment_horizon_months BETWEEN 1 AND 600), + CONSTRAINT fk_advisor_investment_goal_customer + FOREIGN KEY (customer_id) REFERENCES sys_user(id), + CONSTRAINT fk_advisor_investment_goal_book + FOREIGN KEY (goal_book_content_id) REFERENCES client_facing_content(id), + CONSTRAINT fk_advisor_investment_goal_creator + FOREIGN KEY (created_by) REFERENCES sys_user(id), + CONSTRAINT fk_advisor_investment_goal_confirmer + FOREIGN KEY (confirmed_by) REFERENCES sys_user(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("投资目标记录不得自动删除,请使用前向兼容迁移") + diff --git a/alembic/versions/20260910_advisor_market_quote_resilience.py b/alembic/versions/20260910_advisor_market_quote_resilience.py new file mode 100644 index 0000000..f00a394 --- /dev/null +++ b/alembic/versions/20260910_advisor_market_quote_resilience.py @@ -0,0 +1,92 @@ +"""add resilient market quote source monitoring + +Compatibility proof: this revision creates additive advisory market-data +tables only. It does not alter, rename, remove, reuse, or retype any baseline +table or field. +""" + +from alembic import op + +revision = "20260910_adv_quote_resilience" +down_revision = "20260910_adv_monitor_quotes" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_market_quote_source_run ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + sync_run_no VARCHAR(36) NOT NULL, + source VARCHAR(64) NOT NULL, + priority_order TINYINT UNSIGNED NOT NULL, + status VARCHAR(16) NOT NULL, + requested_count BIGINT UNSIGNED NOT NULL, + quote_count BIGINT UNSIGNED NOT NULL, + error_type VARCHAR(128) NULL, + started_at DATETIME(6) NOT NULL, + completed_at DATETIME(6) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + KEY idx_advisor_market_quote_source_run_source (source, created_at DESC), + KEY idx_advisor_market_quote_source_run_sync (sync_run_no, priority_order), + CONSTRAINT chk_advisor_market_quote_source_run_status + CHECK (status IN ('succeeded', 'degraded', 'failed')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_market_quote_source_health ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + source VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + consecutive_failure_count BIGINT UNSIGNED NOT NULL, + last_success_at DATETIME(6) NULL, + last_failure_at DATETIME(6) NULL, + last_error_type VARCHAR(128) NULL, + last_requested_count BIGINT UNSIGNED NOT NULL, + last_quote_count BIGINT UNSIGNED NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_market_quote_source_health_source (source), + KEY idx_advisor_market_quote_source_health_status (status, updated_at DESC), + CONSTRAINT chk_advisor_market_quote_source_health_status + CHECK (status IN ('healthy', 'degraded', 'failed')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_market_quote_alert ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + alert_no VARCHAR(36) NOT NULL, + source VARCHAR(64) NOT NULL, + alert_type VARCHAR(64) NOT NULL, + severity VARCHAR(16) NOT NULL, + status VARCHAR(16) NOT NULL, + occurrence_count BIGINT UNSIGNED NOT NULL, + detail JSON NOT NULL, + first_observed_at DATETIME(6) NOT NULL, + last_observed_at DATETIME(6) NOT NULL, + resolved_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_market_quote_alert_no (alert_no), + KEY idx_advisor_market_quote_alert_open + (status, source, alert_type, last_observed_at DESC), + CONSTRAINT chk_advisor_market_quote_alert_status + CHECK (status IN ('open', 'resolved')), + CONSTRAINT chk_advisor_market_quote_alert_severity + CHECK (severity IN ('medium', 'high')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("market-data health records must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_offsite_fund_reference.py b/alembic/versions/20260910_advisor_offsite_fund_reference.py new file mode 100644 index 0000000..6401b3e --- /dev/null +++ b/alembic/versions/20260910_advisor_offsite_fund_reference.py @@ -0,0 +1,48 @@ +"""add offsite fund reference catalogue + +Compatibility proof: this revision only creates the additive +``advisor_offsite_fund_reference`` table. It does not alter, rename, delete, +reuse, or change the definition of any baseline table or field. The table is +reference-only and is intentionally isolated from exchange-traded simulation +tables. +""" + +from alembic import op + +revision = "20260910_adv_offsite_ref" +down_revision = "20260910_adv_product_ref" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_offsite_fund_reference ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + fund_code VARCHAR(32) NOT NULL, + fund_name VARCHAR(128) NOT NULL, + fund_type VARCHAR(64) NOT NULL, + risk_level VARCHAR(8) NOT NULL, + current_nav DECIMAL(18,6) NULL, + current_nav_at DATETIME NULL, + fund_asset_scale_billion DECIMAL(18,4) NULL, + scale_as_of_date DATE NULL, + source VARCHAR(64) NOT NULL, + risk_mapping_version VARCHAR(32) NOT NULL, + status VARCHAR(16) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_offsite_fund_reference_code (fund_code), + KEY idx_advisor_offsite_fund_reference_risk (risk_level, status), + CONSTRAINT chk_advisor_offsite_fund_reference_risk + CHECK (risk_level IN ('R1', 'R2', 'R3', 'R4', 'R5')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("offsite fund reference catalogue must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_portfolio_projection_checkpoint.py b/alembic/versions/20260910_advisor_portfolio_projection_checkpoint.py new file mode 100644 index 0000000..0547274 --- /dev/null +++ b/alembic/versions/20260910_advisor_portfolio_projection_checkpoint.py @@ -0,0 +1,35 @@ +"""add advisory portfolio graph projection checkpoint + +Compatibility proof: this revision only creates the additive +``advisor_portfolio_projection_checkpoint`` table. It does not alter, rename, +delete, reuse, or change the definition of any baseline table or field. +""" + +from alembic import op + +revision = "20260910_adv_projection_ckpt" +down_revision = "20260910_adv_industry_exp" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_portfolio_projection_checkpoint ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + customer_id BIGINT UNSIGNED NOT NULL, + holding_updated_at DATETIME(6) NULL, + exposure_updated_at DATETIME(6) NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_portfolio_projection_customer (customer_id), + KEY idx_advisor_portfolio_projection_updated (updated_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("portfolio graph projection checkpoints must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_product_asset_classification.py b/alembic/versions/20260910_advisor_product_asset_classification.py new file mode 100644 index 0000000..fd1da64 --- /dev/null +++ b/alembic/versions/20260910_advisor_product_asset_classification.py @@ -0,0 +1,44 @@ +"""add advisory product asset classification snapshots + +Compatibility proof: this revision only creates the additive +``advisor_product_asset_classification`` table. No baseline table or field is +altered, renamed, deleted, reused, or retyped. +""" + +from alembic import op + +revision = "20260910_adv_asset_class" +down_revision = "20260910_adv_metric_snapshot" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_asset_classification ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + as_of_date DATE NOT NULL, + asset_class VARCHAR(32) NOT NULL, + source VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_asset_classification (product_id, as_of_date), + KEY idx_advisor_product_asset_classification_current (product_id, status, as_of_date), + CONSTRAINT chk_advisor_product_asset_classification_class + CHECK (asset_class IN ('cash_management_etf', 'bond_etf', 'equity_etf')), + CONSTRAINT chk_advisor_product_asset_classification_status + CHECK (status IN ('active', 'superseded')), + CONSTRAINT fk_advisor_product_asset_classification_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory asset classifications must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_product_governance_reference.py b/alembic/versions/20260910_advisor_product_governance_reference.py new file mode 100644 index 0000000..14d8979 --- /dev/null +++ b/alembic/versions/20260910_advisor_product_governance_reference.py @@ -0,0 +1,99 @@ +"""add authoritative advisory product governance references + +Compatibility proof: this revision only creates the additive +``advisor_product_suitability_reference`` and +``advisor_product_contract_snapshot`` tables. No baseline table or baseline +field is altered, renamed, deleted, reused, or retyped. +""" + +from alembic import op + +revision = "20260910_adv_product_governance" +down_revision = "20260910_adv_price_history" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_suitability_reference ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + sales_institution VARCHAR(128) NOT NULL, + risk_level VARCHAR(8) NOT NULL, + effective_from DATE NOT NULL, + effective_until DATE NULL, + source_url VARCHAR(1024) NOT NULL, + document_title VARCHAR(256) NOT NULL, + document_published_at DATE NULL, + document_sha256 CHAR(64) NOT NULL, + source VARCHAR(64) NOT NULL, + review_status VARCHAR(16) NOT NULL, + verified_by VARCHAR(128) NULL, + verified_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_suitability_reference + (product_id, sales_institution, effective_from), + KEY idx_advisor_product_suitability_lookup + (sales_institution, review_status, effective_from, effective_until, product_id), + CONSTRAINT chk_advisor_product_suitability_risk + CHECK (risk_level IN ('R1', 'R2', 'R3', 'R4', 'R5')), + CONSTRAINT chk_advisor_product_suitability_period + CHECK (effective_until IS NULL OR effective_until >= effective_from), + CONSTRAINT chk_advisor_product_suitability_review + CHECK (review_status IN ('pending_review', 'verified', 'superseded', 'test_only')), + CONSTRAINT fk_advisor_product_suitability_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE advisor_product_contract_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + effective_from DATE NOT NULL, + effective_until DATE NULL, + fund_type VARCHAR(64) NOT NULL, + investment_scope TEXT NOT NULL, + performance_benchmark VARCHAR(256) NULL, + risk_return_characteristics TEXT NOT NULL, + custodian_name VARCHAR(128) NULL, + management_fee_rate_pct DECIMAL(9,6) NULL, + custodian_fee_rate_pct DECIMAL(9,6) NULL, + inception_date DATE NULL, + source_url VARCHAR(1024) NOT NULL, + document_title VARCHAR(256) NOT NULL, + document_published_at DATE NULL, + document_sha256 CHAR(64) NOT NULL, + source VARCHAR(64) NOT NULL, + review_status VARCHAR(16) NOT NULL, + verified_by VARCHAR(128) NULL, + verified_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_contract_snapshot (product_id, effective_from), + KEY idx_advisor_product_contract_lookup + (product_id, review_status, effective_from, effective_until), + CONSTRAINT chk_advisor_product_contract_period + CHECK (effective_until IS NULL OR effective_until >= effective_from), + CONSTRAINT chk_advisor_product_contract_review + CHECK (review_status IN ('pending_review', 'verified', 'superseded', 'test_only')), + CONSTRAINT chk_advisor_product_contract_management_fee + CHECK (management_fee_rate_pct IS NULL OR management_fee_rate_pct >= 0), + CONSTRAINT chk_advisor_product_contract_custodian_fee + CHECK (custodian_fee_rate_pct IS NULL OR custodian_fee_rate_pct >= 0), + CONSTRAINT fk_advisor_product_contract_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("authoritative product governance references must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_product_industry_exposure.py b/alembic/versions/20260910_advisor_product_industry_exposure.py new file mode 100644 index 0000000..d3d354a --- /dev/null +++ b/alembic/versions/20260910_advisor_product_industry_exposure.py @@ -0,0 +1,48 @@ +"""add advisor product industry exposure reference table + +Compatibility proof: this revision only creates ``advisor_product_industry_exposure``. +It does not alter, rename, delete, or reuse any baseline table or field. The table +references unchanged ``fin_product.id`` and is read by advisory analysis only. +""" + +from alembic import op + +revision = "20260910_adv_industry_exp" +down_revision = "20260910_advisor_investment_goal" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_industry_exposure ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + industry_code VARCHAR(32) NOT NULL, + industry_name VARCHAR(128) NOT NULL, + exposure_weight_pct DECIMAL(7,4) NOT NULL, + as_of_date DATE NOT NULL, + source VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_industry_exposure_snapshot + (product_id, industry_code, as_of_date), + KEY idx_advisor_product_industry_exposure_current + (product_id, status, as_of_date), + CONSTRAINT chk_advisor_product_industry_exposure_weight + CHECK (exposure_weight_pct >= 0 AND exposure_weight_pct <= 100), + CONSTRAINT chk_advisor_product_industry_exposure_status + CHECK (status IN ('active', 'superseded')), + CONSTRAINT fk_advisor_product_industry_exposure_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("产品行业暴露参考数据不得自动删除,请使用前向兼容迁移") + diff --git a/alembic/versions/20260910_advisor_product_metric_snapshot.py b/alembic/versions/20260910_advisor_product_metric_snapshot.py new file mode 100644 index 0000000..209cea2 --- /dev/null +++ b/alembic/versions/20260910_advisor_product_metric_snapshot.py @@ -0,0 +1,44 @@ +"""add advisory product metric snapshots + +Compatibility proof: this revision only creates the additive +``advisor_product_metric_snapshot`` table. It does not alter, rename, delete, +reuse, or change the definition of any baseline table or field. +""" + +from alembic import op + +revision = "20260910_adv_metric_snapshot" +down_revision = "20260910_adv_projection_ckpt" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_metric_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + as_of_date DATE NOT NULL, + trailing_20d_return_pct DECIMAL(10,4) NULL, + trailing_120d_return_pct DECIMAL(10,4) NULL, + annualized_volatility_pct DECIMAL(10,4) NULL, + max_drawdown_pct DECIMAL(10,4) NULL, + average_daily_turnover_amount DECIMAL(24,2) NULL, + observation_count INT UNSIGNED NOT NULL, + source VARCHAR(64) NOT NULL, + calculation_version VARCHAR(16) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_metric_snapshot (product_id, as_of_date), + KEY idx_advisor_product_metric_latest (product_id, as_of_date), + CONSTRAINT fk_advisor_product_metric_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory metric snapshots must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_product_price_history.py b/alembic/versions/20260910_advisor_product_price_history.py new file mode 100644 index 0000000..8ff11de --- /dev/null +++ b/alembic/versions/20260910_advisor_product_price_history.py @@ -0,0 +1,47 @@ +"""add advisory product price history + +Compatibility proof: this revision only creates the additive +``advisor_product_price_history`` table. No baseline table or field is altered, +renamed, deleted, reused, or retyped. The table is the forward-compatible +history store used alongside immutable baseline market tables. +""" + +from alembic import op + +revision = "20260910_adv_price_history" +down_revision = "20260910_adv_offsite_ref" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_price_history ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + trade_date DATE NOT NULL, + price_kind VARCHAR(16) NOT NULL, + close_price DECIMAL(18,6) NOT NULL, + turnover_amount DECIMAL(24,2) NULL, + source VARCHAR(64) NOT NULL, + source_updated_at DATETIME(6) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_price_history (product_id, trade_date, price_kind), + KEY idx_advisor_product_price_history_read (product_id, trade_date DESC), + CONSTRAINT chk_advisor_product_price_history_kind + CHECK (price_kind IN ('fund_nav', 'market_close')), + CONSTRAINT chk_advisor_product_price_history_close + CHECK (close_price > 0), + CONSTRAINT fk_advisor_product_price_history_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory product price history must not be dropped automatically") + diff --git a/alembic/versions/20260910_advisor_product_reference_snapshot.py b/alembic/versions/20260910_advisor_product_reference_snapshot.py new file mode 100644 index 0000000..0462bc0 --- /dev/null +++ b/alembic/versions/20260910_advisor_product_reference_snapshot.py @@ -0,0 +1,43 @@ +"""add advisory product reference snapshots + +Compatibility proof: this revision only creates the additive +``advisor_product_reference_snapshot`` table. No baseline table or baseline +field is altered, renamed, deleted, reused, or retyped. +""" + +from alembic import op + +revision = "20260910_adv_product_ref" +down_revision = "20260910_adv_asset_class" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_product_reference_snapshot ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + product_id BIGINT UNSIGNED NOT NULL, + as_of_date DATE NOT NULL, + fund_type VARCHAR(64) NOT NULL, + fund_asset_scale_billion DECIMAL(18,4) NOT NULL, + source VARCHAR(64) NOT NULL, + risk_mapping_version VARCHAR(32) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_product_reference_snapshot (product_id, as_of_date), + KEY idx_advisor_product_reference_latest (product_id, as_of_date), + CONSTRAINT chk_advisor_product_reference_scale + CHECK (fund_asset_scale_billion >= 0), + CONSTRAINT fk_advisor_product_reference_product + FOREIGN KEY (product_id) REFERENCES fin_product(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("advisory product reference snapshots must not be dropped automatically") + diff --git a/alembic/versions/20260911_advisor_goal_conversation.py b/alembic/versions/20260911_advisor_goal_conversation.py new file mode 100644 index 0000000..b73eaf3 --- /dev/null +++ b/alembic/versions/20260911_advisor_goal_conversation.py @@ -0,0 +1,42 @@ +"""add append-only investment-goal conversation extraction records + +Compatibility proof: this revision creates only the additive +``advisor_goal_conversation_extraction`` table. It does not alter, rename, +delete, reuse, or retype any baseline table or existing field. +""" + +from alembic import op + +revision = "20260911_adv_goal_conversation" +down_revision = "20260910_adv_quality_backtest" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE advisor_goal_conversation_extraction ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + message_id BIGINT UNSIGNED NOT NULL, + session_id VARCHAR(64) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + extraction_version VARCHAR(32) NOT NULL, + extracted_fields JSON NOT NULL, + missing_fields JSON NOT NULL, + status VARCHAR(32) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_goal_conversation_message (message_id), + KEY idx_advisor_goal_conversation_session (session_id, customer_id, id), + CONSTRAINT chk_advisor_goal_conversation_status + CHECK (status IN ('partial', 'complete', 'clarification_limit')) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + + +def downgrade() -> None: + raise RuntimeError("goal conversation extraction records must not be dropped automatically") + diff --git a/alembic/versions/20260911_advisor_profile_tag_governance.py b/alembic/versions/20260911_advisor_profile_tag_governance.py new file mode 100644 index 0000000..c4a18a4 --- /dev/null +++ b/alembic/versions/20260911_advisor_profile_tag_governance.py @@ -0,0 +1,122 @@ +"""add profile-tag provenance, confidence, and drift-review records + +Compatibility proof: this revision creates only the additive +``advisor_profile_drift_review`` and ``advisor_profile_tag`` tables. It does +not alter, rename, delete, reuse, or change any baseline table or field. Its +backfill reads existing current snapshots and writes only new tag records. +""" + +from alembic import op + +revision = "20260911_adv_profile_tags" +down_revision = "20260911_adv_goal_conversation" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE IF NOT EXISTS advisor_profile_drift_review ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + drift_no VARCHAR(36) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + source_assessment_id BIGINT UNSIGNED NOT NULL, + candidate_profile_uuid CHAR(36) NOT NULL, + candidate_profile_version BIGINT UNSIGNED NOT NULL, + candidate_snapshot JSON NOT NULL, + candidate_generation_basis JSON NOT NULL, + changed_tags JSON NOT NULL, + status VARCHAR(16) NOT NULL, + reviewer_user_id BIGINT UNSIGNED NULL, + reviewed_at DATETIME(6) NULL, + review_comment VARCHAR(1000) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_profile_drift_no (drift_no), + UNIQUE KEY uk_advisor_profile_drift_candidate_uuid (candidate_profile_uuid), + KEY idx_advisor_profile_drift_queue (status, created_at), + KEY idx_advisor_profile_drift_customer (customer_id, status, id), + CONSTRAINT chk_advisor_profile_drift_status + CHECK (status IN ('pending_review', 'approved', 'rejected')), + CONSTRAINT fk_advisor_profile_drift_customer + FOREIGN KEY (customer_id) REFERENCES sys_user(id), + CONSTRAINT fk_advisor_profile_drift_assessment + FOREIGN KEY (source_assessment_id) REFERENCES fin_risk_assessment(id), + CONSTRAINT fk_advisor_profile_drift_reviewer + FOREIGN KEY (reviewer_user_id) REFERENCES sys_user(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + op.execute( + """ + CREATE TABLE IF NOT EXISTS advisor_profile_tag ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + tag_uuid CHAR(36) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + tag_key VARCHAR(64) NOT NULL, + tag_value JSON NOT NULL, + tag_value_hash CHAR(64) NOT NULL, + confidence DECIMAL(5,4) NOT NULL, + source_type VARCHAR(32) NOT NULL, + source_reference VARCHAR(128) NOT NULL, + source_confidence DECIMAL(5,4) NOT NULL, + profile_version BIGINT UNSIGNED NOT NULL, + drift_review_id BIGINT UNSIGNED NULL, + previous_tag_id BIGINT UNSIGNED NULL, + drift_reason VARCHAR(32) NULL, + status VARCHAR(16) NOT NULL, + active_customer_tag VARCHAR(160) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_advisor_profile_tag_uuid (tag_uuid), + UNIQUE KEY uk_advisor_profile_tag_active (active_customer_tag), + KEY idx_advisor_profile_tag_customer (customer_id, tag_key, status, id), + KEY idx_advisor_profile_tag_review (drift_review_id, status, id), + CONSTRAINT chk_advisor_profile_tag_confidence CHECK (confidence BETWEEN 0 AND 1), + CONSTRAINT chk_advisor_profile_tag_source_confidence + CHECK (source_confidence BETWEEN 0 AND 1), + CONSTRAINT chk_advisor_profile_tag_status + CHECK (status IN ('active', 'pending_review', 'superseded', 'rejected')), + CONSTRAINT fk_advisor_profile_tag_customer + FOREIGN KEY (customer_id) REFERENCES sys_user(id), + CONSTRAINT fk_advisor_profile_tag_review + FOREIGN KEY (drift_review_id) REFERENCES advisor_profile_drift_review(id), + CONSTRAINT fk_advisor_profile_tag_previous + FOREIGN KEY (previous_tag_id) REFERENCES advisor_profile_tag(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + """ + ) + for tag_key, json_path in ( + ("risk_level", "$.risk_level"), + ("risk_profile", "$.risk_profile"), + ("investment_horizon", "$.investment_horizon"), + ("preferred_asset_classes", "$.preferred_asset_classes"), + ): + op.execute( + f""" + INSERT IGNORE INTO advisor_profile_tag ( + tag_uuid, customer_id, tag_key, tag_value, tag_value_hash, + confidence, source_type, source_reference, source_confidence, + profile_version, drift_review_id, previous_tag_id, drift_reason, + status, active_customer_tag, created_at, updated_at + ) + SELECT + UUID(), p.customer_id, '{tag_key}', JSON_EXTRACT(p.snapshot, '{json_path}'), + SHA2(CAST(JSON_EXTRACT(p.snapshot, '{json_path}') AS CHAR), 256), + 0.7500, 'formal_risk_assessment', + CONCAT('profile_snapshot:', COALESCE(p.profile_uuid, CAST(p.id AS CHAR))), + 0.8000, p.version, NULL, NULL, NULL, + 'active', CONCAT(p.customer_id, CHAR(58), '{tag_key}'), NOW(6), NOW(6) + FROM profile_snapshots p + WHERE p.is_current = 1 + AND JSON_EXTRACT(p.snapshot, '{json_path}') IS NOT NULL + """ + ) + + +def downgrade() -> None: + raise RuntimeError("画像标签证据与漂移复核记录不得自动删除,请使用前向兼容迁移") + diff --git a/docs/21-投顾Agent迁移TODO.md b/docs/21-投顾Agent迁移TODO.md index 3af7654..e516e22 100644 --- a/docs/21-投顾Agent迁移TODO.md +++ b/docs/21-投顾Agent迁移TODO.md @@ -12,7 +12,7 @@ 阶段一测试结果:单元/契约测试 `259 passed`,集成测试 `21 passed`,Ruff 通过,MyPy 通过,数据库结构审计通过,Alembic 当前版本为 `20260911_adv_profile_tags`。 -当前阻塞:远程仓库 `47.106.207.27:3000` 暂时无法连接;`lzl_develop` 尚未确认存在;当前分支缺少 `tools/audit_constraints.py`,约束审计待迁移到新底座后补齐。 +当前阻塞:远程仓库 `47.106.207.27:3000` 暂时无法连接;`lzl_develop` 尚未确认存在。当前新底座已包含 `tools/audit_constraints.py`,旧库版本不兼容问题待切换独立测试库处理。 ### 阶段二:建立新开发分支 @@ -26,6 +26,19 @@ 阶段三测试结果:专项测试 `9 passed`,全量单元/契约测试 `448 passed`,Ruff 通过,MyPy(104 个源文件)通过。适配提交:`5e8eecc`。 +### 阶段四:数据库和迁移 + +已将旧投顾迁移按 `qyqy_develop` 的 `20260910_drop_review_separation` head +重新接入为一条连续链,新增产品治理、合同证据、行业/资产分类、历史行情、行情源健康、 +数据质量、动态配置回测、投资目标、会话目标抽取和画像标签治理表。未修改基线迁移, +未删除、重命名或改变基线表和字段;迁移契约测试对此有静态校验。 + +阶段四测试结果:迁移契约 `2 passed`,全量单元测试 `442 passed`,Ruff 通过,MyPy +(104 个源文件)通过;独立 MySQL 库 `jr_agent_qyqy_migration` 在线执行 +`alembic upgrade head` 成功,结构审计显示 `72` 张业务表无缺失或多余,约束/ORM 审计通过。 +原 `jr_agent` 库仍记录旧投顾迁移版本 `20260911_adv_profile_tags`,不能直接用新底座升级, +已保留不动,待数据库负责人按迁移方案另行切换。 + ## 一、迁移准备 - [ ] 确认远程仓库可访问。(当前失败:连接 `47.106.207.27:3000` 被拒绝) @@ -94,31 +107,31 @@ python -m mypy app ## 四、数据库和迁移 -- [ ] 对比 `qyqy_develop` 数据库基线和 `docs/00-新数据库基线设计.md`。 -- [ ] 确认没有删除或重命名基线表。 -- [ ] 确认没有删除、重命名或复用基线字段。 -- [ ] 确认没有改变基线字段类型和可空性。 -- [ ] 确认迁移链只有一个 head。 -- [ ] 迁移产品参考数据表。 -- [ ] 迁移产品治理和适当性表。 -- [ ] 迁移产品合同证据表。 -- [ ] 迁移产品行业和资产类别表。 -- [ ] 迁移历史行情表。 -- [ ] 迁移产品指标快照表。 -- [ ] 迁移行情源监控和失败记录表。 -- [ ] 迁移行情质量表。 -- [ ] 迁移资产配置回测表。 -- [ ] 迁移投资组合图谱投影检查点表。 -- [ ] 迁移投资目标表。 -- [ ] 迁移会话目标抽取表。 -- [ ] 迁移画像标签表 `advisor_profile_tag`。 -- [ ] 迁移画像漂移复核表 `advisor_profile_drift_review`。 -- [ ] 为历史当前画像回填标签证据。 -- [ ] 在空库执行迁移。 +- [x] 对比 `qyqy_develop` 数据库基线和 `docs/00-新数据库基线设计.md`。(迁移契约测试 + 在线结构审计) +- [x] 确认没有删除或重命名基线表。(14 个投顾迁移仅 `CREATE TABLE`) +- [x] 确认没有删除、重命名或复用基线字段。(迁移契约测试通过) +- [x] 确认没有改变基线字段类型和可空性。(未修改基线迁移) +- [x] 确认迁移链只有一个 head。(`20260911_adv_profile_tags`) +- [x] 迁移产品参考数据表。 +- [x] 迁移产品治理和适当性表。 +- [x] 迁移产品合同证据表。 +- [x] 迁移产品行业和资产类别表。 +- [x] 迁移历史行情表。 +- [x] 迁移产品指标快照表。 +- [x] 迁移行情源监控和失败记录表。 +- [x] 迁移行情质量表。 +- [x] 迁移资产配置回测表。 +- [x] 迁移投资组合图谱投影检查点表。 +- [x] 迁移投资目标表。 +- [x] 迁移会话目标抽取表。 +- [x] 迁移画像标签表 `advisor_profile_tag`。 +- [x] 迁移画像漂移复核表 `advisor_profile_drift_review`。 +- [x] 为历史当前画像回填标签证据。(迁移包含回填逻辑;独立空库无历史画像) +- [x] 在空库执行迁移。(`jr_agent_qyqy_migration`) - [ ] 在已有测试库执行迁移。 -- [ ] 执行数据库结构审计。 -- [ ] 执行数据库约束审计。 -- [ ] 完成数据库迁移提交 `advisor/database-migrations`。 +- [x] 执行数据库结构审计。(72 张业务表,无缺失或多余) +- [x] 执行数据库约束审计。(唯一键与 ORM 映射通过) +- [ ] 完成数据库迁移提交 `advisor/database-migrations`。(待提交) 验收命令: diff --git a/tests/unit/test_advisor_migration_contract.py b/tests/unit/test_advisor_migration_contract.py new file mode 100644 index 0000000..0634d90 --- /dev/null +++ b/tests/unit/test_advisor_migration_contract.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import re +from pathlib import Path + +from alembic.config import Config +from alembic.script import ScriptDirectory + +ROOT = Path(__file__).resolve().parents[2] +VERSIONS = ROOT / "alembic" / "versions" +BASELINE = ROOT / "alembic" / "baseline_generated.sql" + +ADVISOR_FILES = ( + "20260910_advisor_investment_goal.py", + "20260910_advisor_product_industry_exposure.py", + "20260910_advisor_portfolio_projection_checkpoint.py", + "20260910_advisor_product_metric_snapshot.py", + "20260910_advisor_product_asset_classification.py", + "20260910_advisor_product_reference_snapshot.py", + "20260910_advisor_offsite_fund_reference.py", + "20260910_advisor_product_price_history.py", + "20260910_advisor_product_governance_reference.py", + "20260910_advisor_governance_monitor_and_quotes.py", + "20260910_advisor_market_quote_resilience.py", + "20260910_advisor_data_quality_backtest.py", + "20260911_advisor_goal_conversation.py", + "20260911_advisor_profile_tag_governance.py", +) + + +def created_tables(path: Path) -> set[str]: + content = path.read_text(encoding="utf-8") + return set(re.findall(r"CREATE TABLE(?: IF NOT EXISTS)?\s+`?([A-Za-z0-9_]+)`?", content)) + + +def test_advisor_migrations_form_one_chain_from_qyqy_head() -> None: + script = ScriptDirectory.from_config(Config(str(ROOT / "alembic.ini"))) + assert len(script.get_heads()) == 1 + assert script.get_heads()[0] == "20260911_adv_profile_tags" + + first = (VERSIONS / ADVISOR_FILES[0]).read_text(encoding="utf-8") + assert 'down_revision = "20260910_drop_review_separation"' in first + for previous, current in zip(ADVISOR_FILES, ADVISOR_FILES[1:], strict=False): + previous_content = (VERSIONS / previous).read_text(encoding="utf-8") + current_content = (VERSIONS / current).read_text(encoding="utf-8") + previous_revision = re.search(r'revision = "([^"]+)"', previous_content) + assert previous_revision is not None + assert f'down_revision = "{previous_revision.group(1)}"' in current_content + + +def test_advisor_migrations_only_create_additive_tables() -> None: + baseline_tables = created_tables(BASELINE) + advisor_tables: set[str] = set() + for filename in ADVISOR_FILES: + content = (VERSIONS / filename).read_text(encoding="utf-8") + assert "ALTER TABLE" not in content + assert "DROP TABLE" not in content + advisor_tables.update(created_tables(VERSIONS / filename)) + + assert advisor_tables + assert not advisor_tables & baseline_tables + assert "fin_sim_order" not in advisor_tables + assert "fin_transaction" not in advisor_tables + assert "fin_holding" not in advisor_tables diff --git a/tools/audit_schema.py b/tools/audit_schema.py index 8ab6972..b9170c8 100644 --- a/tools/audit_schema.py +++ b/tools/audit_schema.py @@ -37,6 +37,28 @@ INCREMENTAL_TABLES = { "outbox_delivery", "svc_conversation_session", "api_request_receipt", + # 投顾增量表:全部为新增表,基线表和场内交易表保持不变。 + "advisor_investment_goal", + "advisor_product_industry_exposure", + "advisor_portfolio_projection_checkpoint", + "advisor_product_metric_snapshot", + "advisor_product_asset_classification", + "advisor_product_reference_snapshot", + "advisor_offsite_fund_reference", + "advisor_product_price_history", + "advisor_product_suitability_reference", + "advisor_product_contract_snapshot", + "advisor_product_governance_candidate_snapshot", + "advisor_product_governance_sync_run", + "advisor_product_market_quote_snapshot", + "advisor_market_quote_source_run", + "advisor_market_quote_source_health", + "advisor_market_quote_alert", + "advisor_product_data_quality_snapshot", + "advisor_allocation_backtest_run", + "advisor_goal_conversation_extraction", + "advisor_profile_drift_review", + "advisor_profile_tag", }