AnlaAnla hace 3 meses
commit
1da40b82c6

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 8 - 0
.idea/CardScoreDataServer.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 12 - 0
.idea/dataSources.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
+    <data-source source="LOCAL" name="@localhost" uuid="c235f8e6-9d69-42fa-95d3-3f4f90d94fe5">
+      <driver-ref>mysql.8</driver-ref>
+      <synchronize>true</synchronize>
+      <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
+      <jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
+      <working-dir>$ProjectFileDir$</working-dir>
+    </data-source>
+  </component>
+</project>

+ 14 - 0
.idea/deployment.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
+    <serverData>
+      <paths name="192.168.31.243">
+        <serverdata>
+          <mappings>
+            <mapping local="$PROJECT_DIR$" web="/" />
+          </mappings>
+        </serverdata>
+      </paths>
+    </serverData>
+  </component>
+</project>

+ 6 - 0
.idea/encodings.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="PROJECT" charset="GBK" />
+  </component>
+</project>

+ 57 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,57 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <Languages>
+        <language minSize="102" name="Python" />
+      </Languages>
+    </inspection_tool>
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="29">
+            <item index="0" class="java.lang.String" itemvalue="webargs" />
+            <item index="1" class="java.lang.String" itemvalue="transformers" />
+            <item index="2" class="java.lang.String" itemvalue="timm" />
+            <item index="3" class="java.lang.String" itemvalue="fluent-logger" />
+            <item index="4" class="java.lang.String" itemvalue="towhee" />
+            <item index="5" class="java.lang.String" itemvalue="flask_restful" />
+            <item index="6" class="java.lang.String" itemvalue="opencv_python" />
+            <item index="7" class="java.lang.String" itemvalue="fastapi" />
+            <item index="8" class="java.lang.String" itemvalue="seaborn" />
+            <item index="9" class="java.lang.String" itemvalue="matplotlib" />
+            <item index="10" class="java.lang.String" itemvalue="minio" />
+            <item index="11" class="java.lang.String" itemvalue="ipython" />
+            <item index="12" class="java.lang.String" itemvalue="torch" />
+            <item index="13" class="java.lang.String" itemvalue="uvicorn" />
+            <item index="14" class="java.lang.String" itemvalue="python-multipart" />
+            <item index="15" class="java.lang.String" itemvalue="torchvision" />
+            <item index="16" class="java.lang.String" itemvalue="pymilvus" />
+            <item index="17" class="java.lang.String" itemvalue="psutil" />
+            <item index="18" class="java.lang.String" itemvalue="ultralytics" />
+            <item index="19" class="java.lang.String" itemvalue="picamera2" />
+            <item index="20" class="java.lang.String" itemvalue="posix_ipc" />
+            <item index="21" class="java.lang.String" itemvalue="websocket-client" />
+            <item index="22" class="java.lang.String" itemvalue="yolov10" />
+            <item index="23" class="java.lang.String" itemvalue="kornia" />
+            <item index="24" class="java.lang.String" itemvalue="prettytable" />
+            <item index="25" class="java.lang.String" itemvalue="huggingface_hub" />
+            <item index="26" class="java.lang.String" itemvalue="PIL" />
+            <item index="27" class="java.lang.String" itemvalue="sklearn" />
+            <item index="28" class="java.lang.String" itemvalue="faster_whisper" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N803" />
+          <option value="N802" />
+          <option value="N806" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="pytorch" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="pytorch" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/CardScoreDataServer.iml" filepath="$PROJECT_DIR$/.idea/CardScoreDataServer.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/sqldialects.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="SqlDialectMappings">
+    <file url="file://$PROJECT_DIR$/app/api/image_data.py" dialect="GenericSQL" />
+  </component>
+</project>

+ 1 - 0
app/__init__.py

@@ -0,0 +1 @@
+

+ 0 - 0
app/api/__init__.py


+ 361 - 0
app/api/image_data.py

@@ -0,0 +1,361 @@
+import os
+import uuid
+import json
+from datetime import date, datetime
+from typing import Optional, Dict, Any, List
+
+from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Query
+from fastapi.responses import JSONResponse, FileResponse
+from pydantic import BaseModel, field_validator
+from mysql.connector.pooling import PooledMySQLConnection
+
+from app.core.config import settings
+from app.core.logger import get_logger
+from app.core.database_loader import get_db_connection
+
+# --- 初始化 ---
+logger = get_logger(__name__)
+router = APIRouter()
+
+# 创建一个依赖项的别名
+db_dependency = Depends(get_db_connection)
+
+
+# --- Pydantic 数据模型 ---
+class ImageRecordResponse(BaseModel):
+    """用于API响应的数据模型,确保数据结构一致"""
+    img_id: int
+    img_name: Optional[str] = None
+    img_path: str
+    img_result_json: Dict[str, Any]
+    created_at: datetime
+
+    @field_validator('img_result_json', mode='before')
+    @classmethod
+    def parse_json_string(cls, v):
+        """
+        这个验证器会在Pydantic进行类型检查之前运行 (因为 pre=True)。
+        它负责将从数据库取出的JSON字符串转换为Python字典。
+        """
+        if isinstance(v, str):
+            try:
+                return json.loads(v)
+            except json.JSONDecodeError:
+                # 如果数据库中的JSON格式错误,则抛出异常
+                raise ValueError("Invalid JSON string in database")
+        return v
+    # --- FIX ENDS HERE ---
+
+
+# --- 辅助函数 ---
+def map_row_to_model(row: tuple, columns: List[str]) -> ImageRecordResponse:
+    """将数据库查询出的一行数据映射到Pydantic模型"""
+    row_dict = dict(zip(columns, row))
+    # 现在当 ImageRecordResponse 被调用时,上面的验证器会先生效
+    return ImageRecordResponse(**row_dict)
+
+
+# --- API 接口实现 ---
+
+#  1: 存储图片和JSON数据
+@router.post("/insert", status_code=201, summary="1. 存储图片和JSON数据")
+async def create_image_data(
+        image: UploadFile = File(..., description="要上传的图片文件"),
+        json_data_str: str = Form(..., description="与图片关联的JSON格式字符串"),
+        img_name: Optional[str] = Form(None, description="图片的可选名称"),
+        db_conn: PooledMySQLConnection = db_dependency
+):
+    """
+    接收图片和JSON数据,存入数据库。
+    - 图片存储在 `Data` 目录。
+    - 记录存入MySQL,`img_id` 自动生成。
+    """
+    try:
+        img_result_json = json.loads(json_data_str)
+    except json.JSONDecodeError:
+        raise HTTPException(status_code=400, detail="`json_data_str` 格式无效。")
+
+    # 生成唯一文件名并保存图片
+    file_extension = os.path.splitext(image.filename)[1]
+    unique_filename = f"{uuid.uuid4()}{file_extension}"
+    image_path = settings.DATA_DIR / unique_filename
+
+    try:
+        with open(image_path, "wb") as buffer:
+            content = await image.read()
+            buffer.write(content)
+        logger.info(f"图片已保存到: {image_path}")
+    except Exception as e:
+        logger.error(f"保存图片失败: {e}")
+        raise HTTPException(status_code=500, detail="无法保存图片文件。")
+
+    # 将记录插入数据库
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        query = (
+            f"INSERT INTO {settings.DB_TABLE_NAME} (img_name, img_path, img_result_json) "
+            "VALUES (%s, %s, %s)"
+        )
+        # 确保存入数据库的是JSON字符串
+        params = (img_name, str(image_path), json.dumps(img_result_json, ensure_ascii=False))
+        cursor.execute(query, params)
+        db_conn.commit()
+        new_id = cursor.lastrowid
+        logger.info(f"新记录已创建, ID: {new_id}")
+        return {"message": "成功存储图片和数据", "img_id": new_id}
+    except Exception as e:
+        db_conn.rollback()
+        logger.error(f"数据库插入失败: {e}")
+        if os.path.exists(image_path):
+            os.remove(image_path)
+        raise HTTPException(status_code=500, detail="数据库插入失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+# 2: 获取数据列表 (带筛选)
+@router.get("/data_list", response_model=List[ImageRecordResponse], summary="2. 获取数据列表 (可筛选)")
+def list_image_records(
+        start_id: Optional[int] = Query(None, description="筛选条件:起始img_id"),
+        end_id: Optional[int] = Query(None, description="筛选条件:结束img_id"),
+        name_like: Optional[str] = Query(None, description="筛选条件:名称模糊搜索"),
+        start_date: Optional[date] = Query(None, description="筛选条件:起始日期 (YYYY-MM-DD)"),
+        end_date: Optional[date] = Query(None, description="筛选条件:结束日期 (YYYY-MM-DD)"),
+        skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
+        limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
+        db_conn: PooledMySQLConnection = db_dependency
+):
+    """
+    根据多个可选条件查询记录列表,并支持分页。
+    """
+    query = f"SELECT * FROM {settings.DB_TABLE_NAME}"
+    conditions = []
+    params = []
+
+    if start_id is not None:
+        conditions.append("img_id >= %s")
+        params.append(start_id)
+    if end_id is not None:
+        conditions.append("img_id <= %s")
+        params.append(end_id)
+    if name_like:
+        conditions.append("img_name LIKE %s")
+        params.append(f"%{name_like}%")
+    if start_date:
+        conditions.append("created_at >= %s")
+        params.append(start_date)
+    if end_date:
+        conditions.append("created_at < DATE_ADD(%s, INTERVAL 1 DAY)")
+        params.append(end_date)
+
+    if conditions:
+        query += " WHERE " + " AND ".join(conditions)
+
+    query += " ORDER BY img_id DESC LIMIT %s OFFSET %s"
+    params.extend([limit, skip])
+
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        cursor.execute(query, tuple(params))
+        columns = [desc[0] for desc in cursor.description]
+        return [map_row_to_model(row, columns) for row in cursor.fetchall()]
+    except Exception as e:
+        logger.error(f"查询列表失败: {e}")
+        raise HTTPException(status_code=500, detail="获取数据列表失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+#  2: 根据img_id查询
+@router.get("/query/{img_id}", response_model=ImageRecordResponse, summary="2. 根据img_id查询完整记录")
+def get_record_by_id(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
+    """获取指定ID的完整数据库记录。"""
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query, (img_id,))
+        result = cursor.fetchone()
+        if not result:
+            raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
+
+        columns = [desc[0] for desc in cursor.description]
+        return map_row_to_model(result, columns)
+    except Exception as e:
+        logger.error(f"ID查询失败 ({img_id}): {e}")
+        if isinstance(e, HTTPException): raise e
+        raise HTTPException(status_code=500, detail="数据库查询失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+#  3: 根据img_name查询
+@router.get("/query/name/{img_name}", response_model=List[ImageRecordResponse], summary="3. 根据img_name查询记录")
+def get_records_by_name(img_name: str, db_conn: PooledMySQLConnection = db_dependency):
+    """获取所有与指定名称匹配的记录列表。"""
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_name = %s"
+        cursor.execute(query, (img_name,))
+        results = cursor.fetchall()
+        if not results:
+            return []  # 未找到则返回空列表
+
+        columns = [desc[0] for desc in cursor.description]
+        return [map_row_to_model(row, columns) for row in results]
+    except Exception as e:
+        logger.error(f"名称查询失败 ({img_name}): {e}")
+        raise HTTPException(status_code=500, detail="数据库查询失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+# ... (其他接口代码保持不变) ...
+
+#  5: 修改JSON数据
+@router.put("/update/json/{img_id}", status_code=200, summary="5. 修改指定ID记录的JSON数据")
+def update_record_json(
+        img_id: int,
+        new_json_data: Dict[str, Any],
+        db_conn: PooledMySQLConnection = db_dependency
+):
+    """根据img_id,用请求体中的新JSON覆盖原有的JSON数据。"""
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        new_json_str = json.dumps(new_json_data, ensure_ascii=False)
+        query = f"UPDATE {settings.DB_TABLE_NAME} SET img_result_json = %s WHERE img_id = %s"
+        cursor.execute(query, (new_json_str, img_id))
+
+        if cursor.rowcount == 0:
+            raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
+
+        db_conn.commit()
+        logger.info(f"ID {img_id} 的JSON数据已更新。")
+        return {"message": f"成功更新 ID {img_id} 的JSON数据"}
+    except Exception as e:
+        db_conn.rollback()
+        logger.error(f"更新JSON失败 ({img_id}): {e}")
+        if isinstance(e, HTTPException): raise e
+        raise HTTPException(status_code=500, detail="更新JSON数据失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+#  6: 获取图片文件
+@router.get("/image/{img_id}", summary="6. 获取指定ID的图片文件")
+def get_image_file(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
+    """根据img_id查找记录,并返回对应的图片文件。"""
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        query = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query, (img_id,))
+        result = cursor.fetchone()
+
+        if not result:
+            raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
+
+        image_path = result[0]
+        if not os.path.exists(image_path):
+            logger.error(f"文件在服务器上未找到: {image_path} (数据库ID: {img_id})")
+            raise HTTPException(status_code=404, detail="图片文件在服务器上不存在。")
+
+        return FileResponse(image_path)
+    except Exception as e:
+        logger.error(f"获取图片失败 ({img_id}): {e}")
+        if isinstance(e, HTTPException): raise e
+        raise HTTPException(status_code=500, detail="获取图片文件失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+#  7: 获取JSON数据
+@router.get("/json/{img_id}", summary="7. 获取指定ID的JSON数据")
+def get_record_json(img_id: int, db_conn: PooledMySQLConnection = db_dependency):
+    """根据img_id查找记录,并仅返回其JSON数据部分。"""
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+        query = f"SELECT img_result_json FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query, (img_id,))
+        result = cursor.fetchone()
+        if not result:
+            raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
+
+        json_data = json.loads(result[0])
+        return JSONResponse(content=json_data)
+    except Exception as e:
+        logger.error(f"获取JSON失败 ({img_id}): {e}")
+        if isinstance(e, HTTPException): raise e
+        raise HTTPException(status_code=500, detail="获取JSON数据失败。")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+#  8: 根据 img_id 删除记录
+@router.delete("/delete/{img_id}", status_code=200, summary="8. 根据img_id删除记录及其图片")
+def delete_record_by_id(
+        img_id: int,
+        db_conn: PooledMySQLConnection = db_dependency
+):
+    """
+    根据img_id删除数据库记录以及存储在服务器上的对应图片文件。
+    这是一个原子操作,如果文件删除失败,数据库更改将回滚。
+    """
+    cursor = None
+    try:
+        cursor = db_conn.cursor()
+
+        # 1. 先查询记录,获取文件路径,并确认记录存在
+        query_path = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query_path, (img_id,))
+        result = cursor.fetchone()
+
+        if not result:
+            raise HTTPException(status_code=404, detail=f"ID为 {img_id} 的记录未找到。")
+
+        image_path = result[0]
+
+        # 2. 删除数据库记录
+        query_delete = f"DELETE FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query_delete, (img_id,))
+
+        # 3. 删除对应的图片文件(如果存在)
+        if os.path.exists(image_path):
+            try:
+                os.remove(image_path)
+                logger.info(f"图片文件已删除: {image_path}")
+            except OSError as e:
+                # 如果文件删除失败,回滚数据库操作以保持一致性
+                db_conn.rollback()
+                logger.error(f"删除文件失败: {image_path}. 数据库操作已回滚。错误: {e}")
+                raise HTTPException(status_code=500, detail="删除文件失败,数据库操作已回滚。")
+        else:
+            logger.warning(f"数据库记录指向的文件不存在,无需删除: {image_path} (ID: {img_id})")
+
+        # 4. 提交事务
+        db_conn.commit()
+        logger.info(f"ID {img_id} 的记录和关联文件已成功删除。")
+
+        return {"message": f"成功删除 ID {img_id} 的记录及其关联文件"}
+
+    except Exception as e:
+        db_conn.rollback()
+        logger.error(f"删除记录失败 ({img_id}): {e}")
+        if isinstance(e, HTTPException):
+            raise e
+        raise HTTPException(status_code=500, detail="删除记录时发生服务器内部错误。")
+    finally:
+        if cursor:
+            cursor.close()

+ 0 - 0
app/core/__init__.py


+ 33 - 0
app/core/config.py

@@ -0,0 +1,33 @@
+from pathlib import Path
+from typing import Dict, List
+from enum import Enum
+
+
+class Settings:
+    API_ImageData_prefix: str = "/api/image_data"
+    BASE_PATH = Path(__file__).parent.parent.parent.absolute()
+
+    DATA_DIR = BASE_PATH / "Data"
+    SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
+
+    # --- 数据库配置 ---
+    DB_NAME = 'card_score_database'
+    DB_TABLE_NAME = 'image_records'
+    DATABASE_CONFIG: Dict[str, str] = {
+        'user': 'root',
+        'password': '123456',
+        'host': '127.0.0.1',
+    }
+    # 连接到指定数据库的配置
+    DATABASE_CONFIG_WITH_DB: Dict[str, str] = {
+        **DATABASE_CONFIG,
+        'database': DB_NAME
+    }
+
+
+settings = Settings()
+print(f"项目根目录: {settings.BASE_PATH}")
+print(f"数据存储目录: {settings.DATA_DIR}")
+
+# DefectType = Enum("InferenceType", {name: name for name in settings.DEFECT_TYPE})
+# print()

+ 100 - 0
app/core/database_loader.py

@@ -0,0 +1,100 @@
+import mysql.connector
+from mysql.connector import errorcode
+from .config import settings
+from app.core.logger import get_logger
+
+logger = get_logger(__name__)
+
+# 全局的连接池
+db_connection_pool = None
+
+
+def init_database():
+    """
+    初始化数据库:如果数据库或表不存在,则创建它们。
+    """
+    logger.info("--- 开始初始化数据库 ---")
+
+    # 1. 尝试连接MySQL服务器(不指定数据库)
+    try:
+        cnx = mysql.connector.connect(**settings.DATABASE_CONFIG)
+        cursor = cnx.cursor()
+
+        # 2. 创建数据库(如果不存在)
+        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {settings.DB_NAME} DEFAULT CHARACTER SET 'utf8mb4'")
+        logger.info(f"数据库 '{settings.DB_NAME}' 已准备就绪。")
+
+        # 3. 切换到目标数据库
+        cnx.database = settings.DB_NAME
+
+        # 4. 创建表(如果不存在)
+        table_description = (
+            f"CREATE TABLE IF NOT EXISTS `{settings.DB_TABLE_NAME}` ("
+            "  `img_id` INT AUTO_INCREMENT PRIMARY KEY,"
+            "  `img_name` VARCHAR(255) NULL,"
+            "  `img_path` VARCHAR(512) NOT NULL,"
+            "  `img_result_json` JSON NOT NULL,"
+            "  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
+            ") ENGINE=InnoDB"
+        )
+        cursor.execute(table_description)
+        logger.info(f"数据表 '{settings.DB_TABLE_NAME}' 已准备就绪。")
+
+    except mysql.connector.Error as err:
+        logger.error(f"数据库初始化失败: {err}")
+        exit(1)  # 初始化失败直接退出程序
+    finally:
+        if 'cursor' in locals() and cursor:
+            cursor.close()
+        if 'cnx' in locals() and cnx.is_connected():
+            cnx.close()
+
+    logger.info("--- 数据库初始化完成 ---")
+
+
+def load_database_pool():
+    """
+    在应用启动时创建数据库连接池。
+    """
+    global db_connection_pool
+    if db_connection_pool is None:
+        logger.info("--- 创建数据库连接池 ---")
+        try:
+            db_connection_pool = mysql.connector.pooling.MySQLConnectionPool(
+                pool_name="mypool",
+                pool_size=5,  # 池中保持的连接数
+                **settings.DATABASE_CONFIG_WITH_DB
+            )
+            logger.info("--- 数据库连接池创建成功 ---")
+        except mysql.connector.Error as err:
+            logger.error(f"创建数据库连接池失败: {err}")
+            exit(1)
+
+
+def close_database_pool():
+    """
+    在应用关闭时,不需要手动关闭连接池,连接器会自动处理。
+    这个函数留作备用。
+    """
+    logger.info("--- 数据库连接池将自动关闭 ---")
+
+
+# --- FastAPI 依赖注入 ---
+def get_db_connection():
+    """
+    一个FastAPI依赖项,用于从池中获取数据库连接。
+    它确保连接在使用后返回到池中。
+    """
+    if db_connection_pool is None:
+        raise RuntimeError("数据库连接池未初始化")
+
+    db_conn = None
+    try:
+        db_conn = db_connection_pool.get_connection()
+        yield db_conn
+    except mysql.connector.Error as err:
+        logger.error(f"获取数据库连接失败: {err}")
+        # 这里可以根据需要抛出HTTPException
+    finally:
+        if db_conn and db_conn.is_connected():
+            db_conn.close()

+ 47 - 0
app/core/logger.py

@@ -0,0 +1,47 @@
+import logging
+import sys
+
+# 定义一个全局的日志格式
+LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+
+
+def setup_logging():
+    """
+    配置日志系统,使其同时输出到文件和控制台。
+    这个函数应该在应用程序启动时只调用一次。
+    """
+    # 获取根日志记录器
+    root_logger = logging.getLogger()
+    root_logger.setLevel(logging.INFO)  # 设置根日志记录器的级别
+
+    # 如果已经有处理器,先清空,防止重复添加
+    if root_logger.hasHandlers():
+        root_logger.handlers.clear()
+
+    # 创建一个通用的格式化器
+    formatter = logging.Formatter(LOG_FORMAT)
+
+    # 1. 创建控制台处理器 (StreamHandler)
+    #    - 将日志输出到标准输出(您的终端)
+    console_handler = logging.StreamHandler(sys.stdout)
+    console_handler.setFormatter(formatter)
+    root_logger.addHandler(console_handler)
+
+    # 2. 创建文件处理器 (FileHandler)
+    #    - 将日志写入文件 app.log
+    #    - mode='w' 表示每次启动都覆盖旧日志
+    #    - encoding='utf-8' 确保正确处理中文字符
+    file_handler = logging.FileHandler('app.log', mode='w', encoding='utf-8')
+    file_handler.setFormatter(formatter)
+    root_logger.addHandler(file_handler)
+
+    # 配置完成后,可以记录一条消息来确认
+    logging.info("日志系统已成功配置,将同时输出到控制台和 app.log 文件。")
+
+
+def get_logger(name: str) -> logging.Logger:
+    """
+    获取一个指定名称的日志记录器实例。
+    假设 setup_logging() 已经在此之前被调用。
+    """
+    return logging.getLogger(name)

+ 33 - 0
app/main.py

@@ -0,0 +1,33 @@
+from fastapi import FastAPI
+from contextlib import asynccontextmanager
+
+from .core.database_loader import init_database, load_database_pool, close_database_pool
+from app.api.image_data import router as image_data_router
+import os
+
+from .core.config import settings
+from .core.logger import setup_logging, get_logger
+
+setup_logging()
+# 获取一个用于 main 模块的日志记录器
+logger = get_logger(__name__)
+
+@asynccontextmanager
+async def lifespan(main_app: FastAPI):
+    print("--- 应用启动 ---")
+    # --- 文件和目录准备 ---
+    os.makedirs(settings.DATA_DIR, exist_ok=True)
+
+    # --- 数据库初始化 ---
+    init_database()
+    load_database_pool()
+
+    yield
+
+    print("--- 应用关闭 ---")
+    close_database_pool()
+
+
+app = FastAPI(title="卡片分数数据存储服务", lifespan=lifespan)
+
+app.include_router(image_data_router, prefix=settings.API_ImageData_prefix, tags=["Image Data"])

+ 5 - 0
run.py

@@ -0,0 +1,5 @@
+import uvicorn
+
+if __name__ == "__main__":
+    print("http://127.0.0.1:7745/docs")
+    uvicorn.run("app.main:app", host="0.0.0.0", port=7745, reload=True)