功能:H5 工单处理 API(detail/submit/upload-image)

- GET /api/work-order/detail — 告警+工单详情
- POST /api/work-order/submit — 提交处理结果,调 IoT /submit
- POST /api/work-order/upload-image — 上传照片到 COS
- IoT 失败时降级直接更新告警状态
This commit is contained in:
2026-03-23 12:00:04 +08:00
parent d9ad321f24
commit be059b47b0
2 changed files with 198 additions and 0 deletions

View File

@@ -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)
# 注册芋道格式异常处理器

View File

@@ -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}"}