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>
This commit is contained in:
@@ -65,55 +65,86 @@ DEFAULT_PERMISSIONS = [
|
||||
{"code": "view_users", "name": "查看用户", "module": "admin"},
|
||||
{"code": "manage_users", "name": "管理用户", "module": "admin"},
|
||||
{"code": "manage_roles", "name": "管理角色", "module": "admin"},
|
||||
# D17 Human-in-Loop:老师傅经验反馈
|
||||
{"code": "view_experience_feedback", "name": "查看老师傅反馈", "module": "moldinsight"},
|
||||
{"code": "feedback_experience_hint", "name": "提交方案级反馈", "module": "moldinsight"},
|
||||
{"code": "manage_experience_feedback", "name": "管理老师傅反馈", "module": "moldinsight"},
|
||||
]
|
||||
|
||||
DEFAULT_ROLES = [
|
||||
{"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_finance", "manage_receipts", "manage_payments", "void_finance_transaction", "view_users", "manage_users", "manage_roles"]},
|
||||
{"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_finance", "manage_receipts", "manage_payments", "void_finance_transaction", "view_users", "manage_users", "manage_roles", "view_experience_feedback", "feedback_experience_hint", "manage_experience_feedback"]},
|
||||
{"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance", "manage_receipts", "manage_payments"]},
|
||||
{"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance"]},
|
||||
# D17 Human-in-Loop:工艺工程师角色——可查看 + 提交老师傅反馈
|
||||
{"code": "process_engineer", "name": "工艺工程师", "description": "工艺工程师,可查看 + 提交老师傅经验反馈", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_experience_feedback", "feedback_experience_hint"]},
|
||||
]
|
||||
|
||||
|
||||
async def init_permissions(session):
|
||||
"""初始化权限"""
|
||||
"""初始化权限(按 code 补登:已存在跳过,缺失新增)
|
||||
|
||||
设计要点(D17 修复):
|
||||
- 旧实现 `if existing_perms: return` 会让既存 DB 启动期漏掉新增权限码
|
||||
- 改为按 code 比对:已存在的 permission 保留 id(避免 FK 引用失效),
|
||||
缺失的新增;这样后续 DEFAULT_PERMISSIONS 追加的项也能在升级时落到既存 DB
|
||||
"""
|
||||
result = await session.execute(select(Permission))
|
||||
existing_perms = result.scalars().all()
|
||||
|
||||
if existing_perms:
|
||||
logger.info("权限已初始化")
|
||||
return
|
||||
|
||||
perm_map = {}
|
||||
existing_perms = {p.code: p for p in result.scalars().all()}
|
||||
|
||||
perm_map = {p.code: p.id for p in existing_perms.values()}
|
||||
new_count = 0
|
||||
for perm_data in DEFAULT_PERMISSIONS:
|
||||
if perm_data["code"] in existing_perms:
|
||||
continue
|
||||
perm = Permission(**perm_data)
|
||||
session.add(perm)
|
||||
await session.flush()
|
||||
perm_map[perm.code] = perm.id
|
||||
|
||||
logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限")
|
||||
new_count += 1
|
||||
|
||||
if not existing_perms:
|
||||
logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限")
|
||||
elif new_count:
|
||||
logger.info(f"补登了 {new_count} 个权限(既有 DB 升级)")
|
||||
else:
|
||||
logger.info("权限已初始化(无新增)")
|
||||
return perm_map
|
||||
|
||||
|
||||
async def init_roles(session, perm_map):
|
||||
"""初始化角色"""
|
||||
"""初始化角色(按 code 补登:已存在跳过,缺失新建 + 完整绑定 permissions)
|
||||
|
||||
设计要点(D17 修复):
|
||||
- 旧实现 `if existing_roles: return` 会让既存 DB 启动期漏掉新角色
|
||||
- 改为按 code 比对:已存在的角色不重置其 RolePermission 绑定
|
||||
(避免重建关联破坏 user / role 关系),缺失的角色按 DEFAULT_ROLES 完整创建
|
||||
"""
|
||||
result = await session.execute(select(Role))
|
||||
existing_roles = result.scalars().all()
|
||||
|
||||
if existing_roles:
|
||||
logger.info("角色已初始化")
|
||||
return
|
||||
|
||||
existing_roles = {r.code: r for r in result.scalars().all()}
|
||||
|
||||
new_count = 0
|
||||
for role_data in DEFAULT_ROLES:
|
||||
perm_ids = [perm_map[code] for code in role_data.pop("permissions")]
|
||||
code = role_data["code"]
|
||||
if code in existing_roles:
|
||||
continue
|
||||
perm_codes = role_data.pop("permissions")
|
||||
role = Role(**role_data)
|
||||
session.add(role)
|
||||
await session.flush()
|
||||
|
||||
for perm_id in perm_ids:
|
||||
for perm_code in perm_codes:
|
||||
perm_id = perm_map.get(perm_code)
|
||||
if perm_id is None:
|
||||
continue
|
||||
rp = RolePermission(role_id=role.id, permission_id=perm_id)
|
||||
session.add(rp)
|
||||
|
||||
logger.info(f"创建了 {len(DEFAULT_ROLES)} 个角色")
|
||||
new_count += 1
|
||||
|
||||
if not existing_roles:
|
||||
logger.info(f"创建了 {len(DEFAULT_ROLES)} 个角色")
|
||||
elif new_count:
|
||||
logger.info(f"补登了 {new_count} 个角色(既有 DB 升级)")
|
||||
else:
|
||||
logger.info("角色已初始化(无新增)")
|
||||
|
||||
|
||||
async def create_admin_user(session):
|
||||
|
||||
Reference in New Issue
Block a user