Files
geMoldInsight/migrations/versions/b7d1f4a92c3e_add_experience_feedback.py
T
cjw 2c9ba9d6b3 D17 批 1:Human-in-Loop 老师傅经验反馈(数据 + 权限 + 写入 API)
新增 experience_feedback 表(32 表迁移,alembic head b7d1f4a92c3e),
老师傅对系统推荐方案给出"采纳 / 调整 / 拒绝"反馈,按"产品指纹 +
工艺参数"为键跨任务匹配,下次同指纹产品分析自动消费。

变更内容:
- src/moldinsight/models/experience_feedback.py(new)ORM:Base 单点来源、
  跨模块裸 FK(user_id / processing_task_id / stp_file_id)、不建 ORM
  relationship;fingerprint JSON 列存跨任务匹配键
- src/moldinsight/models/__init__.py 导出 ExperienceFeedback
- migrations/versions/b7d1f4a92c3e_add_experience_feedback.py(new)32 表
  迁移;fingerprint 列 PG 下加 GIN 索引(jsonb_path_query 支持)
- src/shared/database/init_db.py 加 3 个权限码(view_experience_feedback /
  feedback_experience_hint / manage_experience_feedback)+ 新角色
  process_engineer;admin 角色 permissions 同步补齐;init_permissions /
  init_roles 改为按 code 比对(新增保留已有 id,避免 FK 引用失效)——
  修复既有 DB 启动期漏掉新权限的幂等 bug
- src/moldinsight/services/experience_feedback_service.py(new)service:
  compute_fingerprint 分桶(bbox_aspect / volume_bucket / face_bucket /
  undercut_class / material_family / is_foam)/ record_feedback(D9 边界:
  service.flush + 路由 commit;D17 衰减:同 stp_file_id 整体续期 90 天 TTL,
  无 celery beat 依赖)/ list_hints_for_task / resolve_for_process_params
- src/moldinsight/api/experience_feedback_router.py(new)路由:Pydantic
  模型写在路由文件内(项目硬规则);POST /api/tasks/{task_id}/experience-feedback
  + GET /api/tasks/{task_id}/experience-hints;归属 TaskQueryService.ensure_task_access
  + User.has_permission 全仓首次调用点
- src/moldinsight/api/__init__.py ROUTE_MODULES 注册新路由
- tests/test_model_ownership.py EXPECTED_TABLES 加 experience_feedback
  (31→32)
- tests/test_experience_feedback_fingerprint.py(new)分桶参数化覆盖
  bbox / volume / face / undercut / material / is_foam 各边界值
- tests/test_experience_feedback_router.py(new)API 契约 9 例
  (401/403/422/200 路径 + 衰减续期 + 任务归属校验 + ORM 注册收口)
- docs/STATUS.md 顶部加 2026-09-23 批 1 日志条目
- docs/TECH_DEBT.md D17 加批 1 已完成描述 + 剩余工作清单
- docs/API_CONTRACT.md §3.2 加 D17 端点表格

测试基线:185 passed, 9 skipped(净增 59 测试)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 16:11:17 +08:00

93 lines
4.9 KiB
Python
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.
"""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')