""" H5 工单处理 API 供企微 H5 工单详情页调用: - GET /auth — 企微 OAuth2 code 换 userid - GET /detail — 获取告警+工单详情 - POST /submit — 提交处理结果(描述+图片) - POST /upload-image — 上传处理后照片到 COS """ from fastapi import APIRouter, Query, UploadFile, File from pydantic import BaseModel from typing import Optional, List import httpx from app.config import settings 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("/auth") async def wechat_oauth( code: str = Query(..., description="企微 OAuth2 授权码"), ): """企微 OAuth2:用 code 换取 userid""" try: # 1. 获取 access_token from app.services.wechat_service import get_wechat_service wechat = get_wechat_service() access_token = await wechat._get_access_token() # 2. 用 code 换 userid url = f"https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={access_token}&code={code}" async with httpx.AsyncClient(timeout=10) as client: resp = await client.get(url) data = resp.json() if data.get("errcode", 0) != 0: logger.warning(f"OAuth2 获取用户失败: {data}") return {"code": -1, "msg": data.get("errmsg", "授权失败"), "data": None} userid = data.get("UserId") or data.get("userid") or "" if not userid: return {"code": -1, "msg": "非企业成员", "data": None} logger.info(f"OAuth2 认证成功: userid={userid}") return {"code": 0, "msg": "success", "data": {"userId": userid}} except Exception as e: logger.error(f"OAuth2 认证异常: {e}") return {"code": -1, "msg": str(e), "data": 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}"}