2c9ba9d6b3
新增 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>
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""D3 模型拆分归属保护(批次 4,2026-09-17)+ D17 Human-in-Loop 闭环。
|
||
|
||
锁定三个拆分成果:
|
||
1. 三包模型全量注册后 mapper 可配置、32 表齐全(含 D17 新增 experience_feedback);
|
||
2. 单模块部署(inventory-only / moldinsight-only + auth)独立配置 mapper 成功——
|
||
跨模块 ORM relationship 已清零,任何一侧不注册对方模型也能工作;
|
||
3. 旧 shared.models.database 模块已删除且无兼容 facade(诚实原则:不留假象)。
|
||
"""
|
||
import importlib
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
SRC = str(Path(__file__).resolve().parent.parent / "src")
|
||
|
||
EXPECTED_TABLES = {
|
||
# identity(shared.models.identity)
|
||
"users", "roles", "permissions", "user_roles", "role_permissions",
|
||
"user_activities", "system_logs",
|
||
# moldinsight.models
|
||
"stp_files", "geometry_data", "mesh_data", "html_files", "processing_tasks",
|
||
"mold_cavity_data", "feature_detections", "design_recommendations", "analysis_metrics",
|
||
# D17:老师傅经验反馈(Human-in-Loop 闭环,2026-09)
|
||
"experience_feedback",
|
||
# inventory.models
|
||
"products", "product_materials", "material_price_history", "material_suppliers",
|
||
"suppliers", "customers", "warehouses", "inventory", "stock_movements",
|
||
"purchase_orders", "purchase_order_items", "sales_orders", "sales_order_items",
|
||
"finance_transactions", "finance_allocations",
|
||
}
|
||
|
||
|
||
def test_full_registration_covers_all_32_tables():
|
||
import shared.models.identity # noqa: F401
|
||
import moldinsight.models # noqa: F401
|
||
import inventory.models # noqa: F401
|
||
from sqlalchemy.orm import configure_mappers
|
||
|
||
from shared.models.base import Base
|
||
|
||
configure_mappers()
|
||
assert set(Base.metadata.tables) == EXPECTED_TABLES
|
||
|
||
|
||
def test_single_module_deployments_configure_mappers_independently():
|
||
"""单模块注册子进程验证:inventory-only 与 moldinsight-only(含 auth identity)
|
||
均可在不 import 对方业务模型的情况下 configure_mappers 成功。
|
||
用子进程隔离,避免污染本进程的 mapper 注册表。"""
|
||
code = (
|
||
"import sys; sys.path.insert(0, r'%s')\n"
|
||
"from sqlalchemy.orm import configure_mappers\n"
|
||
"%s\n"
|
||
"configure_mappers()\n"
|
||
"print('ok')\n"
|
||
)
|
||
cases = [
|
||
# inventory-only:inventory 模型 + auth 必带的 identity
|
||
"import inventory.models, shared.models.identity",
|
||
# moldinsight-only:moldinsight 模型 + auth 必带的 identity
|
||
"import moldinsight.models, shared.models.identity",
|
||
]
|
||
for imports in cases:
|
||
proc = subprocess.run(
|
||
[sys.executable, "-c", code % (SRC, imports)],
|
||
capture_output=True, text=True, timeout=120,
|
||
)
|
||
assert proc.returncode == 0, f"{imports} 配置失败:\n{proc.stderr}"
|
||
assert proc.stdout.strip().endswith("ok")
|
||
|
||
|
||
def test_legacy_database_module_is_gone():
|
||
"""旧 shared.models.database 已物理删除,无兼容 facade。"""
|
||
try:
|
||
importlib.import_module("shared.models.database")
|
||
except ModuleNotFoundError:
|
||
pass
|
||
else:
|
||
raise AssertionError("shared.models.database 仍可导入——拆分后不允许残留兼容 facade")
|