rating_report.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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(
  141. original_img_path, min_rect, filename, points=defect.get("points"),
  142. )
  143. center_x, center_y = min_rect[0]
  144. location_str = f"{int(center_x)},{int(center_y)}"
  145. raw_type = f"{defect.get('defect_type', '').upper()}".strip()
  146. type_str_map = {
  147. "CORNER": "CORNER",
  148. "EDGE": "SIDE",
  149. "FACE": "SURFACE"
  150. }
  151. type_str = type_str_map.get(raw_type)
  152. # compareKey 使用面积、标签、正反面和 location。
  153. compare_key = build_defect_compare_key(defect, side, location_str)
  154. final_defect_list.append({
  155. "id": defect_id,
  156. "compareKey": compare_key,
  157. "side": side,
  158. "location": location_str,
  159. "type": type_str,
  160. "defectImgUrl": defect_img_url
  161. })
  162. response_data["defectDetailList"] = final_defect_list
  163. report_name = f"{cardNo}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  164. history_result = save_rating_report_history(db_conn, card_id, cardNo, report_name, response_data)
  165. response_data["ratingId"] = history_result["rating_id"]
  166. response_data["reportName"] = history_result["report_name"]
  167. response_data["historySaved"] = history_result["created"]
  168. if history_result["created"]:
  169. logger.info(
  170. f"评级报告生成并保存成功: cardNo={cardNo}, rating_id={history_result['rating_id']}, "
  171. f"defect_count={len(final_defect_list)}"
  172. )
  173. else:
  174. logger.info(
  175. f"评级报告生成成功,但内容与上一条历史一致,未重复保存: cardNo={cardNo}, "
  176. f"rating_id={history_result['rating_id']}, defect_count={len(final_defect_list)}"
  177. )
  178. return response_data
  179. @router.get("/history", status_code=200, summary="根据 cardNo 查询评级报告历史列表 [用户调用]")
  180. def get_rating_report_history_list(
  181. cardNo: str,
  182. skip: int = Query(0, ge=0),
  183. page_num: int = Query(None, ge=1),
  184. limit: int = Query(100, ge=1, le=1000),
  185. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  186. ):
  187. if not cardNo or not cardNo.strip():
  188. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  189. if page_num is not None:
  190. skip = (page_num - 1) * limit
  191. try:
  192. with db_conn.cursor(dictionary=True) as cursor:
  193. count_sql = (
  194. f"SELECT COUNT(*) AS total "
  195. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  196. "WHERE cardNo = %s"
  197. )
  198. cursor.execute(count_sql, (cardNo,))
  199. count_row = cursor.fetchone() or {}
  200. total_count = int(count_row.get("total") or 0)
  201. query_sql = (
  202. f"SELECT rating_id, card_id, cardNo, report_name, created_at "
  203. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  204. "WHERE cardNo = %s "
  205. "ORDER BY rating_id DESC "
  206. "LIMIT %s OFFSET %s"
  207. )
  208. cursor.execute(query_sql, (cardNo, limit, skip))
  209. rows = cursor.fetchall()
  210. except Exception as e:
  211. logger.error(f"查询评级报告历史列表失败: cardNo={cardNo}, error={e}")
  212. raise HTTPException(status_code=500, detail="查询评级报告历史列表失败")
  213. history_list = []
  214. for row in rows:
  215. history_list.append({
  216. "ratingId": row.get("rating_id"),
  217. "cardId": row.get("card_id"),
  218. "cardNo": row.get("cardNo"),
  219. "reportName": row.get("report_name"),
  220. "createdAt": format_datetime(row.get("created_at"))
  221. })
  222. return {
  223. "cardNo": cardNo,
  224. "data": {
  225. "total": total_count,
  226. "list": history_list
  227. }
  228. }
  229. @router.get("/history/compare", status_code=200, summary="根据两个 rating_id 对比历史缺陷差异 [用户调用]")
  230. def compare_rating_report_history(
  231. rating_id1: int = Query(..., description="第一个历史记录 ID"),
  232. rating_id2: int = Query(..., description="第二个历史记录 ID"),
  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 IN (%s, %s)"
  241. )
  242. cursor.execute(query_sql, (rating_id1, rating_id2))
  243. rows = cursor.fetchall()
  244. except Exception as e:
  245. logger.error(
  246. f"查询评级报告历史对比数据失败: rating_id1={rating_id1}, rating_id2={rating_id2}, error={e}"
  247. )
  248. raise HTTPException(status_code=500, detail="查询评级报告历史对比数据失败")
  249. row_map = {row.get("rating_id"): row for row in rows}
  250. missing_ids = [rating_id for rating_id in [rating_id1, rating_id2] if rating_id not in row_map]
  251. if missing_ids:
  252. missing_id_text = ",".join(str(item) for item in missing_ids)
  253. raise HTTPException(status_code=404, detail=f"未找到 rating_id={missing_id_text} 的历史记录")
  254. left_row = row_map[rating_id1]
  255. right_row = row_map[rating_id2]
  256. left_report, right_report, common_count = remove_common_defects(
  257. left_row.get("report_json"),
  258. right_row.get("report_json")
  259. )
  260. left_only_count = len(left_report.get("defectDetailList", []))
  261. right_only_count = len(right_report.get("defectDetailList", []))
  262. logger.info(
  263. f"评级报告历史对比完成: rating_id1={rating_id1}, rating_id2={rating_id2}, "
  264. f"same_count={common_count}, left_only_count={left_only_count}, right_only_count={right_only_count}"
  265. )
  266. return {
  267. "comparisonSummary": {
  268. "sameDefectCount": common_count,
  269. "leftOnlyCount": left_only_count,
  270. "rightOnlyCount": right_only_count
  271. },
  272. "left": _build_history_detail_response(left_row, left_report),
  273. "right": _build_history_detail_response(right_row, right_report)
  274. }
  275. @router.get("/history/{rating_id}", status_code=200, summary="根据 rating_id 查询单个评级报告历史 [用户调用]")
  276. def get_rating_report_history_detail(
  277. rating_id: int,
  278. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  279. ):
  280. try:
  281. with db_conn.cursor(dictionary=True) as cursor:
  282. query_sql = (
  283. f"SELECT rating_id, card_id, cardNo, report_name, report_json, created_at, updated_at "
  284. f"FROM {settings.RATING_REPORT_HISTORY_TABLE_NAME} "
  285. "WHERE rating_id = %s LIMIT 1"
  286. )
  287. cursor.execute(query_sql, (rating_id,))
  288. row = cursor.fetchone()
  289. except Exception as e:
  290. logger.error(f"查询评级报告历史详情失败: rating_id={rating_id}, error={e}")
  291. raise HTTPException(status_code=500, detail="查询评级报告历史详情失败")
  292. if not row:
  293. raise HTTPException(status_code=404, detail=f"未找到 rating_id={rating_id} 的历史记录")
  294. return _build_history_detail_response(row, parse_json_value(row.get("report_json")))