"""add experience_feedback Human-in-Loop 闭环(D17,2026-09):老师傅对系统推荐方案给出'采纳 / 调整 / 拒绝' 反馈,按'产品指纹 + 工艺参数'为索引跨任务匹配;下次同指纹产品分析自动消费。 - 新增 experience_feedback 表(方案级反馈) - 跨模块裸 FK(user_id / processing_task_id / stp_file_id) - 复合索引 (stp_file_id, scheme_axis, feedback_status) 用于按方向聚合采纳计数 - GIN 索引(PG only)用于 fingerprint JSON 字段检索 Revision ID: b7d1f4a92c3e Revises: a3f8c2d91e47 Create Date: 2026-09-22 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = 'b7d1f4a92c3e' down_revision: Union[str, Sequence[str], None] = 'a3f8c2d91e47' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( 'experience_feedback', sa.Column('id', sa.Integer(), nullable=False), sa.Column('processing_task_id', sa.Integer(), nullable=False), sa.Column('stp_file_id', sa.Integer(), nullable=False), sa.Column('scheme_id', sa.String(length=64), nullable=False), sa.Column('scheme_axis', sa.String(length=1), nullable=False), sa.Column('scheme_method', sa.String(length=50), nullable=True), sa.Column('feedback_status', sa.String(length=20), nullable=False), sa.Column('feedback_reason', sa.Text(), nullable=True), sa.Column('adjust_suggestion', sa.Text(), nullable=True), sa.Column('process_params_snapshot', sa.JSON(), nullable=True), sa.Column('fingerprint', sa.JSON(), nullable=False), sa.Column('confidence_at_submit', sa.Float(), nullable=True), sa.Column('score_at_submit', sa.Float(), nullable=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('role_code', sa.String(length=50), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('expires_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.ForeignKeyConstraint( ['processing_task_id'], ['processing_tasks.id'], ondelete='CASCADE' ), sa.ForeignKeyConstraint( ['stp_file_id'], ['stp_files.id'], ondelete='CASCADE' ), sa.ForeignKeyConstraint( ['user_id'], ['users.id'], ondelete='RESTRICT' ), ) op.create_index(op.f('ix_experience_feedback_id'), 'experience_feedback', ['id'], unique=False) op.create_index(op.f('ix_experience_feedback_processing_task_id'), 'experience_feedback', ['processing_task_id'], unique=False) op.create_index(op.f('ix_experience_feedback_stp_file_id'), 'experience_feedback', ['stp_file_id'], unique=False) op.create_index(op.f('ix_experience_feedback_scheme_id'), 'experience_feedback', ['scheme_id'], unique=False) op.create_index(op.f('ix_experience_feedback_feedback_status'), 'experience_feedback', ['feedback_status'], unique=False) op.create_index(op.f('ix_experience_feedback_user_id'), 'experience_feedback', ['user_id'], unique=False) op.create_index(op.f('ix_experience_feedback_created_at'), 'experience_feedback', ['created_at'], unique=False) op.create_index(op.f('ix_experience_feedback_expires_at'), 'experience_feedback', ['expires_at'], unique=False) op.create_index( op.f('ix_experience_feedback_stp_axis_status'), 'experience_feedback', ['stp_file_id', 'scheme_axis', 'feedback_status'], unique=False, ) # GIN 索引(PG only):fingerprint JSON 字段 jsonb_path_query 类查询 if op.get_bind().dialect.name == 'postgresql': op.execute( "CREATE INDEX ix_experience_feedback_fingerprint_gin " "ON experience_feedback USING GIN (fingerprint)" ) def downgrade() -> None: if op.get_bind().dialect.name == 'postgresql': op.execute("DROP INDEX IF EXISTS ix_experience_feedback_fingerprint_gin") op.drop_index(op.f('ix_experience_feedback_stp_axis_status'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_expires_at'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_created_at'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_user_id'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_feedback_status'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_scheme_id'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_stp_file_id'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_processing_task_id'), table_name='experience_feedback') op.drop_index(op.f('ix_experience_feedback_id'), table_name='experience_feedback') op.drop_table('experience_feedback')