rating_report.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. from datetime import datetime
  2. from fastapi import APIRouter, Depends, HTTPException, Query
  3. from mysql.connector.pooling import PooledMySQLConnection
  4. from app.core.config import settings
  5. from app.core.database_loader import get_db_connection
  6. from app.core.logger import get_logger
  7. from app.crud import crud_card
  8. from app.utils.rating_report_utils import (
  9. build_defect_compare_key,
  10. crop_defect_image,
  11. format_datetime,
  12. get_active_json,
  13. parse_json_value,
  14. remove_common_defects,
  15. save_rating_report_history,
  16. )
  17. logger = get_logger(__name__)
  18. router = APIRouter()
  19. def _build_history_detail_response(row: dict, report_data: dict) -> dict:
  20. """
  21. 统一历史详情返回结构,避免不同接口拼装不一致。
  22. """
  23. return {
  24. "ratingId": row.get("rating_id"),
  25. "cardId": row.get("card_id"),
  26. "cardNo": row.get("cardNo"),
  27. "reportName": row.get("report_name"),
  28. "createdAt": format_datetime(row.get("created_at")),
  29. "updatedAt": format_datetime(row.get("updated_at")),
  30. "reportData": report_data,
  31. }
  32. @router.get("/generate", status_code=200, summary="生成评级报告数据")
  33. def generate_rating_report(
  34. cardNo: str,
  35. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  36. ):
  37. if not cardNo or not cardNo.strip():
  38. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  39. try:
  40. with db_conn.cursor(buffered=True) as cursor:
  41. query_sql = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE cardNo = %s LIMIT 1"
  42. cursor.execute(query_sql, (cardNo,))
  43. row = cursor.fetchone()
  44. except Exception as e:
  45. logger.error(f"根据 cardNo 查询卡牌失败: cardNo={cardNo}, error={e}")
  46. raise HTTPException(status_code=500, detail="数据库查询失败")
  47. if not row:
  48. raise HTTPException(status_code=404, detail=f"未找到 cardNo={cardNo} 的卡牌记录")
  49. card_id = row[0]
  50. rating_time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
  51. logger.info(f"开始生成评级报告: cardNo={cardNo}, card_id={card_id}")
  52. # 复用卡牌详情查询逻辑,直接拿到当前用于生成报告的图片和缺陷数据。
  53. card_data = crud_card.get_card_with_details(db_conn, card_id)
  54. if not card_data:
  55. raise HTTPException(status_code=404, detail="未找到该卡牌信息")
  56. response_data = {
  57. "backImageUrl": "",
  58. "frontImageUrl": "",
  59. "cardNo": cardNo,
  60. "centerBack": "",
  61. "centerFront": "",
  62. "measureLength": 0.0,
  63. "measureWidth": 0.0,
  64. "cornerBackNum": 0,
  65. "sideBackNum": 0,
  66. "surfaceBackNum": 0,
  67. "cornerFrontNum": 0,
  68. "sideFrontNum": 0,
  69. "surfaceFrontNum": 0,
  70. "scoreThreshold": float(card_data.get("modified_score") or 0),
  71. "evaluateNo": str(card_data.get("id")),
  72. "defectDetailList": []
  73. }
  74. # 先统一收集缺陷,再按面积排序
  75. all_defects_collected = []
  76. images = card_data.get("images", [])
  77. defect_map_keys = {
  78. "front_ring": {
  79. "corner": "cornerFrontNum",
  80. "edge": "sideFrontNum",
  81. "face": "surfaceFrontNum"
  82. },
  83. "back_ring": {
  84. "corner": "cornerBackNum",
  85. "edge": "sideBackNum",
  86. "face": "surfaceBackNum"
  87. }
  88. }
  89. for img in images:
  90. img_type = img.image_type
  91. if img_type not in ["front_ring", "back_ring"]:
  92. continue
  93. if img_type == "front_ring":
  94. response_data["frontImageUrl"] = img.image_path
  95. elif img_type == "back_ring":
  96. response_data["backImageUrl"] = img.image_path
  97. json_data = get_active_json(img)
  98. if not json_data or "result" not in json_data:
  99. continue
  100. result_node = json_data["result"]
  101. center_inf = result_node.get("center_result", {}).get("box_result", {}).get("center_inference", {})
  102. if center_inf:
  103. center_text = (
  104. f"L/R={int(round(center_inf.get('center_left', 0)))}/{int(round(center_inf.get('center_right', 0)))}, "
  105. f"T/B={int(round(center_inf.get('center_top', 0)))}/{int(round(center_inf.get('center_bottom', 0)))}"
  106. )
  107. if img_type == "front_ring":
  108. response_data["centerFront"] = center_text
  109. response_data["measureWidth"] = round(center_inf.get("real_width_mm", 0) / 10.0, 2)
  110. response_data["measureLength"] = round(center_inf.get("real_height_mm", 0) / 10.0, 2)
  111. else:
  112. response_data["centerBack"] = center_text
  113. defects = result_node.get("defect_result", {}).get("defects", [])
  114. for defect in defects:
  115. if defect.get("edit_type") == "del":
  116. continue
  117. defect_type = defect.get("defect_type", "")
  118. count_key = defect_map_keys.get(img_type, {}).get(defect_type)
  119. if count_key:
  120. response_data[count_key] += 1
  121. side_str = "FRONT" if img_type == "front_ring" else "BACK"
  122. all_defects_collected.append({
  123. "defect_data": defect,
  124. "image_path": img.image_path,
  125. "side": side_str,
  126. "area": defect.get("actual_area", 0)
  127. })
  128. all_defects_collected.sort(key=lambda item: item["area"], reverse=True)
  129. final_defect_list = []
  130. for idx, item in enumerate(all_defects_collected, start=1):
  131. defect = item["defect_data"]
  132. side = item["side"]
  133. original_img_path = item["image_path"]
  134. defect_id = idx
  135. filename = f"{card_id}_{defect_id}_{rating_time_now}.jpg"
  136. min_rect = defect.get("min_rect")
  137. defect_img_url = ""
  138. location_str = ""
  139. if min_rect and len(min_rect) == 3:
  140. defect_img_url = crop_defect_image(original_img_path, min_rect, filename)
  141. center_x, center_y = min_rect[0]
  142. location_str = f"{int(center_x)},{int(center_y)}"
  143. raw_type = f"{defect.get('defect_type', '').upper()}".strip()
  144. type_str_map = {
  145. "CORNER": "CORNER",
  146. "EDGE": "SIDE",
  147. "FACE": "SURFACE"
  148. }
  149. type_str = type_str_map.get(raw_type)
  150. # compareKey 使用面积、标签、正反面和 location。
  151. compare_key = build_defect_compare_key(defect, side, location_str)
  152. final_defect_list.append({
  153. "id": defect_id,
  154. "compareKey": compare_key,
  155. "side": side,
  156. "location": location_str,
  157. "type": type_str,
  158. "defectImgUrl": defect_img_url
  159. })
  160. response_data["defectDetailList"] = final_defect_list
  161. report_name = f"{cardNo}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  162. history_result = save_rating_report_history(db_conn, card_id, cardNo, report_name, response_data)
  163. response_data["ratingId"] = history_result["rating_id"]
  164. response_data["reportName"] = history_result["report_name"]
  165. response_data["historySaved"] = history_result["created"]
  166. if history_result["created"]:
  167. logger.info(
  168. f"评级报告生成并保存成功: cardNo={cardNo}, rating_id={history_result['rating_id']}, "
  169. f"defect_count={len(final_defect_list)}"
  170. )
  171. else:
  172. logger.info(
  173. f"评级报告生成成功,但内容与上一条历史一致,未重复保存: cardNo={cardNo}, "
  174. f"rating_id={history_result['rating_id']}, defect_count={len(final_defect_list)}"
  175. )
  176. return response_data
  177. @router.get("/history", status_code=200, summary="根据 cardNo 查询评级报告历史列表")
  178. def get_rating_report_history_list(
  179. cardNo: str,
  180. skip: int = Query(0, ge=0),
  181. page_num: int = Query(None, ge=1),
  182. limit: int = Query(100, ge=1, le=1000),
  183. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  184. ):
  185. if not cardNo or not cardNo.strip():
  186. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  187. if page_num is not None:
  188. skip = (page_num - 1) * limit
  189. try:
  190. with db_conn.cursor(dictionary=True) as cursor:
  191. count_sql = (
  192. f"SELECT COUNT(*) AS total "
  193. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  194. "WHERE cardNo = %s"
  195. )
  196. cursor.execute(count_sql, (cardNo,))
  197. count_row = cursor.fetchone() or {}
  198. total_count = int(count_row.get("total") or 0)
  199. query_sql = (
  200. f"SELECT rating_id, card_id, cardNo, report_name, created_at "
  201. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  202. "WHERE cardNo = %s "
  203. "ORDER BY rating_id DESC "
  204. "LIMIT %s OFFSET %s"
  205. )
  206. cursor.execute(query_sql, (cardNo, limit, skip))
  207. rows = cursor.fetchall()
  208. except Exception as e:
  209. logger.error(f"查询评级报告历史列表失败: cardNo={cardNo}, error={e}")
  210. raise HTTPException(status_code=500, detail="查询评级报告历史列表失败")
  211. history_list = []
  212. for row in rows:
  213. history_list.append({
  214. "ratingId": row.get("rating_id"),
  215. "cardId": row.get("card_id"),
  216. "cardNo": row.get("cardNo"),
  217. "reportName": row.get("report_name"),
  218. "createdAt": format_datetime(row.get("created_at"))
  219. })
  220. return {
  221. "cardNo": cardNo,
  222. "data": {
  223. "total": total_count,
  224. "list": history_list
  225. }
  226. }
  227. @router.get("/history/compare", status_code=200, summary="根据两个 rating_id 对比历史缺陷差异")
  228. def compare_rating_report_history(
  229. rating_id1: int = Query(..., description="第一个历史记录 ID"),
  230. rating_id2: int = Query(..., description="第二个历史记录 ID"),
  231. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  232. ):
  233. try:
  234. with db_conn.cursor(dictionary=True) as cursor:
  235. query_sql = (
  236. f"SELECT rating_id, card_id, cardNo, report_name, report_json, created_at, updated_at "
  237. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  238. "WHERE rating_id IN (%s, %s)"
  239. )
  240. cursor.execute(query_sql, (rating_id1, rating_id2))
  241. rows = cursor.fetchall()
  242. except Exception as e:
  243. logger.error(
  244. f"查询评级报告历史对比数据失败: rating_id1={rating_id1}, rating_id2={rating_id2}, error={e}"
  245. )
  246. raise HTTPException(status_code=500, detail="查询评级报告历史对比数据失败")
  247. row_map = {row.get("rating_id"): row for row in rows}
  248. missing_ids = [rating_id for rating_id in [rating_id1, rating_id2] if rating_id not in row_map]
  249. if missing_ids:
  250. missing_id_text = ",".join(str(item) for item in missing_ids)
  251. raise HTTPException(status_code=404, detail=f"未找到 rating_id={missing_id_text} 的历史记录")
  252. left_row = row_map[rating_id1]
  253. right_row = row_map[rating_id2]
  254. left_report, right_report, common_count = remove_common_defects(
  255. left_row.get("report_json"),
  256. right_row.get("report_json")
  257. )
  258. left_only_count = len(left_report.get("defectDetailList", []))
  259. right_only_count = len(right_report.get("defectDetailList", []))
  260. logger.info(
  261. f"评级报告历史对比完成: rating_id1={rating_id1}, rating_id2={rating_id2}, "
  262. f"same_count={common_count}, left_only_count={left_only_count}, right_only_count={right_only_count}"
  263. )
  264. return {
  265. "comparisonSummary": {
  266. "sameDefectCount": common_count,
  267. "leftOnlyCount": left_only_count,
  268. "rightOnlyCount": right_only_count
  269. },
  270. "left": _build_history_detail_response(left_row, left_report),
  271. "right": _build_history_detail_response(right_row, right_report)
  272. }
  273. @router.get("/history/{rating_id}", status_code=200, summary="根据 rating_id 查询单个评级报告历史")
  274. def get_rating_report_history_detail(
  275. rating_id: int,
  276. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  277. ):
  278. try:
  279. with db_conn.cursor(dictionary=True) as cursor:
  280. query_sql = (
  281. f"SELECT rating_id, card_id, cardNo, report_name, report_json, created_at, updated_at "
  282. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  283. "WHERE rating_id = %s LIMIT 1"
  284. )
  285. cursor.execute(query_sql, (rating_id,))
  286. row = cursor.fetchone()
  287. except Exception as e:
  288. logger.error(f"查询评级报告历史详情失败: rating_id={rating_id}, error={e}")
  289. raise HTTPException(status_code=500, detail="查询评级报告历史详情失败")
  290. if not row:
  291. raise HTTPException(status_code=404, detail=f"未找到 rating_id={rating_id} 的历史记录")
  292. return _build_history_detail_response(row, parse_json_value(row.get("report_json")))