From be059b47b069bb3492cf765f420f9e044261488d Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Mon, 23 Mar 2026 12:00:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9AH5=20=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E5=A4=84=E7=90=86=20API=EF=BC=88detail/submit/upload-?= =?UTF-8?q?image=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/work-order/detail — 告警+工单详情 - POST /api/work-order/submit — 提交处理结果,调 IoT /submit - POST /api/work-order/upload-image — 上传照片到 COS - IoT 失败时降级直接更新告警状态 --- app/main.py | 2 + app/routers/work_order_api.py | 196 ++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 app/routers/work_order_api.py diff --git a/app/main.py b/app/main.py index d723945..5d7ba42 100644 --- a/app/main.py +++ b/app/main.py @@ -31,6 +31,7 @@ from app.routers import yudao_alert_router, yudao_auth_router, yudao_aiot_alarm_ from app.routers.wechat_callback import router as wechat_callback_router from app.routers.notify_manage import router as notify_manage_router from app.routers.wechat_notify_api import router as wechat_notify_router +from app.routers.work_order_api import router as work_order_router from app.yudao_compat import yudao_exception_handler import json @@ -113,6 +114,7 @@ app.include_router(edge_compat_router) # ==================== 企微回调 + 通知管理路由 ==================== app.include_router(wechat_callback_router) app.include_router(wechat_notify_router) +app.include_router(work_order_router) app.include_router(notify_manage_router) # 注册芋道格式异常处理器 diff --git a/app/routers/work_order_api.py b/app/routers/work_order_api.py new file mode 100644 index 0000000..af880de --- /dev/null +++ b/app/routers/work_order_api.py @@ -0,0 +1,196 @@ +""" +H5 工单处理 API + +供企微 H5 工单详情页调用: +- GET /detail — 获取告警+工单详情 +- POST /submit — 提交处理结果(描述+图片) +- POST /upload-image — 上传处理后照片到 COS +""" + +from fastapi import APIRouter, Query, UploadFile, File +from pydantic import BaseModel +from typing import Optional, List + +from app.utils.logger import logger + +router = APIRouter(prefix="/api/work-order", tags=["H5工单处理"]) + + +class SubmitRequest(BaseModel): + alarmId: str + result: str + resultImgUrls: Optional[List[str]] = None + + +@router.get("/detail") +async def get_work_order_detail( + alarmId: str = Query(..., description="告警ID"), +): + """获取工单详情(告警信息+工单状态+截图)""" + from app.services.alarm_event_service import get_alarm_event_service + from app.services.camera_name_service import get_camera_name_service + from app.services.oss_storage import get_oss_storage + from app.models import get_session, AlarmEventExt + + service = get_alarm_event_service() + alarm_dict = service.get_alarm(alarmId) + if not alarm_dict: + return {"code": 404, "msg": "告警不存在", "data": None} + + # 查工单 ID + order_id = "" + db = get_session() + try: + ext = db.query(AlarmEventExt).filter( + AlarmEventExt.alarm_id == alarmId, + AlarmEventExt.ext_type == "WORK_ORDER", + ).first() + if ext and ext.ext_data: + order_id = ext.ext_data.get("order_id", "") + except Exception: + pass + finally: + db.close() + + # 摄像头名称 + camera_name = alarm_dict.get("device_id", "") + try: + camera_service = get_camera_name_service() + info = await camera_service.get_camera_info(alarm_dict.get("device_id", "")) + camera_name = camera_service.format_display_name(alarm_dict.get("device_id", ""), info) + except Exception: + pass + + # 截图 URL + snapshot_url = "" + if alarm_dict.get("snapshot_url"): + try: + oss = get_oss_storage() + snapshot_url = oss.get_presigned_url(alarm_dict["snapshot_url"]) + except Exception: + snapshot_url = alarm_dict.get("snapshot_url", "") + + # 状态映射 + status_map = { + "NEW": "pending", + "CONFIRMED": "processing", + "CLOSED": "completed", + "FALSE": "false_alarm", + } + + type_names = { + "leave_post": "人员离岗", + "intrusion": "周界入侵", + "illegal_parking": "车辆违停", + "vehicle_congestion": "车辆拥堵", + } + level_names = {0: "紧急", 1: "重要", 2: "普通", 3: "轻微"} + + event_time = alarm_dict.get("event_time", "") + if event_time: + try: + event_time = event_time.strftime("%m-%d %H:%M") + except Exception: + event_time = str(event_time)[:16] + + return { + "code": 0, + "msg": "success", + "data": { + "alarmId": alarmId, + "orderId": order_id, + "status": status_map.get(alarm_dict.get("alarm_status", ""), "pending"), + "alarmType": type_names.get(alarm_dict.get("alarm_type", ""), alarm_dict.get("alarm_type", "")), + "alarmLevel": level_names.get(alarm_dict.get("alarm_level"), "普通"), + "cameraName": camera_name, + "eventTime": event_time, + "snapshotUrl": snapshot_url, + "handler": alarm_dict.get("handler", ""), + "handleRemark": alarm_dict.get("handle_remark", ""), + "handledAt": str(alarm_dict.get("handled_at", "")) if alarm_dict.get("handled_at") else "", + }, + } + + +@router.post("/submit") +async def submit_work_order(req: SubmitRequest): + """保安提交处理结果(描述+图片),调 IoT /submit 结单""" + from app.services.work_order_client import get_work_order_client + from app.services.alarm_event_service import get_alarm_event_service + from app.models import get_session, AlarmEventExt + + # 查工单 ID + order_id = "" + db = get_session() + try: + ext = db.query(AlarmEventExt).filter( + AlarmEventExt.alarm_id == req.alarmId, + AlarmEventExt.ext_type == "WORK_ORDER", + ).first() + if ext and ext.ext_data: + order_id = ext.ext_data.get("order_id", "") + except Exception: + pass + finally: + db.close() + + if not order_id: + # 工单不存在时降级直接更新告警状态 + service = get_alarm_event_service() + service.handle_alarm( + alarm_id=req.alarmId, + alarm_status="CLOSED", + handle_status="DONE", + remark=req.result, + ) + return {"code": 0, "msg": "已更新(无关联工单)"} + + # 调 IoT /submit + wo_client = get_work_order_client() + if wo_client.enabled: + success = await wo_client.submit_order( + order_id=order_id, + result=req.result, + result_img_urls=req.resultImgUrls, + ) + if success: + logger.info(f"H5工单提交成功: alarm={req.alarmId}, order={order_id}") + return {"code": 0, "msg": "提交成功,等待系统确认"} + else: + # IoT 失败降级 + service = get_alarm_event_service() + service.handle_alarm( + alarm_id=req.alarmId, + alarm_status="CLOSED", + handle_status="DONE", + remark=req.result, + ) + return {"code": 0, "msg": "已更新(IoT同步失败,已降级处理)"} + else: + service = get_alarm_event_service() + service.handle_alarm( + alarm_id=req.alarmId, + alarm_status="CLOSED", + handle_status="DONE", + remark=req.result, + ) + return {"code": 0, "msg": "已更新(工单未启用)"} + + +@router.post("/upload-image") +async def upload_image(file: UploadFile = File(...)): + """上传处理后照片到 COS,返回永久 URL""" + from app.services.oss_storage import get_oss_storage + import time + + try: + oss = get_oss_storage() + content = await file.read() + object_key = f"work-order/{int(time.time())}_{file.filename}" + oss.upload_file(content, object_key, content_type=file.content_type or "image/jpeg") + url = oss.get_permanent_url(object_key) + + return {"code": 0, "msg": "success", "data": {"url": url, "objectKey": object_key}} + except Exception as e: + logger.error(f"工单图片上传失败: {e}") + return {"code": -1, "msg": f"上传失败: {e}"}