# routers/hazard.py 接口测试文档 ## 文件功能概述 该文件提供隐患识别相关的接口,包括: - 基于 YOLO 的图片隐患识别(下载图片 → AI 检测 → 绘制标注框和水印 → 上传结果) - 保存识别步骤进度 所有接口均需要 Token 认证。 路由前缀:`/apiv1`(以 `routers/__init__.py` 中注册为准) --- ## 接口列表 --- ### 1. POST `/apiv1/hazard` — 隐患识别 **功能说明:** 接收一张图片的 OSS URL,执行以下完整流程: 1. 解密 OSS 代理 URL(失败则直接使用原 URL) 2. 通过 HTTP 下载图片到内存 3. 调用 YOLO 服务进行隐患检测 4. 在图片上绘制检测边界框(红色矩形 + 标签 + 置信度) 5. 添加 45 度角平铺水印(用户名、账号、日期) 6. 将标注后的结果图片上传至 OSS 7. 在数据库中插入 `RecognitionRecord` 记录 8. 返回识别结果(隐患数量、详情、结果图片 URL) **是否需要认证:** 是 **请求方式:** POST **请求体(JSON):** | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | image_url | string | 是 | — | 图片的 OSS URL(可能是加密的代理 URL) | | scene_type | string | 否 | "" | 场景类型(如 "隧道施工"、"桥梁施工") | | user_name | string | 否 | "" | 用户名(用于水印,不传则使用 user.account) | | user_account | string | 否 | "" | 用户账号(用于请求体传递,实际水印使用 user.account) | **成功响应字段:** | 字段 | 类型 | 说明 | |------|------|------| | data.record_id | int | 识别记录 ID | | data.hazard_count | int | 检测到的隐患数量 | | data.hazards | array | 隐患详情列表,每项含 bbox, label, confidence | | data.result_image_url | string | 标注后的结果图片 OSS URL | | data.original_image_url | string | 原始图片 URL(与请求中的一致) | **测试用例:** #### 用例 1:正常识别(有隐患) ```json // 请求 POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "image_url": "encrypted_oss_url_of_construction_site_image", "scene_type": "隧道施工", "user_name": "张三", "user_account": "zhangsan" } // 预期响应 (HTTP 200) { "statusCode": 200, "msg": "识别成功", "data": { "record_id": 1, "hazard_count": 3, "hazards": [ { "bbox": [100, 150, 300, 400], "label": "未佩戴安全帽", "confidence": 0.92 }, { "bbox": [500, 200, 650, 350], "label": "临边防护缺失", "confidence": 0.85 }, { "bbox": [200, 400, 400, 550], "label": "材料堆放不规范", "confidence": 0.78 } ], "result_image_url": "https://oss.example.com/hazard_detection/123/1700000000.jpg", "original_image_url": "encrypted_oss_url_of_construction_site_image" } } ``` #### 用例 2:正常识别(无隐患) ```json // 请求 POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "image_url": "encrypted_oss_url_of_safe_image", "scene_type": "桥梁施工" } // 预期响应 (HTTP 200) { "statusCode": 200, "msg": "识别成功", "data": { "record_id": 2, "hazard_count": 0, "hazards": [], "result_image_url": "https://oss.example.com/hazard_detection/123/1700000001.jpg", "original_image_url": "encrypted_oss_url_of_safe_image" } } ``` #### 用例 3:图片 URL 无效(下载失败) ```json // 请求 POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "image_url": "https://invalid-url.example.com/nonexistent.jpg", "scene_type": "隧道施工" } // 预期响应 (HTTP 200, 业务码 500) { "statusCode": 500, "msg": "图片下载失败: " } ``` #### 用例 4:YOLO 服务异常 ```json // 请求(YOLO 服务不可用时) POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "image_url": "valid_encrypted_oss_url", "scene_type": "隧道施工" } // 预期响应 (HTTP 200, 业务码 500) { "statusCode": 500, "msg": "处理失败: " } ``` #### 用例 5:未认证 ```json // 请求 POST /apiv1/hazard // 预期响应 (HTTP 401) { "statusCode": 401, "msg": "未提供认证Token" } ``` #### 用例 6:缺少必填字段 image_url ```json // 请求 POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "scene_type": "隧道施工" } // 预期响应 (HTTP 422) { "detail": [ { "loc": ["body", "image_url"], "msg": "field required", "type": "value_error.missing" } ] } ``` #### 用例 7:不传 user_name(使用 user.account 作为水印) ```json // 请求 POST /apiv1/hazard token: <有效Token> Content-Type: application/json { "image_url": "valid_encrypted_oss_url", "scene_type": "隧道施工" } // 预期响应 (HTTP 200) // 水印使用 token 中用户的 account 字段 { "statusCode": 200, "msg": "识别成功", "data": { "record_id": 3, "hazard_count": 1, "hazards": [...], "result_image_url": "https://oss.example.com/hazard_detection/123/1700000002.jpg", "original_image_url": "valid_encrypted_oss_url" } } ``` --- ### 2. POST `/apiv1/save_step` — 保存识别步骤 **功能说明:** 更新指定隐患识别记录的当前步骤编号。用于前端多步骤流程中保存用户当前进度。 **是否需要认证:** 是 **请求方式:** POST **请求体(JSON):** | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | record_id | int | 是 | 识别记录 ID | | current_step | int | 是 | 当前步骤编号 | **业务逻辑:** - 通过 `record_id` + `user_id`(当前登录用户)双重条件查找 `RecognitionRecord` - 更新 `current_step` 和 `updated_at` 字段 - 若未找到匹配记录(记录不存在或不属于当前用户),返回 404 - 异常时执行数据库回滚 **测试用例:** #### 用例 1:正常保存步骤 ```json // 请求 POST /apiv1/save_step token: <有效Token> Content-Type: application/json { "record_id": 1, "current_step": 2 } // 预期响应 (HTTP 200) { "statusCode": 200, "msg": "保存成功", "data": { "record_id": 1, "current_step": 2 } } ``` #### 用例 2:更新到最后一步 ```json // 请求 POST /apiv1/save_step token: <有效Token> Content-Type: application/json { "record_id": 1, "current_step": 5 } // 预期响应 (HTTP 200) { "statusCode": 200, "msg": "保存成功", "data": { "record_id": 1, "current_step": 5 } } ``` #### 用例 3:记录不存在 ```json // 请求 POST /apiv1/save_step token: <有效Token> Content-Type: application/json { "record_id": 99999, "current_step": 2 } // 预期响应 (HTTP 200, 业务码 404) { "statusCode": 404, "msg": "记录不存在" } ``` #### 用例 4:记录属于其他用户(权限隔离) ```json // 请求(用用户 A 的 Token 修改用户 B 的记录) POST /apiv1/save_step token: <用户A的Token> Content-Type: application/json { "record_id": 5, "current_step": 3 } // 预期响应 (HTTP 200, 业务码 404) // 因为 user_id 不匹配,查询结果为空 { "statusCode": 404, "msg": "记录不存在" } ``` #### 用例 5:未认证 ```json // 请求 POST /apiv1/save_step // 预期响应 (HTTP 401) { "statusCode": 401, "msg": "未提供认证Token" } ``` #### 用例 6:缺少必填字段 ```json // 请求 POST /apiv1/save_step token: <有效Token> Content-Type: application/json { "record_id": 1 } // 预期响应 (HTTP 422) { "detail": [ { "loc": ["body", "current_step"], "msg": "field required", "type": "value_error.missing" } ] } ``` #### 用例 7:数据库异常 ```json // 预期响应 (HTTP 200, 业务码 500) { "statusCode": 500, "msg": "保存失败: <具体错误信息>" } ``` > 注意:此时代码会执行 `db.rollback()` 回滚事务。 --- ## 内部函数说明 ### `_draw_boxes_and_watermark(image_bytes, hazards, user_name, user_account)` — 绘制标注和水印 **功能:** 在图片上绘制 YOLO 检测的边界框和 45 度角平铺水印。 **参数:** | 参数 | 类型 | 说明 | |------|------|------| | image_bytes | bytes | 原始图片二进制数据 | | hazards | list | YOLO 检测结果列表,每项含 `bbox`(4 元素列表)、`label`、`confidence` | | user_name | string | 用户名(水印文本之一) | | user_account | string | 用户账号(水印文本之一) | **返回:** `bytes` — 处理后的 JPEG 图片二进制数据 **处理逻辑:** 1. 用 PIL 打开图片,转为 RGBA 模式 2. 创建透明覆盖层,为每个隐患绘制红色矩形框和标签文字 3. 创建水印层,以 45 度角平铺三行水印文本(用户名、账号、日期) 4. 旋转水印层后与原图合成 5. 转为 RGB 保存为 JPEG(quality=95) 6. 如处理失败,返回原始图片字节 > 该函数为内部函数,不直接暴露为 API 接口。 --- ## 依赖说明 | 依赖项 | 说明 | |--------|------| | `database.get_db` | SQLAlchemy 数据库会话 | | `models.scene.RecognitionRecord` | 识别记录模型(字段:id, user_id, scene_type, original_image_url, recognition_image_url, hazard_count, hazard_details, current_step, created_at, updated_at, is_deleted) | | `services.yolo_service` | YOLO 目标检测服务,提供 `detect_hazards(url, scene_type)` 方法 | | `services.oss_service` | OSS 存储服务,提供 `upload_bytes(data, filename)` 方法 | | `utils.crypto.decrypt_url` | URL 解密工具函数 | | `PIL (Pillow)` | 图片处理库 | | `httpx` | 异步 HTTP 客户端,用于下载图片 | | `request.state.user` | 从中间件注入的用户信息(含 user_id, account 等) | ## 代码备注 1. `hazard` 接口的 URL 解密有容错处理:`decrypt_url` 失败时直接使用原始 URL,不会抛出异常。 2. `save_step` 接口通过 `record_id + user_id` 双重过滤实现了用户间的数据隔离——用户只能修改自己的记录。 3. 水印字体加载有三级回退:Linux DejaVu → Windows 微软雅黑 → PIL 默认字体。 4. 图片处理失败时(`_draw_boxes_and_watermark` 异常),返回原始未标注图片,不会导致整个接口失败。 5. `hazard_details` 字段以 JSON 字符串形式存储在数据库中(`json.dumps`,`ensure_ascii=False`)。 6. 结果图片以 JPEG 格式保存,quality=95,存储路径为 `hazard_detection/{user_id}/{timestamp}.jpg`。