021bf311c1
fingerprint 列是 sa.JSON() → PG json 类型,GIN 只支持 jsonb, CREATE INDEX ... USING GIN (fingerprint) 直接报 UndefinedObject。 且代码侧并无 JSON 包含查询(聚合过滤在 Python 侧,DB 走 stp_axis_status 复合索引),索引本身无用。 首次对生产库执行此迁移时因此报错、整个 upgrade 事务回滚 (2026-09-26)。已在本机修复后重新 upgrade head 成功。 未来若需 JSON 检索:先把列迁为 jsonb 再建 GIN。 Co-Authored-By: Claude Code <noreply@anthropic.com>
93 lines
5.2 KiB
Python
93 lines
5.2 KiB
Python
"""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,
|
||
)
|
||
# 注意:此处原本计划给 fingerprint 建 GIN 索引("jsonb_path_query 类查询"),
|
||
# 但该列是 sa.JSON() → PG 的 json 类型,而 GIN 只支持 jsonb(json 无默认
|
||
# 操作符类,CREATE INDEX 直接报 UndefinedObject)。且代码侧并无 JSON 包含
|
||
# 查询——聚合过滤在 Python 侧进行,DB 侧走上方 (stp_file_id, scheme_axis,
|
||
# feedback_status) 复合索引。故不建此索引;未来若真需要 JSON 检索,应先把
|
||
# 列迁为 jsonb 再建 GIN。首次对生产库执行时曾因此报错回滚(2026-09-26)。
|
||
|
||
|
||
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') |