import json import logging import time from typing import Any, Optional from fastapi import APIRouter, Depends, Request from pydantic import BaseModel from sqlalchemy.orm import Session from database import get_db from models.scene import ( FirstScene, RecognitionRecord, Scene, SceneTemplate, SecondScene, ThirdScene, ) router = APIRouter() logger = logging.getLogger(__name__) def _get_user_code(user: Any) -> str: return ( getattr(user, "userCode", None) or getattr(user, "user_code", None) or getattr(user, "account", "") ) def _load_hazard_details(record: RecognitionRecord): if not record.hazard_details: return [] try: data = json.loads(record.hazard_details) return data if isinstance(data, list) else [] except Exception: return [] def _split_labels(labels): if not labels: return [] if isinstance(labels, list): return [str(item).strip() for item in labels if str(item).strip()] return [ item.strip() for item in str(labels).replace(",", ",").split(",") if item.strip() ] def _unique_ordered(items): seen = set() ordered = [] for item in items: if not item or item in seen: continue seen.add(item) ordered.append(item) return ordered def _build_record_view(record: RecognitionRecord, db: Session = None): hazard_details = _load_hazard_details(record) derived_labels = _unique_ordered( [ str(item.get("label") or "").strip() for item in hazard_details if str(item.get("label") or "").strip() ] ) display_labels = _split_labels(record.labels) or derived_labels if record.description: third_scenes = [item for item in str(record.description).split(" ") if item] else: third_scenes = derived_labels detections = [ { "label": item.get("label", ""), "box": item.get("bbox") or item.get("box") or [], "bbox": item.get("bbox") or item.get("box") or [], "confidence": item.get("confidence", 0), } for item in hazard_details ] # Rebuild element_hazards from database if db session is provided element_hazards = {} if db and display_labels: try: from models.scene import SecondScene, ThirdScene from utils.label_translator import translate_scenes_for_query for label in display_labels: # Translate sub-secondary scene to secondary scene for database query # e.g., "加油机" -> "加油设施_附属场地" query_label = translate_scenes_for_query([label])[0] if label else label # Find matching secondary scene using translated name second_scene = ( db.query(SecondScene) .filter( SecondScene.second_scene_name == query_label, SecondScene.is_deleted == 0, ) .first() ) if second_scene: # Get all third scenes for this secondary scene third_scene_records = ( db.query(ThirdScene) .filter( ThirdScene.second_scene_id == second_scene.id, ThirdScene.is_deleted == 0, ) .all() ) if third_scene_records: # Use original label as key for frontend matching element_hazards[label] = [ ts.third_scene_name for ts in third_scene_records ] else: element_hazards[label] = [] else: element_hazards[label] = [] except Exception as e: logger.warning( "_build_record_view failed to rebuild element_hazards: record_id=%s error=%s", record.id, e, ) return { "id": record.id, "title": record.title or "隐患提示记录", "description": record.description or " ".join(third_scenes), "original_image_url": record.original_image_url, "recognition_image_url": record.recognition_image_url, "labels": record.labels or ",".join(display_labels), "display_labels": display_labels, "third_scenes": third_scenes, "element_hazards": element_hazards, "tag_type": record.tag_type or record.scene_type, "scene_type": record.scene_type, "effect_evaluation": record.effect_evaluation, "hazard_details": hazard_details, "detections": detections, } def _resolve_record_id( recognition_id: Optional[int] = None, recognition_record_id: Optional[int] = None, ): return recognition_id or recognition_record_id @router.get("/get_scene_list") async def get_scene_list(db: Session = Depends(get_db)): scenes = db.query(Scene).filter(Scene.is_deleted == 0).all() return { "statusCode": 200, "msg": "success", "data": [ { "id": s.id, "scene_name": s.scene_name, "scene_en_name": s.scene_en_name, } for s in scenes ], } @router.get("/get_first_scene_list") async def get_first_scene_list(scene_id: int, db: Session = Depends(get_db)): scenes = ( db.query(FirstScene) .filter(FirstScene.scene_id == scene_id, FirstScene.is_deleted == 0) .all() ) return { "statusCode": 200, "msg": "success", "data": [{"id": s.id, "first_scene_name": s.first_scene_name} for s in scenes], } @router.get("/get_second_scene_list") async def get_second_scene_list( first_scene_id: int, db: Session = Depends(get_db) ): scenes = ( db.query(SecondScene) .filter( SecondScene.first_scene_id == first_scene_id, SecondScene.is_deleted == 0, ) .all() ) return { "statusCode": 200, "msg": "success", "data": [{"id": s.id, "second_scene_name": s.second_scene_name} for s in scenes], } @router.get("/get_third_scene_list") async def get_third_scene_list( second_scene_id: int, db: Session = Depends(get_db) ): scenes = ( db.query(ThirdScene) .filter( ThirdScene.second_scene_id == second_scene_id, ThirdScene.is_deleted == 0, ) .all() ) return { "statusCode": 200, "msg": "success", "data": [ { "id": s.id, "third_scene_name": s.third_scene_name, "correct_example_image": s.correct_example_image, "wrong_example_image": s.wrong_example_image, } for s in scenes ], } @router.get("/get_third_scene_example_image") async def get_third_scene_example_image( third_scene_name: str, db: Session = Depends(get_db) ): if not third_scene_name: return {"statusCode": 400, "msg": "三级场景名称不能为空"} scene = ( db.query(ThirdScene) .filter( ThirdScene.third_scene_name == third_scene_name, ThirdScene.is_deleted == 0, ) .first() ) if not scene: return {"statusCode": 404, "msg": "三级场景不存在"} return { "statusCode": 200, "msg": "success", "data": { "id": scene.id, "third_scene_name": scene.third_scene_name, "correct_example_image": scene.correct_example_image, "wrong_example_image": scene.wrong_example_image, }, } @router.get("/get_history_recognition_record") async def get_history_recognition_record( request: Request, db: Session = Depends(get_db) ): # 让日志更容易被全局日志系统捕获:同时输出 INFO / WARNING / DEBUG 关键点 logger.warning( "=== get_history_recognition_record ENTER === path=%s method=%s client=%s", request.url.path, request.method, request.client.host if request.client else None, ) request_id = getattr(request.state, "request_id", None) or getattr( request.state, "trace_id", None ) logger.warning( "get_history_recognition_record request context: request_id=%s state_keys=%s", request_id, list(getattr(request.state, "__dict__", {}).keys()), ) user = getattr(request.state, "user", None) if not user: logger.warning( "get_history_recognition_record unauthorized: request_id=%s missing request.state.user headers=%s", request_id, { "authorization": request.headers.get("authorization"), "cookie": request.headers.get("cookie"), }, ) return {"statusCode": 401, "msg": "未授权"} user_code = _get_user_code(user) user_id = getattr(user, "user_id", None) account = getattr(user, "account", None) user_snapshot = { "userCode": getattr(user, "userCode", None), "user_code": getattr(user, "user_code", None), "user_id": user_id, "account": account, "type": type(user).__name__, "raw_repr": repr(user), } logger.warning( "get_history_recognition_record user resolved: request_id=%s user_code=%s user_id=%s account=%s user_snapshot=%s", request_id, user_code, user_id, account, user_snapshot, ) query_filter = [ RecognitionRecord.user_id == user_id, RecognitionRecord.is_deleted == 0, ] logger.warning( "get_history_recognition_record query prepared: request_id=%s filters=user_id==%s,is_deleted==0 order_by=updated_at.desc()", request_id, user_id, ) logger.warning( "get_history_recognition_record query detail: request_id=%s sql_filter_user_id_value=%r sql_filter_is_deleted=0", request_id, user_id, ) logger.warning( "get_history_recognition_record query mismatch check: request_id=%s record.user_id_column_against=user_id(%r) user_code_attr=%r", request_id, user_id, user_code, ) try: total = db.query(RecognitionRecord).filter(*query_filter).count() logger.warning( "get_history_recognition_record count done: request_id=%s user_id=%s total=%s", request_id, user_id, total, ) records = ( db.query(RecognitionRecord) .filter(*query_filter) .order_by(RecognitionRecord.updated_at.desc()) .all() ) logger.warning( "get_history_recognition_record records loaded: request_id=%s user_id=%s loaded=%s record_ids=%s", request_id, user_id, len(records), [record.id for record in records[:20]], ) logger.warning( "get_history_recognition_record records debug sample: request_id=%s sample=%s", request_id, [ { "id": r.id, "user_id": r.user_id, "scene_type": r.scene_type, "created_at": r.created_at, "updated_at": r.updated_at, "is_deleted": r.is_deleted, } for r in records[:3] ], ) data = [] for index, record in enumerate(records): try: record_view = _build_record_view(record) logger.warning( "get_history_recognition_record record built: request_id=%s index=%s record_id=%s scene_type=%s tag_type=%s labels=%s hazard_count=%s effect_evaluation=%s created_at=%s updated_at=%s", request_id, index, record.id, record.scene_type, record_view.get("tag_type"), record_view.get("labels"), len(record_view.get("hazard_details") or []), record.effect_evaluation, record.created_at, record.updated_at, ) data.append( { **record_view, "created_at": record.created_at, } ) except Exception as record_error: logger.exception( "get_history_recognition_record record build failed: request_id=%s index=%s record_id=%s error=%s", request_id, index, getattr(record, "id", None), record_error, ) logger.warning( "get_history_recognition_record success: request_id=%s user_id=%s total=%s returned=%s", request_id, user_id, total, len(data), ) logger.warning( "get_history_recognition_record exit summary: request_id=%s user_code=%s user_id=%s account=%s total=%s returned=%s", request_id, user_code, user_id, account, total, len(data), ) logger.warning("=== get_history_recognition_record EXIT OK === request_id=%s", request_id) return { "statusCode": 200, "msg": "success", "data": data, "total": total, } except Exception as exc: logger.exception( "get_history_recognition_record failed: request_id=%s user_code=%s user_id=%s account=%s error=%s", request_id, user_code, user_id, account, exc, ) logger.error( "=== get_history_recognition_record EXIT ERR === request_id=%s user_code=%s user_id=%s account=%s", request_id, user_code, user_id, account, ) return {"statusCode": 500, "msg": "获取历史记录失败"} @router.get("/get_recognition_record_detail") async def get_recognition_record_detail( recognition_id: Optional[int] = None, recognition_record_id: Optional[int] = None, db: Session = Depends(get_db), ): record_id = _resolve_record_id(recognition_id, recognition_record_id) logger.info( "get_recognition_record_detail start: recognition_id=%s recognition_record_id=%s resolved_record_id=%s", recognition_id, recognition_record_id, record_id, ) if not record_id: logger.warning("get_recognition_record_detail invalid request: missing record id") return {"statusCode": 422, "msg": "recognition_id 不能为空"} record = ( db.query(RecognitionRecord) .filter(RecognitionRecord.id == record_id, RecognitionRecord.is_deleted == 0) .first() ) if not record: logger.warning("get_recognition_record_detail not found: record_id=%s", record_id) return {"statusCode": 404, "msg": "记录不存在"} record_view = _build_record_view(record, db) logger.info( "get_recognition_record_detail success: record_id=%s user_id=%s scene_type=%s element_hazards_count=%s", record.id, record.user_id, record.scene_type, len(record_view.get("element_hazards", {})), ) return { "statusCode": 200, "msg": "success", "data": { "id": record.id, "user_id": record.user_id, "title": record_view["title"], "description": record_view["description"], "original_image_url": record.original_image_url, "recognition_image_url": record.recognition_image_url, "labels": record_view["labels"], "display_labels": record_view["display_labels"], "third_scenes": record_view["third_scenes"], "element_hazards": record_view["element_hazards"], "tag_type": record_view["tag_type"], "scene_type": record.scene_type, "scene_match": record.scene_match, "tip_accuracy": record.tip_accuracy, "effect_evaluation": record.effect_evaluation, "user_remark": record.user_remark, "hazard_details": record_view["hazard_details"], "detections": record_view["detections"], "created_at": record.created_at, "updated_at": record.updated_at, }, } class DeleteRecognitionRequest(BaseModel): recognition_id: Optional[int] = None recognition_record_id: Optional[int] = None @router.post("/delete_recognition_record") async def delete_recognition_record( data: DeleteRecognitionRequest, request: Request, db: Session = Depends(get_db), ): user = request.state.user if not user: logger.warning("delete_recognition_record unauthorized: missing request.state.user") return {"statusCode": 401, "msg": "未授权"} record_id = _resolve_record_id(data.recognition_id, data.recognition_record_id) user_id = getattr(user, "user_id", None) user_code = _get_user_code(user) logger.info( "delete_recognition_record start: user_id=%s user_code=%s record_id=%s", user_id, user_code, record_id, ) if not record_id: logger.warning("delete_recognition_record invalid request: missing record id") return {"statusCode": 422, "msg": "recognition_id 不能为空"} affected_rows = ( db.query(RecognitionRecord) .filter( RecognitionRecord.id == record_id, RecognitionRecord.user_id == user_id, ) .update({"is_deleted": 1, "deleted_at": int(time.time())}) ) db.commit() logger.info( "delete_recognition_record success: user_id=%s user_code=%s record_id=%s affected_rows=%s", user_id, user_code, record_id, affected_rows, ) return {"statusCode": 200, "msg": "删除成功"} class EvaluationRequest(BaseModel): id: int scene_match: Optional[int] = None tip_accuracy: Optional[int] = None effect_evaluation: Optional[int] = None user_remark: Optional[str] = None @router.post("/submit_evaluation") async def submit_evaluation(data: EvaluationRequest, db: Session = Depends(get_db)): logger.info( "submit_evaluation start: id=%s scene_match=%s tip_accuracy=%s effect_evaluation=%s has_user_remark=%s", data.id, data.scene_match, data.tip_accuracy, data.effect_evaluation, data.user_remark is not None, ) record = ( db.query(RecognitionRecord) .filter(RecognitionRecord.id == data.id, RecognitionRecord.is_deleted == 0) .first() ) if not record: logger.warning("submit_evaluation record not found: id=%s", data.id) return {"statusCode": 404, "msg": "记录不存在"} if data.scene_match is not None: record.scene_match = data.scene_match if data.tip_accuracy is not None: record.tip_accuracy = data.tip_accuracy if data.effect_evaluation is not None: record.effect_evaluation = data.effect_evaluation if data.user_remark is not None: record.user_remark = data.user_remark record.updated_at = int(time.time()) db.commit() logger.info("submit_evaluation success: id=%s", data.id) return {"statusCode": 200, "msg": "success"} @router.get("/get_latest_recognition_record") async def get_latest_recognition_record( request: Request, db: Session = Depends(get_db) ): user = request.state.user if not user: logger.warning("get_latest_recognition_record unauthorized: missing request.state.user") return {"statusCode": 401, "msg": "未授权"} user_id = getattr(user, "user_id", None) user_code = _get_user_code(user) logger.info("get_latest_recognition_record start: user_id=%s user_code=%s", user_id, user_code) record = ( db.query(RecognitionRecord) .filter( RecognitionRecord.user_id == user_id, RecognitionRecord.is_deleted == 0, ) .order_by(RecognitionRecord.created_at.desc()) .first() ) if not record: return { "statusCode": 200, "msg": "success", "data": {"effect_evaluation": 1}, } return { "statusCode": 200, "msg": "success", "data": { "id": record.id, "title": record.title, "original_image_url": record.original_image_url, "recognition_image_url": record.recognition_image_url, "labels": record.labels, "created_at": record.created_at, "effect_evaluation": record.effect_evaluation, }, } class SceneTemplateCreate(BaseModel): scene_name: str scene_type: str scene_desc: str = "" model_name: str @router.post("/scene_template") async def create_scene_template( data: SceneTemplateCreate, db: Session = Depends(get_db) ): template = SceneTemplate( scene_name=data.scene_name, scene_type=data.scene_type, scene_desc=data.scene_desc, model_name=data.model_name, created_at=int(time.time()), updated_at=int(time.time()), is_deleted=0, ) db.add(template) db.commit() db.refresh(template) return { "statusCode": 200, "msg": "创建成功", "data": {"id": template.id}, } @router.get("/scene_templates") async def get_scene_templates( page: int = 1, page_size: int = 20, db: Session = Depends(get_db), ): if page_size > 100: page_size = 100 offset = (page - 1) * page_size total = db.query(SceneTemplate).filter(SceneTemplate.is_deleted == 0).count() templates = ( db.query(SceneTemplate) .filter(SceneTemplate.is_deleted == 0) .order_by(SceneTemplate.created_at.desc()) .offset(offset) .limit(page_size) .all() ) return { "statusCode": 200, "msg": "success", "data": { "total": total, "items": [ { "id": template.id, "scene_name": template.scene_name, "scene_type": template.scene_type, "scene_desc": template.scene_desc, "model_name": template.model_name, "created_at": template.created_at, } for template in templates ], }, } @router.get("/recognition_records") async def get_recognition_records( request: Request, scene_type: str = "", page: int = 1, page_size: int = 20, db: Session = Depends(get_db), ): user = request.state.user if not user: return {"statusCode": 401, "msg": "未授权"} if page_size > 100: page_size = 100 user_id = getattr(user, "user_id", None) query = db.query(RecognitionRecord).filter( RecognitionRecord.user_id == user_id, RecognitionRecord.is_deleted == 0, ) if scene_type: query = query.filter(RecognitionRecord.scene_type == scene_type) total = query.count() offset = (page - 1) * page_size records = ( query.order_by(RecognitionRecord.created_at.desc()) .offset(offset) .limit(page_size) .all() ) return { "statusCode": 200, "msg": "success", "data": { "total": total, "items": [ { "id": record.id, "scene_type": record.scene_type, "original_image_url": record.original_image_url, "result_image_url": record.recognition_image_url, "hazard_count": record.hazard_count, "current_step": record.current_step, "created_at": record.created_at, } for record in records ], }, }