rating_report.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. from fastapi import APIRouter, HTTPException, Depends, Query
  2. from mysql.connector.pooling import PooledMySQLConnection
  3. from datetime import datetime
  4. from app.core.logger import get_logger
  5. from app.core.config import settings
  6. from app.core.database_loader import get_db_connection
  7. from app.crud import crud_card
  8. from app.utils.rating_report_utils import (get_active_json, save_rating_report_history,
  9. format_datetime, parse_json_value, crop_defect_image)
  10. logger = get_logger(__name__)
  11. router = APIRouter()
  12. @router.get("/generate", status_code=200, summary="生成评级报告数据")
  13. def generate_rating_report(
  14. cardNo: str,
  15. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  16. ):
  17. if not cardNo or not cardNo.strip():
  18. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  19. # 根据cardNo 查询id
  20. try:
  21. with db_conn.cursor(buffered=True) as cursor:
  22. query_sql = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE cardNo = %s LIMIT 1"
  23. cursor.execute(query_sql, (cardNo,))
  24. row = cursor.fetchone()
  25. except Exception as e:
  26. logger.error(f"创建卡牌失败: {e}")
  27. raise HTTPException(status_code=500, detail="数据库查询失败。")
  28. if not row:
  29. raise HTTPException(
  30. status_code=404,
  31. detail=f"未找到卡牌编号为 {cardNo} 的相关记录"
  32. )
  33. card_id = row[0]
  34. rating_time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
  35. # top_n_defects = 3
  36. """
  37. 根据 Card ID 生成评级报告 JSON
  38. """
  39. # 1. 获取卡片详情 (复用 Crud 逻辑,确保能拿到所有图片)
  40. card_data = crud_card.get_card_with_details(db_conn, card_id)
  41. if not card_data:
  42. raise HTTPException(status_code=404, detail="未找到该卡片信息")
  43. # 初始化返回结构
  44. response_data = {
  45. "backImageUrl": "",
  46. "frontImageUrl": "",
  47. "cardNo": cardNo,
  48. "centerBack": "",
  49. "centerFront": "",
  50. "measureLength": 0.0,
  51. "measureWidth": 0.0,
  52. "cornerBackNum": 0,
  53. "sideBackNum": 0,
  54. "surfaceBackNum": 0,
  55. "cornerFrontNum": 0,
  56. "sideFrontNum": 0,
  57. "surfaceFrontNum": 0,
  58. "scoreThreshold": float(card_data.get("modified_score") or 0),
  59. "evaluateNo": str(card_data.get("id")),
  60. "defectDetailList": []
  61. }
  62. # 临时列表用于收集所有缺陷,最后排序取 Top N
  63. all_defects_collected = []
  64. # 遍历图片寻找 Front Ring 和 Back Ring
  65. images = card_data.get("images", [])
  66. # 辅助字典:defect_type 到 统计字段 的映射
  67. defect_map_keys = {
  68. "front_ring": {
  69. "corner": "cornerFrontNum",
  70. "edge": "sideFrontNum",
  71. "face": "surfaceFrontNum"
  72. },
  73. "back_ring": {
  74. "corner": "cornerBackNum",
  75. "edge": "sideBackNum",
  76. "face": "surfaceBackNum"
  77. }
  78. }
  79. for img in images:
  80. img_type = img.image_type
  81. # 只处理环光图
  82. if img_type not in ["front_ring", "back_ring"]:
  83. continue
  84. # 设置主图 URL
  85. if img_type == "front_ring":
  86. response_data["frontImageUrl"] = img.image_path
  87. elif img_type == "back_ring":
  88. response_data["backImageUrl"] = img.image_path
  89. # 获取有效 JSON
  90. json_data = get_active_json(img)
  91. if not json_data or "result" not in json_data:
  92. continue
  93. result_node = json_data["result"]
  94. # 1. 处理居中 (Center)
  95. center_inf = result_node.get("center_result", {}).get("box_result", {}).get("center_inference", {})
  96. if center_inf:
  97. # 格式: L/R=47/53, T/B=51/49 (取整)
  98. # center_inference 包含 center_left, center_right, center_top, center_bottom
  99. c_str = (
  100. f"L/R={int(round(center_inf.get('center_left', 0)))}/{int(round(center_inf.get('center_right', 0)))}, "
  101. f"T/B={int(round(center_inf.get('center_top', 0)))}/{int(round(center_inf.get('center_bottom', 0)))}"
  102. )
  103. if img_type == "front_ring":
  104. response_data["centerFront"] = c_str
  105. # 2. 处理尺寸 (仅从正面取,或者只要有就取) - mm 转 cm,除以 10,保留2位
  106. rw_mm = center_inf.get("real_width_mm", 0)
  107. rh_mm = center_inf.get("real_height_mm", 0)
  108. response_data["measureWidth"] = round(rw_mm / 10.0, 2)
  109. response_data["measureLength"] = round(rh_mm / 10.0, 2)
  110. else:
  111. response_data["centerBack"] = c_str
  112. # 2. 处理缺陷 (Defects)
  113. defects = result_node.get("defect_result", {}).get("defects", [])
  114. for defect in defects:
  115. # 过滤 edit_type == 'del'
  116. if defect.get("edit_type") == "del":
  117. continue
  118. d_type = defect.get("defect_type", "") # corner, edge, face
  119. d_label = defect.get("label", "") # scratch, wear, etc.
  120. # 统计数量
  121. count_key = defect_map_keys.get(img_type, {}).get(d_type)
  122. if count_key:
  123. response_data[count_key] += 1
  124. # 收集详细信息用于 Top N 列表
  125. # 需要保存:缺陷对象本身,图片路径,正反面标识
  126. side_str = "FRONT" if img_type == "front_ring" else "BACK"
  127. all_defects_collected.append({
  128. "defect_data": defect,
  129. "image_path": img.image_path,
  130. "side": side_str,
  131. "area": defect.get("actual_area", 0)
  132. })
  133. # 3. 处理 defectDetailList (Top N 切图)
  134. # 按实际面积从大到小排序
  135. all_defects_collected.sort(key=lambda x: x["area"], reverse=True)
  136. # top_defects = all_defects_collected[:top_n_defects]
  137. top_defects = all_defects_collected
  138. final_defect_list = []
  139. for idx, item in enumerate(top_defects, start=1):
  140. defect = item["defect_data"]
  141. side = item["side"]
  142. original_img_path = item["image_path"]
  143. # 构造 ID
  144. d_id = idx # 1, 2, 3
  145. # 构造文件名: {card_id}_{seq_id}.jpg
  146. filename = f"{card_id}_{d_id}_{rating_time_now}.jpg"
  147. # 执行切图
  148. min_rect = defect.get("min_rect")
  149. defect_img_url = ""
  150. location_str = ""
  151. if min_rect and len(min_rect) == 3:
  152. # 切图并保存
  153. defect_img_url = crop_defect_image(original_img_path, min_rect, filename)
  154. # 计算 Location (中心坐标)
  155. # min_rect[0] 是 [x, y]
  156. cx, cy = min_rect[0]
  157. location_str = f"{int(cx)},{int(cy)}"
  158. # 构造 Type 字符串: defect_type + label (大写)
  159. # 例如: defect_type="edge", label="wear" -> "EDGE WEAR"
  160. d_type_raw = defect.get("defect_type", "")
  161. # d_label_raw = defect.get("label", "")
  162. type_str = f"{d_type_raw.upper()}".strip()
  163. type_str_map = {"CORNER": "CORNER",
  164. "EDGE": "SIDE",
  165. "FACE": "SURFACE"}
  166. type_str = type_str_map.get(type_str)
  167. final_defect_list.append({
  168. "id": d_id,
  169. "side": side,
  170. "location": location_str,
  171. "type": type_str,
  172. "defectImgUrl": defect_img_url
  173. })
  174. response_data["defectDetailList"] = final_defect_list
  175. report_name = f"{cardNo}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  176. rating_id = save_rating_report_history(db_conn, card_id, cardNo, report_name, response_data)
  177. response_data["ratingId"] = rating_id
  178. response_data["reportName"] = report_name
  179. return response_data
  180. @router.get("/history", status_code=200, summary="根据 cardNo 查询评级报告历史列表")
  181. def get_rating_report_history_list(
  182. cardNo: str,
  183. skip: int = Query(0, ge=0),
  184. page_num: int = Query(None, ge=1),
  185. limit: int = Query(100, ge=1, le=1000),
  186. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  187. ):
  188. if not cardNo or not cardNo.strip():
  189. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  190. if page_num is not None:
  191. skip = (page_num - 1) * limit
  192. try:
  193. with db_conn.cursor(dictionary=True) as cursor:
  194. count_sql = (
  195. f"SELECT COUNT(*) AS total "
  196. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  197. "WHERE cardNo = %s"
  198. )
  199. cursor.execute(count_sql, (cardNo,))
  200. count_row = cursor.fetchone() or {}
  201. total_count = int(count_row.get("total") or 0)
  202. query_sql = (
  203. f"SELECT rating_id, card_id, cardNo, report_name, created_at "
  204. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  205. "WHERE cardNo = %s "
  206. "ORDER BY rating_id DESC "
  207. "LIMIT %s OFFSET %s"
  208. )
  209. cursor.execute(query_sql, (cardNo, limit, skip))
  210. rows = cursor.fetchall()
  211. except Exception as e:
  212. logger.error(f"查询评级报告历史列表失败: {e}")
  213. raise HTTPException(status_code=500, detail="查询评级报告历史列表失败。")
  214. history_list = []
  215. for row in rows:
  216. history_list.append({
  217. "ratingId": row.get("rating_id"),
  218. "cardId": row.get("card_id"),
  219. "cardNo": row.get("cardNo"),
  220. "reportName": row.get("report_name"),
  221. "createdAt": format_datetime(row.get("created_at"))
  222. })
  223. return {
  224. "cardNo": cardNo,
  225. "data": {
  226. "total": total_count,
  227. "list": history_list
  228. }
  229. }
  230. @router.get("/history/{rating_id}", status_code=200, summary="根据 rating_id 查询单个评级报告历史")
  231. def get_rating_report_history_detail(
  232. rating_id: int,
  233. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  234. ):
  235. try:
  236. with db_conn.cursor(dictionary=True) as cursor:
  237. query_sql = (
  238. f"SELECT rating_id, card_id, cardNo, report_name, report_json, created_at, updated_at "
  239. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  240. "WHERE rating_id = %s LIMIT 1"
  241. )
  242. cursor.execute(query_sql, (rating_id,))
  243. row = cursor.fetchone()
  244. except Exception as e:
  245. logger.error(f"查询评级报告历史详情失败: {e}")
  246. raise HTTPException(status_code=500, detail="查询评级报告历史详情失败。")
  247. if not row:
  248. raise HTTPException(status_code=404, detail=f"未找到 rating_id={rating_id} 的历史记录")
  249. return {
  250. "ratingId": row.get("rating_id"),
  251. "cardId": row.get("card_id"),
  252. "cardNo": row.get("cardNo"),
  253. "reportName": row.get("report_name"),
  254. "createdAt": format_datetime(row.get("created_at")),
  255. "updatedAt": format_datetime(row.get("updated_at")),
  256. "reportData": parse_json_value(row.get("report_json"))
  257. }