chenkun 1 ماه پیش
والد
کامیت
663f8778de
6فایلهای تغییر یافته به همراه18070 افزوده شده و 46 حذف شده
  1. 17540 0
      logs/lq-admin-app.log.1
  2. 2 2
      src/app/base/mineru_connection.py
  3. 2 0
      src/app/server/app.py
  4. 259 0
      src/app/services/image_service.py
  5. 73 44
      src/app/services/sample_service.py
  6. 194 0
      src/views/image_view.py

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 17540 - 0
logs/lq-admin-app.log.1


+ 2 - 2
src/app/base/mineru_connection.py

@@ -142,7 +142,7 @@ class MinerUManager:
             
             file_name = f"{chinese_name}{file_ext}"
 
-            # 3. 预检查文件类型
+            # 检查文件扩展名,对于 .txt 直接处理为 Markdown
             if file_ext == ".txt":
                 logger.info(f"[{doc_id}] 检测为 .txt 文件,跳过 MinerU 转换,直接处理为 Markdown")
                 # 直接将 txt 内容作为 md 内容上传
@@ -150,7 +150,7 @@ class MinerUManager:
                 md_object_name = f"{self.minio_manager.base_path}/converted/{datetime.now().strftime('%Y%m%d')}/{doc_id}.md"
                 md_cloud_url = self.minio_manager.upload_file(md_content, md_object_name, content_type="text/markdown")
                 
-                # 更新数据库状态为成功
+                # 更新数据库状态为成功,但不写入 binary content
                 self.update_db_status(doc_id, status=2, md_url=md_cloud_url)
                 logger.info(f"[{doc_id}] .txt 文件处理成功. MD: {md_cloud_url}")
                 return

+ 2 - 0
src/app/server/app.py

@@ -50,6 +50,7 @@ from views.knowledge_base_view import router as knowledge_base_router
 from views.snippet_view import router as snippet_router
 from views.tag_view import router as tag_router
 from views.search_engine_view import router as search_engine_router
+from views.image_view import router as image_router
 
 # 导入现有API路由
 # from app.api.v1.api_router import api_router
@@ -252,6 +253,7 @@ app.include_router(knowledge_base_router, prefix="/api/v1")
 app.include_router(snippet_router, prefix="/api/v1")
 app.include_router(tag_router, prefix="/api/v1")
 app.include_router(search_engine_router, prefix="/api/v1")
+app.include_router(image_router, prefix="/api/v1")
 
 
 def create_app() -> FastAPI:

+ 259 - 0
src/app/services/image_service.py

@@ -0,0 +1,259 @@
+"""
+图片管理服务层
+处理图片分类和图片信息的业务逻辑
+"""
+import logging
+import uuid
+import os
+from typing import Optional, List, Dict, Any, Tuple
+from datetime import datetime
+from app.base.async_mysql_connection import get_db_connection
+from app.base.minio_connection import get_minio_manager
+
+logger = logging.getLogger(__name__)
+
+class ImageService:
+    """图片管理服务类"""
+    
+    def __init__(self):
+        """初始化服务"""
+        self.minio_manager = get_minio_manager()
+
+    async def get_categories(self) -> List[Dict[str, Any]]:
+        """获取所有图片分类并构建树形结构"""
+        conn = get_db_connection()
+        if not conn:
+            return []
+        
+        cursor = conn.cursor()
+        try:
+            sql = "SELECT * FROM t_image_category ORDER BY created_time DESC"
+            cursor.execute(sql)
+            categories = cursor.fetchall()
+            
+            # 构建树形结构
+            category_dict = {str(cat['id']): {**cat, 'children': []} for cat in categories}
+            root_nodes = []
+            
+            for cat_id, cat_node in category_dict.items():
+                parent_id = str(cat_node['parent_id'])
+                if parent_id == '0':
+                    root_nodes.append(cat_node)
+                elif parent_id in category_dict:
+                    category_dict[parent_id]['children'].append(cat_node)
+                else:
+                    # 如果父节点找不到,也作为根节点(防御性编程)
+                    root_nodes.append(cat_node)
+            
+            return root_nodes
+        except Exception as e:
+            logger.exception(f"获取图片分类失败: {e}")
+            return []
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def add_category(self, data: Dict[str, Any], user_id: str) -> Tuple[bool, str, Optional[str]]:
+        """新增图片分类"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败", None
+        
+        cursor = conn.cursor()
+        try:
+            category_id = str(uuid.uuid4())
+            parent_id = data.get('parent_id', '0')
+            type_name = data.get('type_name')
+            remark = data.get('remark')
+            
+            if not type_name:
+                return False, "分类名称不能为空", None
+            
+            sql = """
+                INSERT INTO t_image_category (
+                    id, type_name, parent_id, remark, 
+                    created_by, updated_by, created_time, updated_time
+                ) VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
+            """
+            cursor.execute(sql, (category_id, type_name, parent_id, remark, user_id, user_id))
+            conn.commit()
+            return True, "新增成功", category_id
+        except Exception as e:
+            logger.exception(f"新增图片分类失败: {e}")
+            conn.rollback()
+            return False, f"新增失败: {str(e)}", None
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def update_category(self, category_id: str, data: Dict[str, Any], user_id: str) -> Tuple[bool, str]:
+        """更新图片分类"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            type_name = data.get('type_name')
+            remark = data.get('remark')
+            
+            if not type_name:
+                return False, "分类名称不能为空"
+            
+            sql = """
+                UPDATE t_image_category 
+                SET type_name = %s, remark = %s, updated_by = %s, updated_time = NOW()
+                WHERE id = %s
+            """
+            cursor.execute(sql, (type_name, remark, user_id, category_id))
+            conn.commit()
+            return True, "更新成功"
+        except Exception as e:
+            logger.exception(f"更新图片分类失败: {e}")
+            conn.rollback()
+            return False, f"更新失败: {str(e)}"
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def delete_category(self, category_id: str) -> Tuple[bool, str]:
+        """删除图片分类"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            # 检查是否有子分类
+            cursor.execute("SELECT COUNT(*) as count FROM t_image_category WHERE parent_id = %s", (category_id,))
+            if cursor.fetchone()['count'] > 0:
+                return False, "该分类下有子分类,不能直接删除"
+            
+            # 检查是否有图片
+            cursor.execute("SELECT COUNT(*) as count FROM t_image_info WHERE image_type = %s", (category_id,))
+            if cursor.fetchone()['count'] > 0:
+                return False, "该分类下有图片,不能直接删除"
+            
+            cursor.execute("DELETE FROM t_image_category WHERE id = %s", (category_id,))
+            conn.commit()
+            return True, "删除成功"
+        except Exception as e:
+            logger.exception(f"删除图片分类失败: {e}")
+            conn.rollback()
+            return False, f"删除失败: {str(e)}"
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def get_images(self, category_id: Optional[str] = None, page: int = 1, page_size: int = 10) -> Dict[str, Any]:
+        """获取图片列表"""
+        conn = get_db_connection()
+        if not conn:
+            return {"total": 0, "list": []}
+        
+        cursor = conn.cursor()
+        try:
+            where_clauses = []
+            params = []
+            
+            if category_id and category_id != '0':
+                where_clauses.append("image_type = %s")
+                params.append(category_id)
+            
+            where_sql = ""
+            if where_clauses:
+                where_sql = "WHERE " + " AND ".join(where_clauses)
+            
+            # 获取总数
+            count_sql = f"SELECT COUNT(*) as total FROM t_image_info {where_sql}"
+            cursor.execute(count_sql, tuple(params))
+            total = cursor.fetchone()['total']
+            
+            # 获取列表
+            offset = (page - 1) * page_size
+            list_sql = f"""
+                SELECT i.*, c.type_name as category_name, u.username as creator_name
+                FROM t_image_info i
+                LEFT JOIN t_image_category c ON i.image_type = c.id
+                LEFT JOIN t_sys_user u ON i.created_by = u.id
+                {where_sql}
+                ORDER BY i.created_time DESC
+                LIMIT %s OFFSET %s
+            """
+            cursor.execute(list_sql, tuple(params + [page_size, offset]))
+            images = cursor.fetchall()
+            
+            return {
+                "total": total,
+                "list": images,
+                "page": page,
+                "page_size": page_size
+            }
+        except Exception as e:
+            logger.exception(f"获取图片列表失败: {e}")
+            return {"total": 0, "list": []}
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def add_image(self, data: Dict[str, Any], user_id: str) -> Tuple[bool, str]:
+        """保存图片信息"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            image_id = str(uuid.uuid4())
+            image_name = data.get('image_name')
+            image_url = data.get('image_url')
+            image_type = data.get('image_type')
+            description = data.get('description')
+            
+            if not image_name or not image_url or not image_type:
+                return False, "缺少必要参数"
+            
+            sql = """
+                INSERT INTO t_image_info (
+                    id, image_name, image_url, image_type, description,
+                    created_by, updated_by, created_time, updated_time
+                ) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
+            """
+            cursor.execute(sql, (image_id, image_name, image_url, image_type, description, user_id, user_id))
+            conn.commit()
+            return True, "保存成功"
+        except Exception as e:
+            logger.exception(f"保存图片信息失败: {e}")
+            conn.rollback()
+            return False, f"保存失败: {str(e)}"
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def delete_image(self, image_id: str) -> Tuple[bool, str]:
+        """删除图片"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            cursor.execute("DELETE FROM t_image_info WHERE id = %s", (image_id,))
+            conn.commit()
+            return True, "删除成功"
+        except Exception as e:
+            logger.exception(f"删除图片失败: {e}")
+            conn.rollback()
+            return False, f"删除失败: {str(e)}"
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def get_upload_url(self, filename: str, content_type: str) -> Tuple[bool, str, Dict[str, Any]]:
+        """获取 MinIO 预签名上传 URL"""
+        try:
+            data = self.minio_manager.get_upload_url(filename, content_type)
+            return True, "成功获取上传链接", data
+        except Exception as e:
+            logger.exception("生成上传链接失败")
+            return False, f"生成上传链接失败: {str(e)}", {}

+ 73 - 44
src/app/services/sample_service.py

@@ -44,7 +44,12 @@ class SampleService:
     # ==================== 文档管理 ====================
     
     async def batch_enter_knowledge_base(self, doc_ids: List[str], username: str) -> Tuple[int, str]:
-        """批量将文档加入知识库"""
+        """批量将文档入库到知识库
+        
+        Args:
+            doc_ids: 文档ID列表
+            username: 操作人
+        """
         conn = get_db_connection()
         if not conn:
             return 0, "数据库连接失败"
@@ -52,49 +57,27 @@ class SampleService:
         cursor = conn.cursor()
         
         try:
-            # 批量更新主表 t_samp_document_main
-            placeholders = ', '.join(['%s'] * len(doc_ids))
+            # 1. 严格检查转换状态:只有 conversion_status = 2 (转换成功) 且 whether_to_enter = 0 (未入库) 的才能入库
+            check_sql = f"SELECT id, title FROM t_samp_document_main WHERE id IN ({','.join(['%s']*len(doc_ids))}) AND conversion_status = 2 AND whether_to_enter = 0"
+            cursor.execute(check_sql, tuple(doc_ids))
+            valid_docs = cursor.fetchall()
+            valid_ids = [doc['id'] for doc in valid_docs]
+            
+            if not valid_ids:
+                return 0, "选中的文档中没有满足入库条件(已转换成功且未入库)的记录"
+
+            # 2. 更新状态为已入库
+            update_sql = f"UPDATE t_samp_document_main SET whether_to_enter = 1, updated_by = %s, updated_time = NOW() WHERE id IN ({','.join(['%s']*len(valid_ids))})"
+            cursor.execute(update_sql, (username, *valid_ids))
             
-            # 更新主表
-            sql_main = f"""
-                UPDATE t_samp_document_main 
-                SET whether_to_enter = 1, updated_by = %s, updated_time = NOW() 
-                WHERE id IN ({placeholders}) AND whether_to_enter = 0
-            """
-            cursor.execute(sql_main, [username] + doc_ids)
             affected_rows = cursor.rowcount
-            
-            # 尝试更新对应的子表以保持同步
-            try:
-                cursor.execute(f"SELECT id, source_type, source_id FROM t_samp_document_main WHERE id IN ({placeholders})", doc_ids)
-                docs = cursor.fetchall()
-                
-                for doc_row in docs:
-                    d_id = doc_row['id']
-                    s_type = doc_row['source_type']
-                    s_id = doc_row['source_id']
-                    if s_type and s_id:
-                        sub_table = get_table_name(s_type)
-                        if sub_table:
-                            sub_sql = f"UPDATE {sub_table} SET whether_to_enter = 1, updated_time = NOW(), updated_by = %s WHERE id = %s"
-                            try:
-                                cursor.execute(sub_sql, (username, s_id))
-                            except Exception as sub_e:
-                                logger.error(f"更新子表 {sub_table} 入库状态失败: {sub_e}")
-            except Exception as sync_e:
-                logger.error(f"同步更新子表入库状态失败: {sync_e}")
-            
             conn.commit()
             
-            message = f"成功将 {affected_rows} 条数据加入知识库"
-            if affected_rows < len(doc_ids):
-                message += f"(跳过了 {len(doc_ids) - affected_rows} 条已入库数据或未找到数据)"
-            
-            return affected_rows, message
+            return affected_rows, f"成功入库 {affected_rows} 份文档"
         except Exception as e:
-            logger.exception("批量操作失败")
+            logger.exception(f"文档批量入库失败: {e}")
             conn.rollback()
-            return 0, f"批量操作失败: {str(e)}"
+            return 0, f"入库失败: {str(e)}"
         finally:
             cursor.close()
             conn.close()
@@ -696,14 +679,60 @@ class SampleService:
 
             # 2. 更新子表 (移除 file_url)
             if type == 'basis':
-                sql = f"UPDATE {table_name} SET chinese_name = %s, standard_number = %s, issuing_authority = %s, release_date = %s, document_type = %s, professional_field = %s, validity = %s, updated_by = %s, updated_time = NOW() WHERE id = %s"
-                params = (data.get('title'), data.get('standard_no'), data.get('issuing_authority'), self._to_date(data.get('release_date')), data.get('document_type'), data.get('professional_field'), data.get('validity'), updater_id, info_id)
+                sql = f"""
+                UPDATE {table_name} 
+                SET chinese_name = %s, standard_number = %s, issuing_authority = %s, release_date = %s, 
+                    document_type = %s, professional_field = %s, validity = %s, 
+                    english_name = %s, implementation_date = %s, drafting_unit = %s, 
+                    approving_department = %s, participating_units = %s, engineering_phase = %s, 
+                    reference_basis = %s, source_url = %s,
+                    updated_by = %s, updated_time = NOW() 
+                WHERE id = %s
+                """
+                params = (
+                    data.get('title'), data.get('standard_no'), data.get('issuing_authority'), self._to_date(data.get('release_date')), 
+                    data.get('document_type'), data.get('professional_field'), data.get('validity'),
+                    data.get('english_name'), self._to_date(data.get('implementation_date')), data.get('drafting_unit'),
+                    data.get('approving_department'), data.get('participating_units'), data.get('engineering_phase'),
+                    data.get('reference_basis'), data.get('source_url'),
+                    updater_id, info_id
+                )
             elif type == 'work':
-                sql = f"UPDATE {table_name} SET plan_name = %s, project_name = %s, project_section = %s, compiling_unit = %s, compiling_date = %s, updated_by = %s, updated_time = NOW() WHERE id = %s"
-                params = (data.get('title'), data.get('project_name'), data.get('project_section'), data.get('issuing_authority'), self._to_date(data.get('release_date')), updater_id, info_id)
+                # 构造 compilation_basis 更新部分
+                basis_updates = ", ".join([f"compilation_basis_{i} = %s" for i in range(1, 10)])
+                
+                sql = f"""
+                UPDATE {table_name} 
+                SET plan_name = %s, project_name = %s, project_section = %s, compiling_unit = %s, compiling_date = %s, 
+                    plan_summary = %s, {basis_updates},
+                    updated_by = %s, updated_time = NOW() 
+                WHERE id = %s
+                """
+                
+                # 准备 compilation_basis 参数
+                basis_params = [data.get(f'compilation_basis_{i}') for i in range(1, 10)]
+                
+                params = [
+                    data.get('title'), data.get('project_name'), data.get('project_section'), data.get('issuing_authority'), self._to_date(data.get('release_date')),
+                    data.get('plan_summary')
+                ] + basis_params + [updater_id, info_id]
+                
+                # 转换为 tuple
+                params = tuple(params)
+                
             elif type == 'job':
-                sql = f"UPDATE {table_name} SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s, updated_by = %s, updated_time = NOW() WHERE id = %s"
-                params = (data.get('title'), data.get('issuing_authority'), data.get('document_type'), self._to_date(data.get('release_date')), updater_id, info_id)
+                sql = f"""
+                UPDATE {table_name} 
+                SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s, 
+                    effective_start_date = %s, effective_end_date = %s,
+                    updated_by = %s, updated_time = NOW() 
+                WHERE id = %s
+                """
+                params = (
+                    data.get('title'), data.get('issuing_authority'), data.get('document_type'), self._to_date(data.get('release_date')), 
+                    self._to_date(data.get('effective_start_date')), self._to_date(data.get('effective_end_date')),
+                    updater_id, info_id
+                )
             else:
                 return False, "不支持的类型"
             

+ 194 - 0
src/views/image_view.py

@@ -0,0 +1,194 @@
+import logging
+from datetime import datetime, timezone
+from typing import Optional, List, Dict, Any
+
+from fastapi import APIRouter, Depends, HTTPException, Request, Response
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+from app.services.image_service import ImageService
+from app.services.jwt_token import verify_token
+from app.schemas.base import ApiResponse
+from pydantic import BaseModel
+
+# 获取logger
+logger = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/images", tags=["图片管理"])
+security = HTTPBearer()
+
+# --- 请求模型 ---
+
+class CategoryAdd(BaseModel):
+    type_name: str
+    parent_id: str = '0'
+    remark: Optional[str] = None
+
+class CategoryUpdate(BaseModel):
+    type_name: str
+    remark: Optional[str] = None
+
+class ImageAdd(BaseModel):
+    image_name: str
+    image_url: str
+    image_type: str
+    description: Optional[str] = None
+
+class UploadUrlRequest(BaseModel):
+    filename: str
+    content_type: str
+
+# --- 分类管理 API ---
+
+@router.get("/categories")
+async def get_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """获取所有图片分类树"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        service = ImageService()
+        data = await service.get_categories()
+        return ApiResponse(code=0, message="成功", data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("获取分类失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.post("/categories")
+async def add_category(data: CategoryAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """新增分类"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        user_id = payload.get("sub", "system")
+        service = ImageService()
+        success, message, category_id = await service.add_category(data.model_dump(), user_id)
+        
+        if success:
+            return ApiResponse(code=0, message=message, data={"id": category_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("新增分类失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.put("/categories/{category_id}")
+async def update_category(category_id: str, data: CategoryUpdate, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """更新分类"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        user_id = payload.get("sub", "system")
+        service = ImageService()
+        success, message = await service.update_category(category_id, data.model_dump(), user_id)
+        
+        if success:
+            return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("更新分类失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.delete("/categories/{category_id}")
+async def delete_category(category_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """删除分类"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        service = ImageService()
+        success, message = await service.delete_category(category_id)
+        
+        if success:
+            return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=400, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("删除分类失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+# --- 图片管理 API ---
+
+@router.get("")
+async def get_images(
+    category_id: Optional[str] = None, 
+    page: int = 1, 
+    page_size: int = 10, 
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """获取图片列表"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        service = ImageService()
+        data = await service.get_images(category_id, page, page_size)
+        return ApiResponse(code=0, message="成功", data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("获取图片列表失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.post("")
+async def add_image(data: ImageAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """保存图片信息"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        user_id = payload.get("sub", "system")
+        service = ImageService()
+        success, message = await service.add_image(data.model_dump(), user_id)
+        
+        if success:
+            return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("保存图片失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.delete("/{image_id}")
+async def delete_image(image_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """删除图片"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        service = ImageService()
+        success, message = await service.delete_image(image_id)
+        
+        if success:
+            return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("删除图片失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+@router.post("/upload-url")
+async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """获取 MinIO 预签名上传 URL"""
+    try:
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        service = ImageService()
+        success, message, data = await service.get_upload_url(req.filename, req.content_type)
+        
+        if success:
+            return ApiResponse(code=0, message=message, data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        else:
+            return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+    except Exception as e:
+        logger.exception("获取上传链接失败")
+        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است