image_data.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import os
  2. import uuid
  3. import json
  4. from datetime import date, datetime
  5. from typing import Optional, Dict, Any, List
  6. from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Query
  7. from fastapi.responses import JSONResponse, FileResponse
  8. from mysql.connector.pooling import PooledMySQLConnection
  9. from app.core.config import settings
  10. from app.core.logger import get_logger
  11. from app.utils.scheme import ImageRecordResponse, map_row_to_model
  12. from app.core.database_loader import get_db_connection
  13. # --- 初始化 ---
  14. logger = get_logger(__name__)
  15. router = APIRouter()
  16. # 创建一个依赖项的别名
  17. db_dependency = Depends(get_db_connection)
  18. # --- API 接口实现 ---
  19. # 1: 存储图片和JSON数据
  20. @router.post("/insert", status_code=201, summary="1. 存储图片和JSON数据")
  21. async def create_image_data(
  22. image: UploadFile = File(..., description="要上传的图片文件"),
  23. json_data_str: str = Form(..., description="与图片关联的JSON格式字符串"),
  24. img_name: Optional[str] = Form(None, description="图片的可选名称"),
  25. db_conn: PooledMySQLConnection = db_dependency
  26. ):
  27. """
  28. 接收图片和JSON数据,存入数据库。
  29. - 图片存储在 `Data` 目录。
  30. - 记录存入MySQL,`img_id` 自动生成。
  31. """
  32. try:
  33. img_result_json = json.loads(json_data_str)
  34. except json.JSONDecodeError:
  35. raise HTTPException(status_code=400, detail="`json_data_str` 格式无效。")
  36. # 生成唯一文件名并保存图片
  37. file_extension = os.path.splitext(image.filename)[1]
  38. unique_filename = f"{uuid.uuid4()}{file_extension}"
  39. image_path = settings.DATA_DIR / unique_filename
  40. try:
  41. with open(image_path, "wb") as buffer:
  42. content = await image.read()
  43. buffer.write(content)
  44. logger.info(f"图片已保存到: {image_path}")
  45. except Exception as e:
  46. logger.error(f"保存图片失败: {e}")
  47. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  48. # 将记录插入数据库
  49. cursor = None
  50. try:
  51. cursor = db_conn.cursor()
  52. query = (
  53. f"INSERT INTO {settings.DB_TABLE_NAME} (img_name, img_path, img_result_json) "
  54. "VALUES (%s, %s, %s)"
  55. )
  56. # 确保存入数据库的是JSON字符串
  57. params = (img_name, str(image_path), json.dumps(img_result_json, ensure_ascii=False))
  58. cursor.execute(query, params)
  59. db_conn.commit()
  60. new_id = cursor.lastrowid
  61. logger.info(f"新记录已创建, ID: {new_id}")
  62. return {"message": "成功存储图片和数据", "img_id": new_id}
  63. except Exception as e:
  64. db_conn.rollback()
  65. logger.error(f"数据库插入失败: {e}")
  66. if os.path.exists(image_path):
  67. os.remove(image_path)
  68. raise HTTPException(status_code=500, detail="数据库插入失败。")
  69. finally:
  70. if cursor:
  71. cursor.close()
  72. # 2: 获取数据列表 (带筛选)
  73. @router.get("/data_list", response_model=List[ImageRecordResponse], summary="2. 获取数据列表 (可筛选)")
  74. def list_image_records(
  75. start_id: Optional[int] = Query(None, description="筛选条件:起始img_id"),
  76. end_id: Optional[int] = Query(None, description="筛选条件:结束img_id"),
  77. name_like: Optional[str] = Query(None, description="筛选条件:名称模糊搜索"),
  78. start_date: Optional[date] = Query(None, description="筛选条件:起始日期 (YYYY-MM-DD)"),
  79. end_date: Optional[date] = Query(None, description="筛选条件:结束日期 (YYYY-MM-DD)"),
  80. skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
  81. limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
  82. db_conn: PooledMySQLConnection = db_dependency
  83. ):
  84. """
  85. 根据多个可选条件查询记录列表,并支持分页。
  86. """
  87. query = f"SELECT * FROM {settings.DB_TABLE_NAME}"
  88. conditions = []
  89. params = []
  90. if start_id is not None:
  91. conditions.append("img_id >= %s")
  92. params.append(start_id)
  93. if end_id is not None:
  94. conditions.append("img_id <= %s")
  95. params.append(end_id)
  96. if name_like:
  97. conditions.append("img_name LIKE %s")
  98. params.append(f"%{name_like}%")
  99. if start_date:
  100. conditions.append("created_at >= %s")
  101. params.append(start_date)
  102. if end_date:
  103. conditions.append("created_at < DATE_ADD(%s, INTERVAL 1 DAY)")
  104. params.append(end_date)
  105. if conditions:
  106. query += " WHERE " + " AND ".join(conditions)
  107. query += " ORDER BY img_id DESC LIMIT %s OFFSET %s"
  108. params.extend([limit, skip])
  109. cursor = None
  110. try:
  111. cursor = db_conn.cursor()
  112. cursor.execute(query, tuple(params))
  113. columns = [desc[0] for desc in cursor.description]
  114. return [map_row_to_model(row, columns) for row in cursor.fetchall()]
  115. except Exception as e:
  116. logger.error(f"查询列表失败: {e}")
  117. raise HTTPException(status_code=500, detail="获取数据列表失败。")
  118. finally:
  119. if cursor:
  120. cursor.close()
  121. # 2: 根据img_id查询
  122. @router.get("/query/{img_id}", response_model=ImageRecordResponse, summary="2. 根据img_id查询完整记录")
  123. def get_record_by_id(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
  124. """获取指定ID的完整数据库记录。"""
  125. cursor = None
  126. try:
  127. cursor = db_conn.cursor()
  128. query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
  129. cursor.execute(query, (img_id,))
  130. result = cursor.fetchone()
  131. if not result:
  132. raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
  133. columns = [desc[0] for desc in cursor.description]
  134. return map_row_to_model(result, columns)
  135. except Exception as e:
  136. logger.error(f"ID查询失败 ({img_id}): {e}")
  137. if isinstance(e, HTTPException): raise e
  138. raise HTTPException(status_code=500, detail="数据库查询失败。")
  139. finally:
  140. if cursor:
  141. cursor.close()
  142. # 3: 根据img_name查询
  143. @router.get("/query/name/{img_name}", response_model=List[ImageRecordResponse], summary="3. 根据img_name查询记录")
  144. def get_records_by_name(img_name: str, db_conn: PooledMySQLConnection = db_dependency):
  145. """获取所有与指定名称匹配的记录列表。"""
  146. cursor = None
  147. try:
  148. cursor = db_conn.cursor()
  149. query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_name = %s"
  150. cursor.execute(query, (img_name,))
  151. results = cursor.fetchall()
  152. if not results:
  153. return [] # 未找到则返回空列表
  154. columns = [desc[0] for desc in cursor.description]
  155. return [map_row_to_model(row, columns) for row in results]
  156. except Exception as e:
  157. logger.error(f"名称查询失败 ({img_name}): {e}")
  158. raise HTTPException(status_code=500, detail="数据库查询失败。")
  159. finally:
  160. if cursor:
  161. cursor.close()
  162. # 5: 修改JSON数据
  163. @router.put("/update/json/{img_id}", status_code=200, summary="5. 修改指定ID记录的JSON数据")
  164. def update_record_json(
  165. img_id: int,
  166. new_json_data: Dict[str, Any],
  167. db_conn: PooledMySQLConnection = db_dependency
  168. ):
  169. """根据img_id,用请求体中的新JSON覆盖原有的JSON数据。"""
  170. cursor = None
  171. try:
  172. cursor = db_conn.cursor()
  173. new_json_str = json.dumps(new_json_data, ensure_ascii=False)
  174. query = f"UPDATE {settings.DB_TABLE_NAME} SET img_result_json_new = %s WHERE img_id = %s"
  175. cursor.execute(query, (new_json_str, img_id))
  176. if cursor.rowcount == 0:
  177. raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
  178. db_conn.commit()
  179. logger.info(f"ID {img_id} 的JSON数据已更新。")
  180. return {"message": f"成功更新 ID {img_id} 的JSON数据"}
  181. except Exception as e:
  182. db_conn.rollback()
  183. logger.error(f"更新JSON失败 ({img_id}): {e}")
  184. if isinstance(e, HTTPException): raise e
  185. raise HTTPException(status_code=500, detail="更新JSON数据失败。")
  186. finally:
  187. if cursor:
  188. cursor.close()
  189. # 6: 获取图片文件
  190. @router.get("/image/{img_id}", summary="6. 获取指定ID的图片文件")
  191. def get_image_file(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
  192. """根据img_id查找记录,并返回对应的图片文件。"""
  193. cursor = None
  194. try:
  195. cursor = db_conn.cursor()
  196. query = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
  197. cursor.execute(query, (img_id,))
  198. result = cursor.fetchone()
  199. if not result:
  200. raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
  201. image_path = result[0]
  202. if not os.path.exists(image_path):
  203. logger.error(f"文件在服务器上未找到: {image_path} (数据库ID: {img_id})")
  204. raise HTTPException(status_code=404, detail="图片文件在服务器上不存在。")
  205. return FileResponse(image_path)
  206. except Exception as e:
  207. logger.error(f"获取图片失败 ({img_id}): {e}")
  208. if isinstance(e, HTTPException): raise e
  209. raise HTTPException(status_code=500, detail="获取图片文件失败。")
  210. finally:
  211. if cursor:
  212. cursor.close()
  213. # 7: 获取JSON数据
  214. @router.get("/json/{img_id}", summary="7. 获取指定ID的JSON数据")
  215. def get_record_json(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
  216. """
  217. 根据img_id查找记录,并仅返回其JSON数据部分。
  218. 优先返回 img_result_json_new,如果为 NULL 则返回 img_result_json。
  219. """
  220. cursor = None
  221. try:
  222. cursor = db_conn.cursor()
  223. query = (
  224. f"SELECT IFNULL({settings.DB_TABLE_NAME}.img_result_json_new, {settings.DB_TABLE_NAME}.img_result_json) AS img_result_json "
  225. f"FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
  226. )
  227. cursor.execute(query, (img_id,))
  228. result = cursor.fetchone() # result will be a tuple like ('{"key": "value"}',)
  229. if not result:
  230. raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
  231. # result[0] 已经是经过 IFNULL 处理后的目标 JSON 字符串
  232. json_to_return = json.loads(result[0])
  233. return JSONResponse(content=json_to_return)
  234. except Exception as e:
  235. logger.error(f"获取JSON失败 ({img_id}): {e}")
  236. if isinstance(e, HTTPException): raise e
  237. raise HTTPException(status_code=500, detail="获取JSON数据失败。")
  238. finally:
  239. if cursor:
  240. cursor.close()
  241. # 8: 根据 img_id 删除记录
  242. @router.delete("/delete/{img_id}", status_code=200, summary="8. 根据img_id删除记录及其图片")
  243. def delete_record_by_id(
  244. img_id: int,
  245. db_conn: PooledMySQLConnection = db_dependency
  246. ):
  247. """
  248. 根据img_id删除数据库记录以及存储在服务器上的对应图片文件。
  249. 这是一个原子操作,如果文件删除失败,数据库更改将回滚。
  250. """
  251. cursor = None
  252. try:
  253. cursor = db_conn.cursor()
  254. # 1. 先查询记录,获取文件路径,并确认记录存在
  255. query_path = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
  256. cursor.execute(query_path, (img_id,))
  257. result = cursor.fetchone()
  258. if not result:
  259. raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
  260. image_path = result[0]
  261. # 2. 删除数据库记录
  262. query_delete = f"DELETE FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
  263. cursor.execute(query_delete, (img_id,))
  264. # 3. 删除对应的图片文件(如果存在)
  265. if os.path.exists(image_path):
  266. try:
  267. os.remove(image_path)
  268. logger.info(f"图片文件已删除: {image_path}")
  269. except OSError as e:
  270. # 如果文件删除失败,回滚数据库操作以保持一致性
  271. db_conn.rollback()
  272. logger.error(f"删除文件失败: {image_path}. 数据库操作已回滚。错误: {e}")
  273. raise HTTPException(status_code=500, detail="删除文件失败,数据库操作已回滚。")
  274. else:
  275. logger.warning(f"数据库记录指向的文件不存在,无需删除: {image_path} (ID: {img_id})")
  276. # 4. 提交事务
  277. db_conn.commit()
  278. logger.info(f"ID {img_id} 的记录和关联文件已成功删除。")
  279. return {"message": f"成功删除 ID {img_id} 的记录及其关联文件"}
  280. except Exception as e:
  281. db_conn.rollback()
  282. logger.error(f"删除记录失败 ({img_id}): {e}")
  283. if isinstance(e, HTTPException):
  284. raise e
  285. raise HTTPException(status_code=500, detail="删除记录时发生服务器内部错误。")
  286. finally:
  287. if cursor:
  288. cursor.close()