chenkun преди 1 месец
родител
ревизия
3c87a5d431

+ 4 - 0
run_server.py

@@ -97,11 +97,15 @@ def main():
     
     # 启动服务器
     try:
+        # 排除 logs 目录,避免写日志导致热重载进入死循环
+        reload_dirs = [os.path.join(project_root, "src")] if reload else None
+        
         uvicorn.run(
             "src.app.server.app:app",
             host=host,
             port=port,
             reload=reload,
+            reload_dirs=reload_dirs,
             log_level=config_handler.get("admin_app", "LOG_LEVEL", "INFO").lower(),
             access_log=True,
             use_colors=True

+ 0 - 61
scripts/fix_app_id_nullable.py

@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-"""
-修改 oauth_access_tokens 表的 app_id 字段为可空
-"""
-import pymysql
-import os
-from urllib.parse import urlparse
-
-# 导入配置
-import sys
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
-from app.core.config import config_handler
-
-print("=" * 60)
-print("修改 oauth_access_tokens 表结构")
-print("=" * 60)
-
-# 获取数据库配置
-database_url = config_handler.get("admin_app", "DATABASE_URL", "")
-parsed = urlparse(database_url)
-
-try:
-    conn = pymysql.connect(
-        host=parsed.hostname,
-        port=parsed.port,
-        user=parsed.username,
-        password=parsed.password,
-        database=parsed.path[1:] if parsed.path else None,
-        charset='utf8mb4'
-    )
-    
-    cursor = conn.cursor()
-    
-    # 修改 app_id 字段为可空
-    print("\n修改 app_id 字段为可空...")
-    cursor.execute("""
-        ALTER TABLE oauth_access_tokens 
-        MODIFY COLUMN app_id CHAR(36) NULL COMMENT '应用ID(系统内部登录时可为空)'
-    """)
-    
-    conn.commit()
-    print("✅ 修改成功!")
-    
-    # 验证修改
-    cursor.execute("DESCRIBE oauth_access_tokens")
-    columns = cursor.fetchall()
-    
-    print("\n表结构:")
-    for col in columns:
-        if col[0] == 'app_id':
-            print(f"  app_id: {col[1]}, NULL={col[2]}, Key={col[3]}, Default={col[4]}")
-    
-    cursor.close()
-    conn.close()
-    
-except Exception as e:
-    print(f"❌ 错误: {e}")
-    import traceback
-    traceback.print_exc()
-
-print("\n" + "=" * 60)

+ 0 - 62
scripts/fix_db_indexes.py

@@ -1,62 +0,0 @@
-import os
-import pymysql
-from urllib.parse import urlparse
-
-# 导入配置
-import sys
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
-from app.core.config import config_handler
-
-def fix_indexes():
-    """执行索引添加 SQL"""
-    database_url = config_handler.get("admin_app", "DATABASE_URL", "")
-    if not database_url:
-        print("❌ 错误: 未在配置中找到 DATABASE_URL")
-        return
-        
-    parsed = urlparse(database_url)
-    config = {
-        'host': parsed.hostname or 'localhost',
-        'port': parsed.port or 3306,
-        'user': parsed.username or 'root',
-        'password': parsed.password or '',
-        'database': parsed.path[1:] if parsed.path else 'sso_db',
-        'charset': 'utf8mb4',
-        'autocommit': True
-    }
-    
-    print(f"📡 正在尝试连接数据库: {config['host']}...")
-    
-    conn = None
-    try:
-        conn = pymysql.connect(**config)
-        cursor = conn.cursor()
-        
-        tables = ['t_basis_of_preparation', 't_work_of_preparation', 't_job_of_preparation']
-        
-        for table in tables:
-            print(f"⚡ 正在为 {table} 添加索引...")
-            try:
-                # 检查索引是否已存在,防止重复添加报错
-                cursor.execute(f"SHOW INDEX FROM {table} WHERE Key_name = 'idx_enter_status'")
-                if cursor.fetchone():
-                    print(f"   ✅ {table} 的索引已存在,跳过。")
-                    continue
-                    
-                sql = f"ALTER TABLE {table} ADD INDEX idx_enter_status (whether_to_enter)"
-                cursor.execute(sql)
-                print(f"   ✅ {table} 索引添加成功!")
-            except Exception as e:
-                print(f"   ❌ {table} 处理失败: {e}")
-        
-        print("\n🎉 所有任务处理完成!现在您可以重新启动后端服务了。")
-        
-    except Exception as e:
-        print(f"\n❌ 数据库连接失败: {e}")
-        print("💡 提示: 请确保您已经关闭了 full_server.py,否则连接可能被占用。")
-    finally:
-        if conn:
-            conn.close()
-
-if __name__ == "__main__":
-    fix_indexes()

+ 80 - 22
scripts/miner_u.py

@@ -1,25 +1,79 @@
 import os
 import time
 import json
+import logging
 import requests
 import pymysql
 import zipfile
 import io
+from datetime import datetime
 from pathlib import Path
 from urllib.parse import urlparse
+from minio import Minio
+
+# 配置日志
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger("MinerU")
 
 # 导入配置
 import sys
 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
 from app.core.config import config_handler
 
-TOKEN = "eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFM1MTIifQ.eyJqdGkiOiI1MzgwMDYyNSIsInJvbCI6IlJPTEVfUkVHSVNURVIiLCJpc3MiOiJPcGVuWExhYiIsImlhdCI6MTc2Nzg1OTg5NywiY2xpZW50SWQiOiJsa3pkeDU3bnZ5MjJqa3BxOXgydyIsInBob25lIjoiMTgwMzA5ODIxNTQiLCJvcGVuSWQiOm51bGwsInV1aWQiOiI0NTYyZTUyNi1iZjE3LTRhMmItODExMi04YmM5ZjNjYzMwZGMiLCJlbWFpbCI6IiIsImV4cCI6MTc2OTA2OTQ5N30.mNH7afPPANNQq_BRsBOlbk-2P7e_ewdfzPQXO4woeoT15mDEbPKc45Auk_BuRuNaAS-Gm2GK3qKGjQ2VDtepvA"
-API_APPLY = "https://mineru.net/api/v4/file-urls/batch"
-API_BATCH_RESULT = "https://mineru.net/api/v4/extract-results/batch/{}"
+# MinIO 配置
+MINIO_ENDPOINT = config_handler.get("admin_app", "MINIO_ENDPOINT", "192.168.91.15:19000")
+MINIO_ACCESS_KEY = config_handler.get("admin_app", "MINIO_ACCESS_KEY", "minioadmin")
+MINIO_SECRET_KEY = config_handler.get("admin_app", "MINIO_SECRET_KEY", "minioadmin")
+MINIO_BUCKET = config_handler.get("admin_app", "MINIO_BUCKET_NAME", "aidata")
+MINIO_USE_SSL = config_handler.get_bool("admin_app", "MINIO_USE_SSL", False)
+MINIO_BASE_PATH = config_handler.get("admin_app", "MINIO_BASE_PATH", "sampledata")
+
+def get_minio_client():
+    try:
+        return Minio(
+            MINIO_ENDPOINT,
+            access_key=MINIO_ACCESS_KEY,
+            secret_key=MINIO_SECRET_KEY,
+            secure=MINIO_USE_SSL
+        )
+    except Exception as e:
+        logger.error(f"MinIO client init failed: {e}")
+        return None
+
+def upload_to_minio(file_content, object_name, content_type="text/markdown"):
+    client = get_minio_client()
+    if not client:
+        return None
+    try:
+        # 确保桶存在
+        if not client.bucket_exists(MINIO_BUCKET):
+            client.make_bucket(MINIO_BUCKET)
+        
+        # 上传文件
+        client.put_object(
+            MINIO_BUCKET,
+            object_name,
+            io.BytesIO(file_content),
+            len(file_content),
+            content_type=content_type
+        )
+        # 返回访问链接
+        return f"http://{MINIO_ENDPOINT}/{MINIO_BUCKET}/{object_name}"
+    except Exception as e:
+        logger.error(f"Upload to MinIO failed: {e}")
+        return None
+
+# MinERU 配置
+MINERU_TOKEN = config_handler.get("admin_app", "MINERU_TOKEN", "")
+API_APPLY = config_handler.get("admin_app", "MINERU_API_APPLY", "https://mineru.net/api/v4/file-urls/batch")
+API_BATCH_RESULT = config_handler.get("admin_app", "MINERU_API_BATCH_RESULT", "https://mineru.net/api/v4/extract-results/batch/{}")
 
 HEADERS = {
     "Content-Type": "application/json",
-    "Authorization": f"Bearer {TOKEN}",
+    "Authorization": f"Bearer {MINERU_TOKEN}",
 }
 
 SUPPORTED_SUFFIX = {".pdf", ".doc", ".docx", ".ppt", ".pptx", ".png", ".jpg", ".jpeg", ".html"}
@@ -27,7 +81,7 @@ SUPPORTED_SUFFIX = {".pdf", ".doc", ".docx", ".ppt", ".pptx", ".png", ".jpg", ".
 def get_db_connection():
     database_url = config_handler.get("admin_app", "DATABASE_URL", "")
     if not database_url:
-        print("DATABASE_URL not found in configuration")
+        logger.error("DATABASE_URL not found in configuration")
         return None
     try:
         parsed = urlparse(database_url)
@@ -41,7 +95,7 @@ def get_db_connection():
             autocommit=True
         )
     except Exception as e:
-        print(f"Database connection error: {e}")
+        logger.error(f"Database connection error: {e}")
         return None
 
 def update_db_status(doc_id, status=None, progress=None, error=None, converted_file_name=None):
@@ -75,7 +129,7 @@ def update_db_status(doc_id, status=None, progress=None, error=None, converted_f
             params.append(doc_id)
             cursor.execute(sql, params)
     except Exception as e:
-        print(f"Update DB failed: {e}")
+        logger.error(f"Update DB failed: {e}")
     finally:
         conn.close()
 
@@ -121,7 +175,7 @@ def process_document(doc_id, chinese_name, file_url, out_dir):
         update_db_status(doc_id, status=1, progress=10)
         
         # 2. 下载原始文件
-        print(f"Downloading {file_url}...")
+        logger.info(f"Downloading {file_url}...")
         resp = requests.get(file_url, timeout=60)
         resp.raise_for_status()
         file_content = resp.content
@@ -157,21 +211,25 @@ def process_document(doc_id, chinese_name, file_url, out_dir):
                 zip_resp = requests.get(zip_url, timeout=300)
                 zip_resp.raise_for_status()
                 
-                # 解压并保存 Markdown
-                converted_file_name = f"{chinese_name}.md"
+                # 解压并处理结果
                 with zipfile.ZipFile(io.BytesIO(zip_resp.content)) as z:
                     # 查找 .md 文件
                     md_files = [f for f in z.namelist() if f.endswith(".md")]
                     if md_files:
                         md_content = z.read(md_files[0])
-                        save_path = Path(out_dir) / converted_file_name
-                        save_path.parent.mkdir(parents=True, exist_ok=True)
-                        with open(save_path, "wb") as f:
-                            f.write(md_content)
-                        print(f"Saved Markdown to {save_path}")
-                
-                update_db_status(doc_id, status=2, progress=100, converted_file_name=converted_file_name)
-                return True
+                        # 构造云端存储路径
+                        object_name = f"{MINIO_BASE_PATH}/converted/{datetime.now().strftime('%Y%m%d')}/{doc_id}.md"
+                        # 上传到 MinIO
+                        cloud_url = upload_to_minio(md_content, object_name)
+                        
+                        if cloud_url:
+                            logger.info(f"Uploaded converted file to {cloud_url}")
+                            update_db_status(doc_id, status=2, progress=100, converted_file_name=cloud_url)
+                            return True
+                        else:
+                            raise RuntimeError("Failed to upload converted file to MinIO")
+                    else:
+                        raise RuntimeError("No .md file found in the converted zip")
             else:
                 raise RuntimeError("No zip URL in result")
         else:
@@ -179,7 +237,7 @@ def process_document(doc_id, chinese_name, file_url, out_dir):
             raise RuntimeError(f"MinerU extraction failed: {err_msg}")
             
     except Exception as e:
-        print(f"Process failed: {e}")
+        logger.error(f"Process failed: {e}")
         update_db_status(doc_id, status=3, error=str(e))
         return False
 
@@ -187,7 +245,7 @@ def main_cli(doc_id, out_dir=r"d:\UGit\MinerU"):
     # 从数据库获取详细信息 - 直接从 t_document_main 获取
     conn = get_db_connection()
     if not conn:
-        print("Database connection failed")
+        logger.error("Database connection failed")
         return
         
     try:
@@ -197,7 +255,7 @@ def main_cli(doc_id, out_dir=r"d:\UGit\MinerU"):
             row = cursor.fetchone()
             if not row or not row[1]: # 如果主表没有 file_url,尝试从子表获取
                 if not row:
-                    print(f"Document not found: {doc_id}")
+                    logger.warning(f"Document not found: {doc_id}")
                     return
                 
                 title = row[0]
@@ -226,7 +284,7 @@ def main_cli(doc_id, out_dir=r"d:\UGit\MinerU"):
                             except:
                                 continue
                 
-                print(f"No file_url found for document: {doc_id}")
+                logger.error(f"No file_url found for document: {doc_id}")
                 update_db_status(doc_id, status=3, error="未找到文件链接(file_url)")
                 return
             

+ 0 - 0
scripts/test.sql


+ 0 - 235
scripts/verify_structure.py

@@ -1,235 +0,0 @@
-#!/usr/bin/env python3
-"""
-项目结构验证脚本
-验证重构后的项目结构是否符合要求
-"""
-import os
-import sys
-from pathlib import Path
-
-# 项目根目录
-PROJECT_ROOT = Path(__file__).parent.parent
-
-# 期望的目录结构
-EXPECTED_STRUCTURE = {
-    "src/app/server": ["__init__.py", "app.py"],
-    "src/app/logger": ["__init__.py", "loggering.py"],
-    "src/app/base": [
-        "__init__.py",
-        "async_mysql_connection.py",
-        "async_redis_connection.py",
-        "milvus_connection.py"
-    ],
-    "src/app/core": ["__init__.py", "exceptions.py"],
-    "src/app/models": [
-        "__init__.py",
-        "base.py",
-        "user.py",
-        "app.py",
-        "token.py",
-        "log.py",
-        "knowledge_base.py"
-    ],
-    "src/app/schemas": [
-        "__init__.py",
-        "base.py",
-        "user.py",
-        "auth.py"
-    ],
-    "src/app/utils": ["__init__.py", "security.py"],
-    "src/app/config": [
-        "__init__.py",
-        "settings.py",
-        "database.py",
-        "simple_settings.py"
-    ],
-    "src/app/system": ["__init__.py"],
-    "src/app/system/models": ["__init__.py"],
-    "src/app/system/schemas": ["__init__.py"],
-    "src/app/oauth": ["__init__.py"],
-    "src/app/oauth/models": ["__init__.py"],
-    "src/app/oauth/schemas": ["__init__.py"],
-    "src/app/sample": ["__init__.py"],
-    "src/app/sample/models": ["__init__.py"],
-    "src/app/sample/schemas": ["__init__.py"],
-    "src/views": [
-        "__init__.py",
-        "system_view.py",
-        "oauth_view.py",
-        "sample_view.py"
-    ],
-}
-
-# 期望的根目录文件
-EXPECTED_ROOT_FILES = [
-    "run_server.py",
-    "full_server.py",
-    "README.md",
-    "REFACTOR_README.md",
-    "MIGRATION_GUIDE.md",
-    ".env.example",
-    ".gitignore"
-]
-
-
-def check_directory_structure():
-    """检查目录结构"""
-    print("=" * 60)
-    print("检查项目目录结构")
-    print("=" * 60)
-    
-    all_passed = True
-    
-    for directory, files in EXPECTED_STRUCTURE.items():
-        dir_path = PROJECT_ROOT / directory
-        
-        # 检查目录是否存在
-        if not dir_path.exists():
-            print(f"❌ 目录不存在: {directory}")
-            all_passed = False
-            continue
-        
-        print(f"✅ 目录存在: {directory}")
-        
-        # 检查文件是否存在
-        for file in files:
-            file_path = dir_path / file
-            if not file_path.exists():
-                print(f"  ❌ 文件缺失: {directory}/{file}")
-                all_passed = False
-            else:
-                print(f"  ✅ 文件存在: {file}")
-    
-    return all_passed
-
-
-def check_root_files():
-    """检查根目录文件"""
-    print("\n" + "=" * 60)
-    print("检查根目录文件")
-    print("=" * 60)
-    
-    all_passed = True
-    
-    for file in EXPECTED_ROOT_FILES:
-        file_path = PROJECT_ROOT / file
-        if not file_path.exists():
-            print(f"⚠️  文件缺失: {file}")
-            if file not in [".env.example"]:  # 某些文件可选
-                all_passed = False
-        else:
-            print(f"✅ 文件存在: {file}")
-    
-    return all_passed
-
-
-def check_imports():
-    """检查关键模块是否可以导入"""
-    print("\n" + "=" * 60)
-    print("检查模块导入")
-    print("=" * 60)
-    
-    # 添加 src 到路径
-    sys.path.insert(0, str(PROJECT_ROOT / "src"))
-    
-    modules_to_check = [
-        "app.server.app",
-        "app.logger.loggering",
-        "app.base",
-        "app.core.exceptions",
-        "app.system",
-        "app.oauth",
-        "app.sample",
-        "views.system_view",
-        "views.oauth_view",
-        "views.sample_view",
-    ]
-    
-    all_passed = True
-    
-    for module in modules_to_check:
-        try:
-            __import__(module)
-            print(f"✅ 模块可导入: {module}")
-        except ImportError as e:
-            print(f"❌ 模块导入失败: {module}")
-            print(f"   错误: {e}")
-            all_passed = False
-        except Exception as e:
-            print(f"⚠️  模块导入警告: {module}")
-            print(f"   错误: {e}")
-    
-    return all_passed
-
-
-def check_module_structure():
-    """检查模块结构是否符合要求"""
-    print("\n" + "=" * 60)
-    print("检查模块结构")
-    print("=" * 60)
-    
-    modules = ["system", "oauth", "sample"]
-    all_passed = True
-    
-    for module in modules:
-        module_path = PROJECT_ROOT / "src" / "app" / module
-        
-        # 检查是否有 models 和 schemas 子目录
-        models_path = module_path / "models"
-        schemas_path = module_path / "schemas"
-        
-        if models_path.exists() and (models_path / "__init__.py").exists():
-            print(f"✅ {module}/models 结构正确")
-        else:
-            print(f"❌ {module}/models 结构不正确")
-            all_passed = False
-        
-        if schemas_path.exists() and (schemas_path / "__init__.py").exists():
-            print(f"✅ {module}/schemas 结构正确")
-        else:
-            print(f"❌ {module}/schemas 结构不正确")
-            all_passed = False
-    
-    return all_passed
-
-
-def main():
-    """主函数"""
-    print("\n" + "=" * 60)
-    print("LQAdminPlatform 项目结构验证")
-    print("=" * 60)
-    print(f"项目根目录: {PROJECT_ROOT}")
-    print("=" * 60 + "\n")
-    
-    results = []
-    
-    # 执行各项检查
-    results.append(("目录结构", check_directory_structure()))
-    results.append(("根目录文件", check_root_files()))
-    results.append(("模块结构", check_module_structure()))
-    results.append(("模块导入", check_imports()))
-    
-    # 输出总结
-    print("\n" + "=" * 60)
-    print("验证结果总结")
-    print("=" * 60)
-    
-    all_passed = True
-    for name, passed in results:
-        status = "✅ 通过" if passed else "❌ 失败"
-        print(f"{name}: {status}")
-        if not passed:
-            all_passed = False
-    
-    print("=" * 60)
-    
-    if all_passed:
-        print("\n🎉 所有检查通过!项目结构符合要求。")
-        return 0
-    else:
-        print("\n⚠️  部分检查未通过,请查看上面的详细信息。")
-        return 1
-
-
-if __name__ == "__main__":
-    sys.exit(main())

+ 13 - 0
src/app/config/config.ini

@@ -78,6 +78,19 @@ SESSION_TTL=86400
 CELERY_BROKER_URL=redis://localhost:6379/1
 CELERY_RESULT_BACKEND=redis://localhost:6379/2
 
+# MinIO配置
+MINIO_ENDPOINT=192.168.91.15:19000
+MINIO_ACCESS_KEY=HVFOa38zUDg3X6c14hpH
+MINIO_SECRET_KEY=ilVITuWEmjG0YDBAWspSG6UniHpEoMDECSuCo0f1
+MINIO_BUCKET_NAME=aidata
+MINIO_USE_SSL=False
+MINIO_BASE_PATH=sampledata
+
+# MinERU 配置
+MINERU_TOKEN=eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFM1MTIifQ.eyJqdGkiOiI1MzgwMDYyNSIsInJvbCI6IlJPTEVfUkVHSVNURVIiLCJpc3MiOiJPcGVuWExhYiIsImlhdCI6MTc2Nzg1OTg5NywiY2xpZW50SWQiOiJsa3pkeDU3bnZ5MjJqa3BxOXgydyIsInBob25lIjoiMTgwMzA5ODIxNTQiLCJvcGVuSWQiOm51bGwsInV1aWQiOiI0NTYyZTUyNi1iZjE3LTRhMmItODExMi04YmM5ZjNjYzMwZGMiLCJlbWFpbCI6IiIsImV4cCI6MTc2OTA2OTQ5N30.mNH7afPPANNQq_BRsBOlbk-2P7e_ewdfzPQXO4woeoT15mDEbPKc45Auk_BuRuNaAS-Gm2GK3qKGjQ2VDtepvA
+MINERU_API_APPLY=https://mineru.net/api/v4/file-urls/batch
+MINERU_API_BATCH_RESULT=https://mineru.net/api/v4/extract-results/batch/{}
+
 
 # embedding模型配置
 EMBEDDING_BASE_URL=http://192.168.91.253:9003/v1

+ 10 - 1
src/app/sample/schemas/sample_schemas.py

@@ -50,4 +50,13 @@ class DocumentListRequest(BaseModel):
     size: int = 50
     keyword: Optional[str] = None
     table_type: Optional[str] = None
-    whether_to_enter: Optional[int] = None
+    whether_to_enter: Optional[int] = None
+
+class UploadUrlRequest(BaseModel):
+    filename: str
+    content_type: str
+
+class UploadUrlResponse(BaseModel):
+    upload_url: str
+    file_url: str
+    object_name: str

+ 330 - 175
src/app/services/sample_service.py

@@ -2,18 +2,14 @@
 样本中心服务层
 从 sample_view.py 提取的SQL查询逻辑
 """
-import sys
 import os
-
-# 添加src目录到Python路径
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
-
 import logging
 import uuid
 from typing import Optional, List, Dict, Any, Tuple
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
+from minio import Minio
 from app.base.async_mysql_connection import get_db_connection
+from app.core.config import config_handler
 
 logger = logging.getLogger(__name__)
 
@@ -35,8 +31,58 @@ class SampleService:
     
     def __init__(self):
         """初始化服务"""
-        pass
-    
+        # 初始化 MinIO 客户端
+        try:
+            self.minio_endpoint = config_handler.get("admin_app", "MINIO_ENDPOINT", "192.168.91.15:19000")
+            self.minio_access_key = config_handler.get("admin_app", "MINIO_ACCESS_KEY", "minioadmin")
+            self.minio_secret_key = config_handler.get("admin_app", "MINIO_SECRET_KEY", "minioadmin")
+            self.minio_bucket = config_handler.get("admin_app", "MINIO_BUCKET_NAME", "lq-bucket")
+            self.minio_secure = config_handler.get_bool("admin_app", "MINIO_USE_SSL", False)
+            self.minio_base_path = config_handler.get("admin_app", "MINIO_BASE_PATH", "lqadmin")
+            
+            self.minio_client = Minio(
+                self.minio_endpoint,
+                access_key=self.minio_access_key,
+                secret_key=self.minio_secret_key,
+                secure=self.minio_secure
+            )
+            # 确保桶存在
+            if not self.minio_client.bucket_exists(self.minio_bucket):
+                self.minio_client.make_bucket(self.minio_bucket)
+        except Exception as e:
+            logger.error(f"MinIO 初始化失败: {e}")
+            self.minio_client = None
+
+    async def get_upload_url(self, filename: str, content_type: str) -> Tuple[bool, str, Dict[str, Any]]:
+        """获取 MinIO 预签名上传 URL"""
+        if not self.minio_client:
+            return False, "MinIO 服务不可用", {}
+            
+        try:
+            unique_id = str(uuid.uuid4())
+            ext = os.path.splitext(filename)[1]
+            object_name = f"{self.minio_base_path}/uploads/{datetime.now().strftime('%Y%m%d')}/{unique_id}{ext}"
+            
+            # 生成预签名 URL (PUT)
+            upload_url = self.minio_client.presigned_put_object(
+                self.minio_bucket,
+                object_name,
+                expires=timedelta(minutes=15)
+            )
+            
+            # 构造访问 URL
+            # 如果是开发环境,可能需要处理 endpoint 转换,这里直接拼接
+            file_url = f"http://{self.minio_endpoint}/{self.minio_bucket}/{object_name}"
+            
+            return True, "成功获取上传链接", {
+                "upload_url": upload_url,
+                "file_url": file_url,
+                "object_name": object_name
+            }
+        except Exception as e:
+            logger.exception("生成上传链接失败")
+            return False, f"生成上传链接失败: {str(e)}", {}
+
     # ==================== 文档管理 ====================
     
     async def batch_enter_knowledge_base(self, doc_ids: List[str], username: str) -> Tuple[int, str]:
@@ -146,6 +192,9 @@ class SampleService:
         whether_to_enter: Optional[int] = None,
         keyword: Optional[str] = None,
         table_type: Optional[str] = None,
+        primary_category_id: Optional[str] = None,
+        secondary_category_id: Optional[str] = None,
+        year: Optional[int] = None,
         page: int = 1,
         size: int = 50
     ) -> Tuple[List[Dict[str, Any]], int, int, int]:
@@ -166,9 +215,18 @@ class SampleService:
             if whether_to_enter is not None:
                 where_clauses.append("whether_to_enter = %s")
                 params.append(whether_to_enter)
+            if primary_category_id:
+                where_clauses.append("primary_category_id = %s")
+                params.append(primary_category_id)
+            if secondary_category_id:
+                where_clauses.append("secondary_category_id = %s")
+                params.append(secondary_category_id)
+            if year:
+                where_clauses.append("year = %s")
+                params.append(year)
             if keyword:
-                where_clauses.append("(title LIKE %s OR content LIKE %s)")
-                params.extend([f"%{keyword}%", f"%{keyword}%"])
+                where_clauses.append("(title LIKE %s OR content LIKE %s OR standard_no LIKE %s OR issuing_authority LIKE %s OR document_type LIKE %s)")
+                params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
             
             where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
             offset = (page - 1) * size
@@ -176,11 +234,11 @@ class SampleService:
             sql = f"SELECT * FROM t_document_main {where_sql} ORDER BY created_time DESC LIMIT %s OFFSET %s"
             params.extend([size, offset])
             
+            logger.info(f"Executing SQL: {sql} with params: {params}")
             cursor.execute(sql, tuple(params))
-            columns = [desc[0] for desc in cursor.description]
             items = []
             for row in cursor.fetchall():
-                item = dict(zip(columns, row))
+                item = row # DictCursor already returns dict
                 # 格式化时间
                 for key in ['created_time', 'updated_time', 'release_date']:
                     if item.get(key) and hasattr(item[key], 'isoformat'):
@@ -188,15 +246,19 @@ class SampleService:
                 items.append(item)
             
             # 总数
-            count_sql = f"SELECT COUNT(*) FROM t_document_main {where_sql}"
+            count_sql = f"SELECT COUNT(*) as count FROM t_document_main {where_sql}"
             cursor.execute(count_sql, tuple(params[:-2]))
-            total = cursor.fetchone()[0]
+            res = cursor.fetchone()
+            total = res['count'] if res else 0
             
             # 统计数据
-            cursor.execute("SELECT COUNT(*) FROM t_document_main")
-            all_total = cursor.fetchone()[0]
-            cursor.execute("SELECT COUNT(*) FROM t_document_main WHERE whether_to_enter = 1")
-            total_entered = cursor.fetchone()[0]
+            cursor.execute("SELECT COUNT(*) as count FROM t_document_main")
+            res = cursor.fetchone()
+            all_total = res['count'] if res else 0
+            
+            cursor.execute("SELECT COUNT(*) as count FROM t_document_main WHERE whether_to_enter = 1")
+            res = cursor.fetchone()
+            total_entered = res['count'] if res else 0
             
             return items, total, all_total, total_entered
         except Exception as e:
@@ -217,13 +279,10 @@ class SampleService:
         try:
             # 查询主表
             cursor.execute("SELECT * FROM t_document_main WHERE id = %s", (doc_id,))
-            main_row = cursor.fetchone()
-            if not main_row:
+            doc = cursor.fetchone()
+            if not doc:
                 return None
             
-            columns = [desc[0] for desc in cursor.description]
-            doc = dict(zip(columns, main_row))
-            
             # 查询子表
             source_type = doc.get('source_type')
             source_id = doc.get('source_id')
@@ -231,11 +290,8 @@ class SampleService:
             
             if table_name and source_id:
                 cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (source_id,))
-                sub_row = cursor.fetchone()
-                if sub_row:
-                    sub_columns = [desc[0] for desc in cursor.description]
-                    sub_data = dict(zip(sub_columns, sub_row))
-                    
+                sub_data = cursor.fetchone()
+                if sub_data:
                     # 将子表字段映射到通用字段名
                     if source_type == 'basis':
                         doc['standard_no'] = sub_data.get('standard_number')
@@ -254,13 +310,10 @@ class SampleService:
                         doc['document_type'] = sub_data.get('document_type')
                         doc['release_date'] = str(sub_data.get('publish_date')) if sub_data.get('publish_date') else None
             
-            # 格式化主表时间
-            if doc.get('created_time'):
-                doc['created_time'] = doc['created_time'].isoformat()
-            if doc.get('updated_time'):
-                doc['updated_time'] = doc['updated_time'].isoformat()
-            if doc.get('release_date') and not isinstance(doc['release_date'], str):
-                doc['release_date'] = doc['release_date'].isoformat()
+            # 格式化时间
+            for key in ['created_time', 'updated_time', 'release_date']:
+                if doc.get(key) and hasattr(doc[key], 'isoformat'):
+                    doc[key] = doc[key].isoformat()
             
             return doc
         except Exception as e:
@@ -270,6 +323,24 @@ class SampleService:
             cursor.close()
             conn.close()
     
+    def _to_int(self, value: Any) -> Optional[int]:
+        """安全转换为整数"""
+        if value is None or value == '' or str(value).lower() == 'null':
+            return None
+        try:
+            return int(value)
+        except (ValueError, TypeError):
+            return None
+
+    def _to_date(self, value: Any) -> Optional[str]:
+        """安全处理日期字符串"""
+        if value is None or value == '' or str(value).lower() == 'null':
+            return None
+        # 如果已经是 datetime.date 或 datetime.datetime 对象
+        if hasattr(value, 'strftime'):
+            return value.strftime('%Y-%m-%d')
+        return str(value)
+
     async def add_document(self, doc_data: Dict[str, Any], user_id: str) -> Tuple[bool, str, Optional[str]]:
         """添加新文档(同步主表和子表)"""
         conn = get_db_connection()
@@ -281,36 +352,47 @@ class SampleService:
         try:
             doc_id = str(uuid.uuid4())
             source_id = str(uuid.uuid4())
-            table_name = TABLE_MAP.get(doc_data.get('table_type'), "t_basis_of_preparation")
+            table_type = doc_data.get('table_type', 'basis')
+            table_name = TABLE_MAP.get(table_type, "t_basis_of_preparation")
+            
+            # 安全转换字段
+            p_cat_id = self._to_int(doc_data.get('primary_category_id'))
+            s_cat_id = self._to_int(doc_data.get('secondary_category_id'))
+            year = self._to_int(doc_data.get('year'))
+            release_date = self._to_date(doc_data.get('release_date'))
             
-            # 插入子表
-            table_type = doc_data.get('table_type')
+            # 插入子表 (会触发数据库触发器自动向 t_document_main 插入记录)
             if table_type == 'basis':
                 cursor.execute(
-                    f"INSERT INTO {table_name} (id, chinese_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc_data.get('title'), user_id)
+                    f"INSERT INTO {table_name} (id, chinese_name, standard_number, issuing_authority, release_date, document_type, professional_field, validity, file_url, content, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())",
+                    (source_id, doc_data.get('title'), doc_data.get('standard_no'), doc_data.get('issuing_authority'), release_date, doc_data.get('document_type'), doc_data.get('professional_field'), doc_data.get('validity'), doc_data.get('file_url'), doc_data.get('content'), user_id)
                 )
             elif table_type == 'work':
                 cursor.execute(
-                    f"INSERT INTO {table_name} (id, plan_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc_data.get('title'), user_id)
+                    f"INSERT INTO {table_name} (id, plan_name, project_name, project_section, compiling_unit, compiling_date, file_url, content, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())",
+                    (source_id, doc_data.get('title'), doc_data.get('project_name'), doc_data.get('project_section'), doc_data.get('issuing_authority'), release_date, doc_data.get('file_url'), doc_data.get('content'), user_id)
                 )
             elif table_type == 'job':
                 cursor.execute(
-                    f"INSERT INTO {table_name} (id, file_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc_data.get('title'), user_id)
+                    f"INSERT INTO {table_name} (id, file_name, issuing_department, document_type, publish_date, file_url, content, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())",
+                    (source_id, doc_data.get('title'), doc_data.get('issuing_authority'), doc_data.get('document_type'), release_date, doc_data.get('file_url'), doc_data.get('content'), user_id)
                 )
             
-            # 插入主表
+            # 获取触发器自动生成的 doc_id
+            cursor.execute("SELECT id FROM t_document_main WHERE source_id = %s AND source_type = %s", (source_id, table_type))
+            row = cursor.fetchone()
+            if not row:
+                raise Exception("触发器未能在主表创建记录")
+            doc_id = row['id']
+
+            # 更新主表中触发器未处理的字段
             cursor.execute("""
-                INSERT INTO t_document_main 
-                (id, title, content, created_by, source_type, source_id, whether_to_enter, 
-                 primary_category_id, secondary_category_id, year, file_url, file_extension) 
-                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
-            """, (doc_id, doc_data.get('title'), doc_data.get('content'), user_id, 
-                  doc_data.get('table_type'), source_id, 0, doc_data.get('primary_category_id'), 
-                  doc_data.get('secondary_category_id'), doc_data.get('year'), 
-                  doc_data.get('file_url'), doc_data.get('file_extension')))
+                UPDATE t_document_main 
+                SET primary_category_id = %s, secondary_category_id = %s, year = %s,
+                    file_extension = %s, project_name = %s, project_section = %s
+                WHERE id = %s
+            """, (p_cat_id, s_cat_id, year, doc_data.get('file_extension'),
+                  doc_data.get('project_name'), doc_data.get('project_section'), doc_id))
             
             conn.commit()
             return True, "文档添加成功", doc_id
@@ -321,6 +403,70 @@ class SampleService:
         finally:
             cursor.close()
             conn.close()
+
+    async def edit_document(self, doc_data: Dict[str, Any]) -> Tuple[bool, str]:
+        """编辑文档(同步主表和子表)"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        
+        try:
+            doc_id = doc_data.get('id')
+            source_id = doc_data.get('source_id')
+            table_type = doc_data.get('table_type', 'basis')
+            table_name = TABLE_MAP.get(table_type, "t_basis_of_preparation")
+            
+            # 安全转换字段
+            p_cat_id = self._to_int(doc_data.get('primary_category_id'))
+            s_cat_id = self._to_int(doc_data.get('secondary_category_id'))
+            year = self._to_int(doc_data.get('year'))
+            release_date = self._to_date(doc_data.get('release_date'))
+            
+            # 1. 更新子表 (会触发数据库触发器更新 t_document_main 的基本字段)
+            if table_type == 'basis':
+                cursor.execute(
+                    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, file_url = %s, content = %s, updated_time = NOW() WHERE id = %s",
+                    (doc_data.get('title'), doc_data.get('standard_no'), doc_data.get('issuing_authority'), release_date, doc_data.get('document_type'), doc_data.get('professional_field'), doc_data.get('validity'), doc_data.get('file_url'), doc_data.get('content'), source_id)
+                )
+            elif table_type == 'work':
+                cursor.execute(
+                    f"UPDATE {table_name} SET plan_name = %s, project_name = %s, project_section = %s, compiling_unit = %s, compiling_date = %s, file_url = %s, content = %s, updated_time = NOW() WHERE id = %s",
+                    (doc_data.get('title'), doc_data.get('project_name'), doc_data.get('project_section'), doc_data.get('issuing_authority'), release_date, doc_data.get('file_url'), doc_data.get('content'), source_id)
+                )
+            elif table_type == 'job':
+                cursor.execute(
+                    f"UPDATE {table_name} SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s, file_url = %s, content = %s, updated_time = NOW() WHERE id = %s",
+                    (doc_data.get('title'), doc_data.get('issuing_authority'), doc_data.get('document_type'), release_date, doc_data.get('file_url'), doc_data.get('content'), source_id)
+                )
+
+            # 2. 更新主表中触发器未处理的字段(或者被触发器覆盖的非子表字段)
+            cursor.execute("""
+                UPDATE t_document_main 
+                SET primary_category_id = %s, secondary_category_id = %s, year = %s,
+                    file_extension = %s, project_name = %s, project_section = %s,
+                    updated_time = NOW()
+                WHERE id = %s
+            """, (p_cat_id, s_cat_id, year, 
+                  doc_data.get('file_extension'), doc_data.get('project_name'), doc_data.get('project_section'),
+                  doc_id))
+
+            
+            conn.commit()
+            return True, "文档更新成功"
+        except Exception as e:
+            logger.exception("编辑文档失败")
+            conn.rollback()
+            return False, str(e)
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def enter_document(self, doc_id: str, username: str) -> Tuple[bool, str]:
+        """文档入库(单个)"""
+        affected_rows, message = await self.batch_enter_knowledge_base([doc_id], username)
+        return affected_rows > 0, message
     
     async def get_basic_info_list(
         self,
@@ -341,7 +487,7 @@ class SampleService:
             # 根据类型选择表名和字段映射
             if type == 'basis':
                 table_name = "t_basis_of_preparation"
-                fields = "id, chinese_name as title, standard_number as standard_no, issuing_authority, release_date, document_type, professional_field, validity, created_by, created_time as created_at"
+                fields = "id, chinese_name as title, standard_number as standard_no, issuing_authority, release_date, document_type, professional_field, validity, created_by, created_time as created_at, file_url"
                 field_map = {
                     'title': 'chinese_name',
                     'standard_no': 'standard_number',
@@ -353,7 +499,7 @@ class SampleService:
                 }
             elif type == 'work':
                 table_name = "t_work_of_preparation"
-                fields = "id, plan_name as title, NULL as standard_no, compiling_unit as issuing_authority, compiling_date as release_date, NULL as document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at"
+                fields = "id, plan_name as title, NULL as standard_no, compiling_unit as issuing_authority, compiling_date as release_date, NULL as document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at, file_url"
                 field_map = {
                     'title': 'plan_name',
                     'issuing_authority': 'compiling_unit',
@@ -361,7 +507,7 @@ class SampleService:
                 }
             elif type == 'job':
                 table_name = "t_job_of_preparation"
-                fields = "id, file_name as title, NULL as standard_no, issuing_department as issuing_authority, publish_date as release_date, document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at"
+                fields = "id, file_name as title, NULL as standard_no, issuing_department as issuing_authority, publish_date as release_date, document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at, file_url"
                 field_map = {
                     'title': 'file_name',
                     'issuing_authority': 'issuing_department',
@@ -388,17 +534,20 @@ class SampleService:
             
             # 精细化检索
             for filter_key, filter_value in filters.items():
-                if filter_value and filter_key in field_map:
+                if not filter_value:
+                    continue
+                
+                # 处理日期范围
+                if filter_key == 'release_date_start':
+                    where_clauses.append("release_date >= %s")
+                    params.append(filter_value)
+                elif filter_key == 'release_date_end':
+                    where_clauses.append("release_date <= %s")
+                    params.append(filter_value)
+                elif filter_key in field_map:
                     db_field = field_map[filter_key]
-                    if filter_key.endswith('_start'):
-                        where_clauses.append(f"{db_field} >= %s")
-                        params.append(filter_value)
-                    elif filter_key.endswith('_end'):
-                        where_clauses.append(f"{db_field} <= %s")
-                        params.append(filter_value)
-                    else:
-                        where_clauses.append(f"{db_field} LIKE %s")
-                        params.append(f"%{filter_value}%")
+                    where_clauses.append(f"{db_field} LIKE %s")
+                    params.append(f"%{filter_value}%")
             
             where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
             
@@ -407,11 +556,11 @@ class SampleService:
             sql = f"SELECT {fields} FROM {table_name}{where_sql} ORDER BY created_at DESC LIMIT %s OFFSET %s"
             params.extend([size, offset])
             
+            logger.info(f"Executing Basic Info SQL: {sql} with params: {params}")
             cursor.execute(sql, tuple(params))
-            columns = [desc[0] for desc in cursor.description]
             items = []
             for row in cursor.fetchall():
-                item = dict(zip(columns, row))
+                item = row # DictCursor already returns dict
                 # 格式化日期
                 for key in ['release_date', 'created_at']:
                     if item.get(key) and hasattr(item[key], 'isoformat'):
@@ -421,9 +570,10 @@ class SampleService:
                 items.append(item)
             
             # 总数查询
-            count_sql = f"SELECT COUNT(*) FROM {table_name}{where_sql}"
+            count_sql = f"SELECT COUNT(*) as count FROM {table_name}{where_sql}"
             cursor.execute(count_sql, tuple(params[:-2]))
-            total = cursor.fetchone()[0]
+            res = cursor.fetchone()
+            total = res['count'] if res else 0
             
             return items, total
         except Exception as e:
@@ -433,110 +583,6 @@ class SampleService:
             cursor.close()
             conn.close()
     
-    async def edit_document(self, doc_data: Dict[str, Any]) -> Tuple[bool, str]:
-        """编辑文档(同步主表和子表)"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            doc_id = doc_data.get('id')
-            source_id = doc_data.get('source_id')
-            table_type = doc_data.get('table_type')
-            
-            if not doc_id or not source_id:
-                return False, "缺少ID参数"
-            
-            table_name = TABLE_MAP.get(table_type, "t_basis_of_preparation")
-            
-            # 更新子表内容
-            if table_type == 'basis':
-                cursor.execute(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
-                    WHERE id = %s
-                """, (doc_data.get('title'), doc_data.get('standard_no'), doc_data.get('issuing_authority'), 
-                      doc_data.get('release_date'), doc_data.get('document_type'), 
-                      doc_data.get('professional_field'), doc_data.get('validity'), source_id))
-            elif table_type == 'work':
-                cursor.execute(f"""
-                    UPDATE {table_name} 
-                    SET plan_name = %s, project_name = %s, project_section = %s, 
-                        compiling_unit = %s, compiling_date = %s
-                    WHERE id = %s
-                """, (doc_data.get('title'), doc_data.get('project_name'), doc_data.get('project_section'), 
-                      doc_data.get('issuing_authority'), doc_data.get('release_date'), source_id))
-            elif table_type == 'job':
-                cursor.execute(f"""
-                    UPDATE {table_name} 
-                    SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s
-                    WHERE id = %s
-                """, (doc_data.get('title'), doc_data.get('issuing_authority'), 
-                      doc_data.get('document_type'), doc_data.get('release_date'), source_id))
-            
-            # 更新主表内容
-            cursor.execute("""
-                UPDATE t_document_main 
-                SET title = %s, content = %s, updated_time = NOW(),
-                    primary_category_id = %s, secondary_category_id = %s, year = %s,
-                    file_url = %s, file_extension = %s
-                WHERE id = %s
-            """, (doc_data.get('title'), doc_data.get('content'), doc_data.get('primary_category_id'), 
-                  doc_data.get('secondary_category_id'), doc_data.get('year'), 
-                  doc_data.get('file_url'), doc_data.get('file_extension'), doc_id))
-            
-            conn.commit()
-            return True, "文档更新成功"
-        except Exception as e:
-            logger.exception("编辑文档失败")
-            conn.rollback()
-            return False, str(e)
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def enter_document(self, doc_id: str, username: str) -> Tuple[bool, str]:
-        """文档入库"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 更新主表
-            cursor.execute("UPDATE t_document_main SET whether_to_enter = 1, updated_time = NOW() WHERE id = %s", (doc_id,))
-            
-            # 尝试同步更新子表
-            try:
-                cursor.execute("SELECT source_type, source_id FROM t_document_main WHERE id = %s", (doc_id,))
-                res = cursor.fetchone()
-                if res and res[0] and res[1]:
-                    s_type, s_id = res
-                    sub_table = get_table_name(s_type)
-                    if sub_table:
-                        sub_sql = f"UPDATE {sub_table} SET whether_to_enter = 1, updated_at = 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()
-            return True, "入库成功"
-        except Exception as e:
-            logger.exception("入库失败")
-            conn.rollback()
-            return False, str(e)
-        finally:
-            cursor.close()
-            conn.close()
-            conn.close()
-
     # ==================== 文档转换 ====================
     
     async def get_document_source_type(self, doc_id: str) -> Optional[str]:
@@ -624,3 +670,112 @@ class SampleService:
         finally:
             cursor.close()
             conn.close()
+
+    # ==================== 基础信息管理 ====================
+
+    async def add_basic_info(self, type: str, data: Dict[str, Any], user_id: str) -> Tuple[bool, str]:
+        """新增基本信息"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            table_name = TABLE_MAP.get(type)
+            if not table_name:
+                return False, "无效的类型"
+            
+            source_id = str(uuid.uuid4())
+            doc_id = str(uuid.uuid4())
+            
+            # 1. 插入子表 (触发器会自动同步到主表)
+            if type == 'basis':
+                sql = f"INSERT INTO {table_name} (id, chinese_name, standard_number, issuing_authority, release_date, document_type, professional_field, validity, file_url, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())"
+                params = (source_id, 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('file_url'), user_id)
+            elif type == 'work':
+                sql = f"INSERT INTO {table_name} (id, plan_name, project_name, project_section, compiling_unit, compiling_date, file_url, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())"
+                params = (source_id, data.get('title'), data.get('project_name'), data.get('project_section'), data.get('issuing_authority'), self._to_date(data.get('release_date')), data.get('file_url'), user_id)
+            elif type == 'job':
+                sql = f"INSERT INTO {table_name} (id, file_name, issuing_department, document_type, publish_date, file_url, created_by, created_time, updated_time) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())"
+                params = (source_id, data.get('title'), data.get('issuing_authority'), data.get('document_type'), self._to_date(data.get('release_date')), data.get('file_url'), user_id)
+            else:
+                return False, "不支持的类型"
+            
+            cursor.execute(sql, params)
+            
+            # 2. 触发器已经同步到主表,不需要手动插入
+            # 但如果需要更新主表中触发器没处理的字段,可以在这里更新
+            
+            conn.commit()
+            return True, "新增成功"
+        except Exception as e:
+            logger.exception("新增基本信息失败")
+            conn.rollback()
+            return False, str(e)
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def edit_basic_info(self, type: str, info_id: str, data: Dict[str, Any]) -> Tuple[bool, str]:
+        """编辑基本信息"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            table_name = TABLE_MAP.get(type)
+            if not table_name:
+                return False, "无效的类型"
+            
+            # 1. 更新子表 (触发器会自动同步到主表)
+            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, file_url = %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('file_url'), 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, file_url = %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')), data.get('file_url'), info_id)
+            elif type == 'job':
+                sql = f"UPDATE {table_name} SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s, file_url = %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')), data.get('file_url'), info_id)
+            else:
+                return False, "不支持的类型"
+            
+            cursor.execute(sql, params)
+            
+            # 2. 触发器已经同步到主表的基本字段,如果主表有额外字段需要更新,可以在这里操作
+            
+            conn.commit()
+            return True, "编辑成功"
+        except Exception as e:
+            logger.exception("编辑基本信息失败")
+            conn.rollback()
+            return False, str(e)
+        finally:
+            cursor.close()
+            conn.close()
+
+    async def delete_basic_info(self, type: str, info_id: str) -> Tuple[bool, str]:
+        """删除基本信息"""
+        conn = get_db_connection()
+        if not conn:
+            return False, "数据库连接失败"
+        
+        cursor = conn.cursor()
+        try:
+            table_name = TABLE_MAP.get(type)
+            if not table_name:
+                return False, "无效的类型"
+            
+            # 1. 删除子表记录 (触发器会自动删除主表记录)
+            cursor.execute(f"DELETE FROM {table_name} WHERE id = %s", (info_id,))
+            
+            conn.commit()
+            return True, "删除成功"
+        except Exception as e:
+            logger.exception("删除基本信息失败")
+            conn.rollback()
+            return False, str(e)
+        finally:
+            cursor.close()
+            conn.close()

+ 162 - 28
src/views/sample_view.py

@@ -1,30 +1,20 @@
-# 在 views/sample_view.py 中
 import sys
 import os
 import logging
 import httpx
+import urllib.parse
+import asyncio
+from datetime import datetime, timezone
+from typing import Optional, List, Any, Union
 
-# 添加src目录到Python路径
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../..'))
-
-"""
-样本中心视图路由
-包含:知识库管理、文档管理、知识片段管理、标签管理、检索引擎、图片管理
-"""
-from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
-from fastapi.responses import HTMLResponse, Response
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, BackgroundTasks
+from fastapi.responses import HTMLResponse
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from typing import Optional, List
-from datetime import datetime, timezone
-from pydantic import BaseModel
-from typing import Optional, Any, Union
-from fastapi import FastAPI, HTTPException, Depends, Request, Response, BackgroundTasks
-from app.sample.schemas.sample_schemas import BatchEnterRequest , BatchDeleteRequest , ConvertRequest, DocumentAdd
+
+from app.sample.schemas.sample_schemas import BatchEnterRequest, BatchDeleteRequest, ConvertRequest, DocumentAdd, UploadUrlRequest
 from app.services.sample_service import SampleService
 from app.services.jwt_token import verify_token
 from app.schemas.base import ApiResponse
-from app.base.async_mysql_connection import get_db_connection
 
 # 获取logger
 logger = logging.getLogger(__name__)
@@ -39,10 +29,32 @@ security_optional = HTTPBearer(auto_error=False)
 
 # --- 文档管理中心 API ---
 
+@router.post("/documents/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()
+        
+        sample_service = SampleService()
+        success, message, data = await sample_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()
+
 @router.get("/documents/proxy-view")
 async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
     """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
     try:
+        # 确保 URL 已解码
+        url = urllib.parse.unquote(url)
+        
         # 优先从 Header 获取,如果没有则从参数获取
         actual_token = None
         if credentials:
@@ -100,6 +112,56 @@ async def proxy_view(url: str, token: Optional[str] = None, credentials: Optiona
         error_msg = f"<html><body><h3>无法加载内容</h3><p>错误原因: {str(e)}</p><p>URL: {url}</p></body></html>"
         return HTMLResponse(content=error_msg, status_code=500)
 
+@router.get("/documents/download")
+async def download_document(url: str, filename: Optional[str] = None, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
+    """代理下载云端文件,支持从 MinIO 等外部地址下载"""
+    try:
+        if not url:
+             return ApiResponse(code=400, message="缺少URL参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+        
+        # 确保 URL 已解码
+        url = urllib.parse.unquote(url)
+        
+        # 优先从 Header 获取,如果没有则从参数获取
+        actual_token = None
+        if credentials:
+            actual_token = credentials.credentials
+        elif token:
+            actual_token = token
+            
+        if not actual_token:
+            return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
+        payload = verify_token(actual_token)
+        if not payload or not payload.get("is_superuser"):
+            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+            
+        # 增加超时时间,支持大文件下载
+        async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
+            headers = {
+                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+            }
+            response = await client.get(url, headers=headers)
+            response.raise_for_status()
+            
+            content_type = response.headers.get("content-type", "application/octet-stream")
+            
+            # 设置下载文件名
+            headers = {
+                "Content-Disposition": f"attachment; filename*=UTF-8''{urllib.parse.quote(filename or 'downloaded_file')}" if filename else "attachment",
+                "Content-Type": content_type
+            }
+            
+            return Response(
+                content=response.content,
+                media_type=content_type,
+                headers=headers
+            )
+                
+    except Exception as e:
+        logger.exception(f"文件下载失败, url={url}")
+        return ApiResponse(code=500, message=f"下载失败: {str(e)} (URL: {url})", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
+
 @router.post("/documents/batch-enter")
 async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
     """批量将文档加入知识库"""
@@ -140,25 +202,24 @@ async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAutho
 
 async def simulate_conversion(doc_id: str):
     """模拟文档转换过程"""
-    import time
     sample_service = SampleService()
     
     try:
         # 1. 模拟开始 (10%)
         await sample_service.update_conversion_progress(doc_id, status=1, progress=10)
-        time.sleep(2)
+        await asyncio.sleep(2)
         
         # 2. 模拟进行中 (40%)
         await sample_service.update_conversion_progress(doc_id, status=1, progress=40)
-        time.sleep(3)
+        await asyncio.sleep(3)
         
         # 3. 模拟进行中 (75%)
         await sample_service.update_conversion_progress(doc_id, status=1, progress=75)
-        time.sleep(2)
+        await asyncio.sleep(2)
         
         # 4. 模拟完成 (100%)
-        title = await sample_service.get_document_title(doc_id)
-        converted_file_name = f"{title}_已转换.pdf"
+        # 模拟云端存储地址
+        converted_file_name = f"http://192.168.91.15:19000/aidata/sampledata/converted/simulated/{doc_id}.pdf"
         await sample_service.update_conversion_progress(doc_id, status=2, progress=100, 
                                                        converted_file_name=converted_file_name)
         
@@ -182,7 +243,8 @@ async def convert_document(req: ConvertRequest, background_tasks: BackgroundTask
             table_type = await sample_service.get_document_source_type(req.id)
 
         # 1. 优先尝试启动真实转换脚本
-        script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts", "miner_u.py"))
+        # 修正脚本路径:从 src/views 到根目录下的 scripts
+        script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "scripts", "miner_u.py"))
         if os.path.exists(script_path):
             import subprocess
             python_exe = sys.executable
@@ -225,7 +287,15 @@ async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentia
             'secondary_category_id': doc.secondary_category_id,
             'year': doc.year,
             'file_url': doc.file_url,
-            'file_extension': doc.file_extension
+            'file_extension': doc.file_extension,
+            'standard_no': doc.standard_no,
+            'issuing_authority': doc.issuing_authority,
+            'release_date': doc.release_date,
+            'document_type': doc.document_type,
+            'professional_field': doc.professional_field,
+            'validity': doc.validity,
+            'project_name': doc.project_name,
+            'project_section': doc.project_section
         }
         
         success, message, doc_id = await sample_service.add_document(doc_data, user_id)
@@ -266,6 +336,9 @@ async def get_document_list(
     whether_to_enter: Optional[int] = None,
     keyword: Optional[str] = None,
     table_type: Optional[str] = None,
+    primary_category_id: Optional[str] = None,
+    secondary_category_id: Optional[str] = None,
+    year: Optional[int] = None,
     page: int = 1, 
     size: int = 50,
     credentials: HTTPAuthorizationCredentials = Depends(security)
@@ -281,6 +354,9 @@ async def get_document_list(
             whether_to_enter=whether_to_enter,
             keyword=keyword,
             table_type=table_type,
+            primary_category_id=primary_category_id,
+            secondary_category_id=secondary_category_id,
+            year=year,
             page=page,
             size=size
         )
@@ -435,6 +511,64 @@ async def get_basic_info_list(
         logger.exception("查询基本信息失败")
         return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
+@router.post("/basic-info/add")
+async def add_basic_info(type: str, data: dict, 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("username", "admin")
+        sample_service = SampleService()
+        success, message = await sample_service.add_basic_info(type, data, 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.post("/basic-info/edit")
+async def edit_basic_info(type: str, id: str, data: dict, 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()
+        
+        sample_service = SampleService()
+        success, message = await sample_service.edit_basic_info(type, id, data)
+        
+        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("/basic-info/delete")
+async def delete_basic_info(type: str, 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()
+        
+        sample_service = SampleService()
+        success, message = await sample_service.delete_basic_info(type, 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.get("/documents/categories/primary")
 async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
     """获取所有一级分类(仅保留指定的分类)"""
@@ -482,8 +616,8 @@ async def search_documents(
 ):
     """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
     return await get_document_list(
-        primaryCategoryId=primaryCategoryId,
-        secondaryCategoryId=secondaryCategoryId,
+        primary_category_id=primaryCategoryId,
+        secondary_category_id=secondaryCategoryId,
         year=year,
         whether_to_enter=whether_to_enter,
         keyword=keyword,

+ 0 - 4573
test/full_server.py

@@ -1,4573 +0,0 @@
-#!/usr/bin/env python3
-"""
-完整的SSO服务器 - 包含认证API
-"""
-import sys
-import os
-import socket
-import json
-import uuid
-
-# 添加src目录到Python路径
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
-
-# 加载环境变量
-from dotenv import load_dotenv
-# 加载配置文件 - 从 src/app/config/.env
-env_path = os.path.join(os.path.dirname(__file__), "..", "src", "app", "config", ".env")
-load_dotenv(dotenv_path=env_path)
-
-from fastapi import FastAPI, HTTPException, Depends, Request, Response, BackgroundTasks
-from fastapi.responses import HTMLResponse, JSONResponse
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from pydantic import BaseModel
-from typing import Optional, Any, Union
-import hashlib
-import secrets
-import requests
-from urllib.parse import urlparse
-
-# MIME 类型到后缀的映射
-MIME_MAP = {
-    'application/pdf': '.pdf',
-    'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
-    'application/msword': '.doc',
-    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
-    'application/vnd.ms-excel': '.xls',
-    'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
-    'application/vnd.ms-powerpoint': '.ppt',
-    'text/markdown': '.md',
-    'text/plain': '.txt',
-    'text/html': '.html',
-    'image/jpeg': '.jpg',
-    'image/png': '.png',
-    'application/zip': '.zip',
-}
-
-def detect_file_extension(url: str) -> str:
-    """通过 URL 路径或 HEAD 请求检测文件后缀"""
-    if not url:
-        return ""
-    
-    # 1. 尝试从路径解析
-    path = urlparse(url).path
-    ext = os.path.splitext(path)[1].lower()
-    if ext and len(ext) <= 6:
-        return ext
-    
-    # 2. 尝试 HEAD 请求检测 Content-Type
-    try:
-        response = requests.head(url, allow_redirects=True, timeout=5)
-        content_type = response.headers.get('Content-Type', '').split(';')[0].strip()
-        return MIME_MAP.get(content_type, "")
-    except Exception as e:
-        print(f"检测文件后缀失败: {e}")
-        return ""
-# 修复JWT导入 - 确保使用正确的JWT库
-try:
-    # 首先尝试使用PyJWT
-    import jwt as pyjwt
-    # 测试是否有encode方法
-    test_token = pyjwt.encode({"test": "data"}, "secret", algorithm="HS256")
-    jwt = pyjwt
-    print("✅ 使用PyJWT库")
-except (ImportError, AttributeError, TypeError) as e:
-    print(f"PyJWT导入失败: {e}")
-    try:
-        # 尝试使用python-jose
-        from jose import jwt
-        print("✅ 使用python-jose库")
-    except ImportError as e:
-        print(f"python-jose导入失败: {e}")
-        # 最后尝试安装PyJWT
-        print("尝试安装PyJWT...")
-        import subprocess
-        import sys
-        try:
-            subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"])
-            import jwt
-            print("✅ PyJWT安装成功")
-        except Exception as install_error:
-            print(f"❌ PyJWT安装失败: {install_error}")
-            raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
-
-from datetime import datetime, timedelta, timezone, date
-import pymysql
-from urllib.parse import urlparse
-
-# 导入RBAC API - 移除循环导入
-# from rbac_api import get_user_menus, get_all_menus, get_all_roles, get_user_permissions
-
-# 数据模型
-class LoginRequest(BaseModel):
-    username: str
-    password: str
-    remember_me: bool = False
-
-class TokenResponse(BaseModel):
-    access_token: str
-    refresh_token: Optional[str] = None
-    token_type: str = "Bearer"
-    expires_in: int
-    scope: Optional[str] = None
-
-class UserInfo(BaseModel):
-    id: str
-    username: str
-    email: str
-    phone: Optional[str] = None
-    avatar_url: Optional[str] = None
-    is_active: bool
-    is_superuser: bool = False
-    roles: list = []
-    permissions: list = []
-
-class ApiResponse(BaseModel):
-    code: int
-    message: str
-    data: Optional[Any] = None
-    timestamp: str
-
-# 文档管理数据模型
-# --- 文档中心配置 ---
-TABLE_MAP = {
-    "basis": "t_basis_of_preparation", # 编制依据
-    "work": "t_work_of_preparation",   # 施工方案
-    "job": "t_job_of_preparation"      # 办公制度
-}
-
-def get_db_connection():
-    """获取数据库连接"""
-    try:
-        # 导入配置
-        from app.core.config import config_handler
-        database_url = config_handler.get("admin_app", "DATABASE_URL", "")
-        if not database_url:
-            return None
-            
-        parsed = urlparse(database_url)
-        config = {
-            'host': parsed.hostname or 'localhost',
-            'port': parsed.port or 3306,
-            'user': parsed.username or 'root',
-            'password': parsed.password or '',
-            'database': parsed.path[1:] if parsed.path else 'sso_db',
-            'charset': 'utf8mb4'
-        }
-        
-        return pymysql.connect(**config)
-    except Exception as e:
-        print(f"数据库连接失败: {e}")
-        return None
-
-# --- 初始化主表 ---
-def init_master_table():
-    """初始化主表结构,并确保所有必要字段都存在"""
-    conn = get_db_connection()
-    if not conn:
-        return
-    try:
-        cursor = conn.cursor()
-        # 1. 创建主表 (如果不存在)
-        cursor.execute("""
-            CREATE TABLE IF NOT EXISTS t_document_main (
-                id CHAR(36) PRIMARY KEY,
-                title VARCHAR(255) NOT NULL,
-                standard_no VARCHAR(100),
-                issuing_authority VARCHAR(255),
-                release_date DATE,
-                document_type VARCHAR(100),
-                professional_field VARCHAR(100),
-                validity VARCHAR(50) DEFAULT '现行',
-                created_by VARCHAR(100),
-                created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-                updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-                conversion_status TINYINT DEFAULT 0, -- 0:待转化, 1:转化中, 2:已完成, 3:失败
-                conversion_progress INT DEFAULT 0,
-                converted_file_name VARCHAR(255),
-                conversion_error TEXT,
-                whether_to_enter TINYINT DEFAULT 0, -- 0:未入库, 1:已入库
-                source_type ENUM('basis', 'work', 'job') NOT NULL,
-                source_id CHAR(36) NOT NULL,
-                file_url TEXT,
-                file_extension VARCHAR(10),
-                content TEXT,
-                primary_category_id INT,
-                secondary_category_id INT,
-                year INT
-            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-        """)
-        conn.commit()
-        print("✅ 主表 t_document_main 初始化成功")
-    except Exception as e:
-        print(f"❌ 初始化主表失败: {e}")
-    finally:
-        conn.close()
-
-# 执行初始化
-init_master_table()
-
-def get_table_name(table_type: Optional[str]) -> str:
-    """根据类型获取对应的数据库表名,默认为编制依据"""
-    return TABLE_MAP.get(table_type, "t_basis_of_preparation")
-
-class DocumentAdd(BaseModel):
-    title: str
-    content: str
-    primary_category_id: Optional[Any] = None
-    secondary_category_id: Optional[Any] = None
-    year: Optional[int] = None
-    table_type: Optional[str] = "basis"
-    # 新增编辑需要的字段
-    id: Optional[str] = None
-    source_id: Optional[str] = None
-    # 扩展字段 (子表特有属性)
-    standard_no: Optional[str] = None
-    issuing_authority: Optional[str] = None
-    release_date: Optional[str] = None
-    document_type: Optional[str] = None
-    professional_field: Optional[str] = None
-    validity: Optional[str] = None
-    project_name: Optional[str] = None
-    project_section: Optional[str] = None
-    # 文件相关字段
-    file_url: Optional[str] = None
-    file_extension: Optional[str] = None
-
-class DocumentListRequest(BaseModel):
-    page: int = 1
-    size: int = 50
-    keyword: Optional[str] = None
-    table_type: Optional[str] = None
-    whether_to_enter: Optional[int] = None
-
-# 导入配置
-from app.core.config import config_handler
-
-# 配置
-JWT_SECRET_KEY = config_handler.get("admin_app", "JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
-ACCESS_TOKEN_EXPIRE_MINUTES = config_handler.get_int("admin_app", "ACCESS_TOKEN_EXPIRE_MINUTES", 30)
-
-def check_port(port):
-    """检查端口是否可用"""
-    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-        try:
-            s.bind(('localhost', port))
-            return True
-        except OSError:
-            return False
-
-def find_available_port(start_port=8000, max_port=8010):
-    """查找可用端口"""
-    for port in range(start_port, max_port + 1):
-        if check_port(port):
-            return port
-    return None
-
-def verify_password_simple(password: str, stored_hash: str) -> bool:
-    """验证密码(简化版)"""
-    if stored_hash.startswith("sha256$"):
-        parts = stored_hash.split("$")
-        if len(parts) == 3:
-            salt = parts[1]
-            expected_hash = parts[2]
-            actual_hash = hashlib.sha256((password + salt).encode()).hexdigest()
-            return actual_hash == expected_hash
-    return False
-
-def create_access_token(data: dict) -> str:
-    """创建访问令牌"""
-    to_encode = data.copy()
-    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
-    to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
-    
-    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
-    return encoded_jwt
-
-def verify_token(token: str) -> Optional[dict]:
-    """验证令牌"""
-    try:
-        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
-        return payload
-    except jose.PyJWTError:
-        return None
-
-# 创建FastAPI应用
-app = FastAPI(
-    title="SSO认证中心",
-    version="1.0.0",
-    description="OAuth2单点登录认证中心",
-    docs_url="/docs",
-    redoc_url="/redoc"
-)
-
-# 配置CORS
-app.add_middleware(
-    CORSMiddleware,
-    allow_origins=["*"],
-    allow_credentials=True,
-    allow_methods=["*"],
-    allow_headers=["*"],
-)
-
-security = HTTPBearer()
-security_optional = HTTPBearer(auto_error=False)
-
-@app.get("/")
-async def root():
-    """根路径"""
-    return ApiResponse(
-        code=0,
-        message="欢迎使用SSO认证中心",
-        data={
-            "name": "SSO认证中心",
-            "version": "1.0.0",
-            "docs": "/docs"
-        },
-        timestamp=datetime.now(timezone.utc).isoformat()
-    ).model_dump()
-
-@app.get("/health")
-async def health_check():
-    """健康检查"""
-    return ApiResponse(
-        code=0,
-        message="服务正常运行",
-        data={
-            "status": "healthy",
-            "version": "1.0.0",
-            "timestamp": datetime.now(timezone.utc).isoformat()
-        },
-        timestamp=datetime.now(timezone.utc).isoformat()
-    ).model_dump()
-
-@app.post("/api/v1/auth/login")
-async def login(request: Request, login_data: LoginRequest):
-    """用户登录"""
-    print(f"🔐 收到登录请求: username={login_data.username}")
-    
-    conn = None
-    cursor = None
-    try:
-        # 获取数据库连接
-        print("📊 尝试连接数据库...")
-        conn = get_db_connection()
-        if not conn:
-            print("❌ 数据库连接失败")
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        print("✅ 数据库连接成功")
-        cursor = conn.cursor()
-        
-        # 查找用户
-        print(f"🔍 查找用户: {login_data.username}")
-        cursor.execute(
-            "SELECT id, username, email, password_hash, is_active, is_superuser FROM users WHERE username = %s OR email = %s",
-            (login_data.username, login_data.username)
-        )
-        
-        user_data = cursor.fetchone()
-        print(f"👤 用户查询结果: {user_data is not None}")
-        
-        if not user_data:
-            print("❌ 用户不存在")
-            return ApiResponse(
-                code=200001,
-                message="用户名或密码错误",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id, username, email, password_hash, is_active, is_superuser = user_data
-        print(f"✅ 找到用户: {username}, 激活状态: {is_active}")
-        
-        # 检查用户状态
-        if not is_active:
-            print("❌ 用户已被禁用")
-            return ApiResponse(
-                code=200002,
-                message="用户已被禁用",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 验证密码
-        print(f"🔑 验证密码,哈希格式: {password_hash[:20]}...")
-        password_valid = verify_password_simple(login_data.password, password_hash)
-        print(f"🔑 密码验证结果: {password_valid}")
-        
-        if not password_valid:
-            print("❌ 密码验证失败")
-            return ApiResponse(
-                code=200001,
-                message="用户名或密码错误",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 生成令牌
-        print("🎫 生成访问令牌...")
-        token_data = {
-            "sub": user_id,
-            "username": username,
-            "email": email,
-            "is_superuser": is_superuser
-        }
-        
-        access_token = create_access_token(token_data)
-        print(f"✅ 令牌生成成功: {access_token[:50]}...")
-        
-        token_response = TokenResponse(
-            access_token=access_token,
-            token_type="Bearer",
-            expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
-            scope="profile email"
-        )
-        
-        print("🎉 登录成功")
-        return ApiResponse(
-            code=0,
-            message="登录成功",
-            data=token_response.model_dump(),
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"❌ 登录错误详情: {type(e).__name__}: {str(e)}")
-        import traceback
-        print(f"❌ 错误堆栈: {traceback.format_exc()}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-    finally:
-        if cursor:
-            cursor.close()
-        if conn:
-            conn.close()
-
-@app.get("/api/v1/users/profile")
-async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """获取用户资料"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        if not user_id:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 查找用户详细信息
-        cursor.execute("""
-            SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
-                   u.last_login_at, u.created_at, u.updated_at,
-                   p.real_name, p.company, p.department, p.position
-            FROM users u
-            LEFT JOIN user_profiles p ON u.id = p.user_id
-            WHERE u.id = %s
-        """, (user_id,))
-        
-        user_data = cursor.fetchone()
-        
-        # 获取用户角色
-        cursor.execute("""
-            SELECT r.name
-            FROM user_roles ur
-            JOIN roles r ON ur.role_id = r.id
-            WHERE ur.user_id = %s AND ur.is_active = 1
-        """, (user_id,))
-        
-        roles = [row[0] for row in cursor.fetchall()]
-        
-        cursor.close()
-        conn.close()
-        
-        if not user_data:
-            return ApiResponse(
-                code=200001,
-                message="用户不存在",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 构建用户信息
-        user_info = {
-            "id": user_data[0],
-            "username": user_data[1],
-            "email": user_data[2],
-            "phone": user_data[3],
-            "avatar_url": user_data[4],
-            "is_active": user_data[5],
-            "is_superuser": user_data[6],
-            "last_login_at": user_data[7].isoformat() if user_data[7] else None,
-            "created_at": user_data[8].isoformat() if user_data[8] else None,
-            "updated_at": user_data[9].isoformat() if user_data[9] else None,
-            "real_name": user_data[10],
-            "company": user_data[11],
-            "department": user_data[12],
-            "position": user_data[13],
-            "roles": roles
-        }
-        
-        return ApiResponse(
-            code=0,
-            message="获取用户资料成功",
-            data=user_info,
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取用户资料错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.put("/api/v1/users/profile")
-async def update_user_profile(
-    request: Request,
-    profile_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """更新用户资料"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 更新用户基本信息
-        update_fields = []
-        update_values = []
-        
-        if 'email' in profile_data:
-            update_fields.append('email = %s')
-            update_values.append(profile_data['email'])
-        
-        if 'phone' in profile_data:
-            update_fields.append('phone = %s')
-            update_values.append(profile_data['phone'])
-        
-        if update_fields:
-            update_values.append(user_id)
-            cursor.execute(f"""
-                UPDATE users 
-                SET {', '.join(update_fields)}, updated_at = NOW()
-                WHERE id = %s
-            """, update_values)
-        
-        # 更新或插入用户详情
-        profile_fields = ['real_name', 'company', 'department', 'position']
-        profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
-        
-        if profile_updates:
-            # 检查是否已有记录
-            cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
-            profile_exists = cursor.fetchone()
-            
-            if profile_exists:
-                # 更新现有记录
-                update_fields = []
-                update_values = []
-                for field, value in profile_updates.items():
-                    update_fields.append(f'{field} = %s')
-                    update_values.append(value)
-                
-                update_values.append(user_id)
-                cursor.execute(f"""
-                    UPDATE user_profiles 
-                    SET {', '.join(update_fields)}, updated_at = NOW()
-                    WHERE user_id = %s
-                """, update_values)
-            else:
-                # 插入新记录
-                fields = ['user_id'] + list(profile_updates.keys())
-                values = [user_id] + list(profile_updates.values())
-                placeholders = ', '.join(['%s'] * len(values))
-                
-                cursor.execute(f"""
-                    INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
-                    VALUES ({placeholders}, NOW(), NOW())
-                """, values)
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="用户资料更新成功",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"更新用户资料错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.put("/api/v1/users/password")
-async def change_user_password(
-    request: Request,
-    password_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """修改用户密码"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        old_password = password_data.get('old_password')
-        new_password = password_data.get('new_password')
-        
-        if not old_password or not new_password:
-            return ApiResponse(
-                code=100001,
-                message="缺少必要参数",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 验证当前密码
-        cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
-        result = cursor.fetchone()
-        
-        if not result or not verify_password_simple(old_password, result[0]):
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="当前密码错误",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 生成新密码哈希
-        new_password_hash = hash_password_simple(new_password)
-        
-        # 更新密码
-        cursor.execute("""
-            UPDATE users 
-            SET password_hash = %s, updated_at = NOW()
-            WHERE id = %s
-        """, (new_password_hash, user_id))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="密码修改成功",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"修改密码错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-def hash_password_simple(password):
-    """简单的密码哈希"""
-    import hashlib
-    import secrets
-    
-    # 生成盐值
-    salt = secrets.token_hex(16)
-    
-    # 使用SHA256哈希
-    password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
-    
-    return f"sha256${salt}${password_hash}"
-
-@app.post("/api/v1/auth/logout")
-async def logout():
-    """用户登出"""
-    return ApiResponse(
-        code=0,
-        message="登出成功",
-        timestamp=datetime.now(timezone.utc).isoformat()
-    ).model_dump()
-
-# OAuth2 授权端点
-@app.get("/oauth/authorize")
-async def oauth_authorize(
-    response_type: str,
-    client_id: str,
-    redirect_uri: str,
-    scope: str = "profile",
-    state: str = None
-):
-    """OAuth2授权端点"""
-    try:
-        print(f"🔐 OAuth授权请求: client_id={client_id}, redirect_uri={redirect_uri}, scope={scope}")
-        
-        # 验证必要参数
-        if not response_type or not client_id or not redirect_uri:
-            error_url = f"{redirect_uri}?error=invalid_request&error_description=Missing required parameters"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "invalid_request", "redirect_url": error_url}
-        
-        # 验证response_type
-        if response_type != "code":
-            error_url = f"{redirect_uri}?error=unsupported_response_type&error_description=Only authorization code flow is supported"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "unsupported_response_type", "redirect_url": error_url}
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "server_error", "redirect_url": error_url}
-        
-        cursor = conn.cursor()
-        
-        # 验证client_id和redirect_uri
-        cursor.execute("""
-            SELECT id, name, redirect_uris, scope, is_active, is_trusted
-            FROM apps 
-            WHERE app_key = %s AND is_active = 1
-        """, (client_id,))
-        
-        app_data = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        if not app_data:
-            error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client_id"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "invalid_client", "redirect_url": error_url}
-        
-        app_id, app_name, redirect_uris_json, app_scope_json, is_active, is_trusted = app_data
-        
-        # 验证redirect_uri
-        redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
-        if redirect_uri not in redirect_uris:
-            error_url = f"{redirect_uri}?error=invalid_request&error_description=Invalid redirect_uri"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "invalid_request", "redirect_url": error_url}
-        
-        # 验证scope
-        app_scopes = json.loads(app_scope_json) if app_scope_json else []
-        requested_scopes = scope.split() if scope else []
-        invalid_scopes = [s for s in requested_scopes if s not in app_scopes]
-        if invalid_scopes:
-            error_url = f"{redirect_uri}?error=invalid_scope&error_description=Invalid scope: {' '.join(invalid_scopes)}"
-            if state:
-                error_url += f"&state={state}"
-            return {"error": "invalid_scope", "redirect_url": error_url}
-        
-        # TODO: 检查用户登录状态
-        # 这里应该检查用户是否已登录(通过session或cookie)
-        # 如果未登录,应该重定向到登录页面
-        
-        # 临时方案:返回登录页面,让用户先登录
-        # 生产环境应该使用session管理
-        
-        # 构建登录页面URL,登录后返回授权页面
-        login_page_url = f"/oauth/login?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
-        if state:
-            login_page_url += f"&state={state}"
-        
-        print(f"🔐 需要用户登录,重定向到登录页面: {login_page_url}")
-        
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=login_page_url, status_code=302)
-        
-        # 非受信任应用需要用户授权确认
-        # 这里返回授权页面HTML
-        authorization_html = f"""
-        <!DOCTYPE html>
-        <html>
-        <head>
-            <title>授权确认 - SSO认证中心</title>
-            <meta charset="utf-8">
-            <meta name="viewport" content="width=device-width, initial-scale=1">
-            <style>
-                body {{
-                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-                    margin: 0;
-                    padding: 20px;
-                    min-height: 100vh;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                }}
-                .auth-container {{
-                    background: white;
-                    border-radius: 10px;
-                    padding: 40px;
-                    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
-                    max-width: 400px;
-                    width: 100%;
-                }}
-                .auth-header {{
-                    text-align: center;
-                    margin-bottom: 30px;
-                }}
-                .auth-header h1 {{
-                    color: #333;
-                    margin-bottom: 10px;
-                }}
-                .app-info {{
-                    background: #f8f9fa;
-                    padding: 20px;
-                    border-radius: 8px;
-                    margin-bottom: 20px;
-                }}
-                .scope-list {{
-                    list-style: none;
-                    padding: 0;
-                    margin: 10px 0;
-                }}
-                .scope-list li {{
-                    padding: 5px 0;
-                    color: #666;
-                }}
-                .scope-list li:before {{
-                    content: "✓ ";
-                    color: #28a745;
-                    font-weight: bold;
-                }}
-                .auth-buttons {{
-                    display: flex;
-                    gap: 10px;
-                    margin-top: 20px;
-                }}
-                .btn {{
-                    flex: 1;
-                    padding: 12px 20px;
-                    border: none;
-                    border-radius: 6px;
-                    font-size: 16px;
-                    cursor: pointer;
-                    text-decoration: none;
-                    text-align: center;
-                    display: inline-block;
-                }}
-                .btn-primary {{
-                    background: #007bff;
-                    color: white;
-                }}
-                .btn-secondary {{
-                    background: #6c757d;
-                    color: white;
-                }}
-                .btn:hover {{
-                    opacity: 0.9;
-                }}
-            </style>
-        </head>
-        <body>
-            <div class="auth-container">
-                <div class="auth-header">
-                    <h1>授权确认</h1>
-                    <p>应用请求访问您的账户</p>
-                </div>
-                
-                <div class="app-info">
-                    <h3>{app_name}</h3>
-                    <p>该应用请求以下权限:</p>
-                    <ul class="scope-list">
-        """
-        
-        # 添加权限列表
-        scope_descriptions = {
-            "profile": "访问您的基本信息(用户名、头像等)",
-            "email": "访问您的邮箱地址",
-            "phone": "访问您的手机号码",
-            "roles": "访问您的角色和权限信息"
-        }
-        
-        for scope_item in requested_scopes:
-            description = scope_descriptions.get(scope_item, f"访问 {scope_item} 信息")
-            authorization_html += f"<li>{description}</li>"
-        
-        authorization_html += f"""
-                    </ul>
-                </div>
-                
-                <div class="auth-buttons">
-                    <a href="/oauth/authorize/deny?client_id={client_id}&redirect_uri={redirect_uri}&state={state or ''}" class="btn btn-secondary">拒绝</a>
-                    <a href="/oauth/authorize/approve?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}" class="btn btn-primary">授权</a>
-                </div>
-            </div>
-        </body>
-        </html>
-        """
-        
-        from fastapi.responses import HTMLResponse
-        return HTMLResponse(content=authorization_html)
-        
-    except Exception as e:
-        print(f"❌ OAuth授权错误: {e}")
-        error_url = f"{redirect_uri}?error=server_error&error_description=Internal server error"
-        if state:
-            error_url += f"&state={state}"
-        return {"error": "server_error", "redirect_url": error_url}
-
-@app.get("/oauth/login")
-async def oauth_login_page(
-    response_type: str,
-    client_id: str,
-    redirect_uri: str,
-    scope: str = "profile",
-    state: str = None
-):
-    """OAuth2登录页面"""
-    try:
-        print(f"🔐 显示OAuth登录页面: client_id={client_id}")
-        
-        # 获取应用信息
-        conn = get_db_connection()
-        if not conn:
-            return {"error": "server_error", "message": "数据库连接失败"}
-        
-        cursor = conn.cursor()
-        cursor.execute("SELECT name FROM apps WHERE app_key = %s", (client_id,))
-        app_data = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        app_name = app_data[0] if app_data else "未知应用"
-        
-        # 构建登录页面HTML
-        login_html = f"""
-        <!DOCTYPE html>
-        <html lang="zh-CN">
-        <head>
-            <meta charset="UTF-8">
-            <meta name="viewport" content="width=device-width, initial-scale=1.0">
-            <title>SSO登录 - {app_name}</title>
-            <style>
-                body {{
-                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-                    margin: 0;
-                    padding: 20px;
-                    min-height: 100vh;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                }}
-                .login-container {{
-                    background: white;
-                    border-radius: 15px;
-                    padding: 40px;
-                    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
-                    max-width: 400px;
-                    width: 100%;
-                }}
-                .login-header {{
-                    text-align: center;
-                    margin-bottom: 30px;
-                }}
-                .login-header h1 {{
-                    color: #333;
-                    margin-bottom: 10px;
-                }}
-                .app-info {{
-                    background: #f8f9fa;
-                    padding: 15px;
-                    border-radius: 8px;
-                    margin-bottom: 20px;
-                    text-align: center;
-                }}
-                .form-group {{
-                    margin-bottom: 20px;
-                }}
-                .form-group label {{
-                    display: block;
-                    margin-bottom: 5px;
-                    font-weight: 500;
-                    color: #333;
-                }}
-                .form-group input {{
-                    width: 100%;
-                    padding: 12px;
-                    border: 1px solid #ddd;
-                    border-radius: 6px;
-                    font-size: 16px;
-                    box-sizing: border-box;
-                }}
-                .form-group input:focus {{
-                    outline: none;
-                    border-color: #007bff;
-                    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
-                }}
-                .btn {{
-                    width: 100%;
-                    padding: 12px;
-                    background: #007bff;
-                    color: white;
-                    border: none;
-                    border-radius: 6px;
-                    font-size: 16px;
-                    font-weight: 500;
-                    cursor: pointer;
-                    transition: background 0.3s;
-                }}
-                .btn:hover {{
-                    background: #0056b3;
-                }}
-                .btn:disabled {{
-                    background: #6c757d;
-                    cursor: not-allowed;
-                }}
-                .error-message {{
-                    color: #dc3545;
-                    font-size: 14px;
-                    margin-top: 10px;
-                    text-align: center;
-                }}
-                .success-message {{
-                    color: #28a745;
-                    font-size: 14px;
-                    margin-top: 10px;
-                    text-align: center;
-                }}
-            </style>
-        </head>
-        <body>
-            <div class="login-container">
-                <div class="login-header">
-                    <h1>🔐 SSO登录</h1>
-                    <p>请登录以继续访问应用</p>
-                </div>
-                
-                <div class="app-info">
-                    <strong>{app_name}</strong> 请求访问您的账户
-                </div>
-                
-                <form id="loginForm" onsubmit="handleLogin(event)">
-                    <div class="form-group">
-                        <label for="username">用户名或邮箱</label>
-                        <input type="text" id="username" name="username" required>
-                    </div>
-                    
-                    <div class="form-group">
-                        <label for="password">密码</label>
-                        <input type="password" id="password" name="password" required>
-                    </div>
-                    
-                    <button type="submit" class="btn" id="loginBtn">登录</button>
-                    
-                    <div id="message"></div>
-                </form>
-                
-                <div style="margin-top: 20px; text-align: center; font-size: 14px; color: #666;">
-                    <p>测试账号: admin / Admin123456</p>
-                </div>
-            </div>
-            
-            <script>
-                async function handleLogin(event) {{
-                    event.preventDefault();
-                    
-                    const loginBtn = document.getElementById('loginBtn');
-                    const messageDiv = document.getElementById('message');
-                    
-                    loginBtn.disabled = true;
-                    loginBtn.textContent = '登录中...';
-                    messageDiv.innerHTML = '';
-                    
-                    const formData = new FormData(event.target);
-                    const loginData = {{
-                        username: formData.get('username'),
-                        password: formData.get('password'),
-                        remember_me: false
-                    }};
-                    
-                    try {{
-                        // 调用登录API
-                        const response = await fetch('/api/v1/auth/login', {{
-                            method: 'POST',
-                            headers: {{
-                                'Content-Type': 'application/json'
-                            }},
-                            body: JSON.stringify(loginData)
-                        }});
-                        
-                        const result = await response.json();
-                        
-                        if (result.code === 0) {{
-                            messageDiv.innerHTML = '<div class="success-message">登录成功,正在跳转...</div>';
-                            
-                            // 登录成功后,重定向到授权页面
-                            const authUrl = `/oauth/authorize/authenticated?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}&access_token=${{result.data.access_token}}`;
-                            
-                            setTimeout(() => {{
-                                window.location.href = authUrl;
-                            }}, 1000);
-                        }} else {{
-                            messageDiv.innerHTML = `<div class="error-message">${{result.message}}</div>`;
-                        }}
-                    }} catch (error) {{
-                        messageDiv.innerHTML = '<div class="error-message">登录失败,请重试</div>';
-                    }} finally {{
-                        loginBtn.disabled = false;
-                        loginBtn.textContent = '登录';
-                    }}
-                }}
-            </script>
-        </body>
-        </html>
-        """
-        
-        from fastapi.responses import HTMLResponse
-        return HTMLResponse(content=login_html)
-        
-    except Exception as e:
-        print(f"❌ OAuth登录页面错误: {e}")
-        return {"error": "server_error", "message": "服务器内部错误"}
-
-@app.get("/oauth/authorize/authenticated")
-async def oauth_authorize_authenticated(
-    response_type: str,
-    client_id: str,
-    redirect_uri: str,
-    access_token: str,
-    scope: str = "profile",
-    state: str = None
-):
-    """用户已登录后的授权处理"""
-    try:
-        print(f"🔐 用户已登录,处理授权: client_id={client_id}")
-        
-        # 验证访问令牌
-        payload = verify_token(access_token)
-        if not payload:
-            error_url = f"{redirect_uri}?error=invalid_token&error_description=Invalid access token"
-            if state:
-                error_url += f"&state={state}"
-            from fastapi.responses import RedirectResponse
-            return RedirectResponse(url=error_url, status_code=302)
-        
-        user_id = payload.get("sub")
-        username = payload.get("username", "")
-        
-        print(f"✅ 用户已验证: {username} ({user_id})")
-        
-        # 获取应用信息
-        conn = get_db_connection()
-        if not conn:
-            error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
-            if state:
-                error_url += f"&state={state}"
-            from fastapi.responses import RedirectResponse
-            return RedirectResponse(url=error_url, status_code=302)
-        
-        cursor = conn.cursor()
-        cursor.execute("SELECT name, is_trusted FROM apps WHERE app_key = %s", (client_id,))
-        app_data = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        if not app_data:
-            error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client"
-            if state:
-                error_url += f"&state={state}"
-            from fastapi.responses import RedirectResponse
-            return RedirectResponse(url=error_url, status_code=302)
-        
-        app_name, is_trusted = app_data
-        
-        # 如果是受信任应用,直接授权
-        if is_trusted:
-            # 生成授权码
-            auth_code = secrets.token_urlsafe(32)
-            
-            # TODO: 将授权码存储到数据库,关联用户和应用
-            # 这里简化处理,实际应该存储到数据库
-            
-            # 重定向回应用
-            callback_url = f"{redirect_uri}?code={auth_code}"
-            if state:
-                callback_url += f"&state={state}"
-            
-            print(f"✅ 受信任应用自动授权: {callback_url}")
-            
-            from fastapi.responses import RedirectResponse
-            return RedirectResponse(url=callback_url, status_code=302)
-        
-        # 非受信任应用,显示授权确认页面
-        # 这里可以返回授权确认页面的HTML
-        # 为简化,暂时也直接授权
-        auth_code = secrets.token_urlsafe(32)
-        callback_url = f"{redirect_uri}?code={auth_code}"
-        if state:
-            callback_url += f"&state={state}"
-        
-        print(f"✅ 用户授权完成: {callback_url}")
-        
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=callback_url, status_code=302)
-        
-    except Exception as e:
-        print(f"❌ 授权处理错误: {e}")
-        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
-        if state:
-            error_url += f"&state={state}"
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=error_url, status_code=302)
-
-async def oauth_approve(
-    client_id: str,
-    redirect_uri: str,
-    scope: str = "profile",
-    state: str = None
-):
-    """用户同意授权"""
-    try:
-        print(f"✅ 用户同意授权: client_id={client_id}")
-        
-        # 生成授权码
-        auth_code = secrets.token_urlsafe(32)
-        
-        # TODO: 将授权码存储到数据库,关联用户和应用
-        # 这里简化处理,实际应该:
-        # 1. 验证用户登录状态
-        # 2. 将授权码存储到数据库
-        # 3. 设置过期时间(通常10分钟)
-        
-        # 构建回调URL
-        callback_url = f"{redirect_uri}?code={auth_code}"
-        if state:
-            callback_url += f"&state={state}"
-        
-        print(f"🔄 重定向到: {callback_url}")
-        
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=callback_url, status_code=302)
-        
-    except Exception as e:
-        print(f"❌ 授权确认错误: {e}")
-        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
-        if state:
-            error_url += f"&state={state}"
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=error_url, status_code=302)
-
-@app.get("/oauth/authorize/deny")
-async def oauth_deny(
-    client_id: str,
-    redirect_uri: str,
-    state: str = None
-):
-    """用户拒绝授权"""
-    try:
-        print(f"❌ 用户拒绝授权: client_id={client_id}")
-        
-        # 构建错误回调URL
-        error_url = f"{redirect_uri}?error=access_denied&error_description=User denied authorization"
-        if state:
-            error_url += f"&state={state}"
-        
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=error_url, status_code=302)
-        
-    except Exception as e:
-        print(f"❌ 拒绝授权错误: {e}")
-        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
-        if state:
-            error_url += f"&state={state}"
-        from fastapi.responses import RedirectResponse
-        return RedirectResponse(url=error_url, status_code=302)
-
-@app.post("/oauth/token")
-async def oauth_token(request: Request):
-    """OAuth2令牌端点"""
-    try:
-        # 获取请求数据
-        form_data = await request.form()
-        
-        grant_type = form_data.get("grant_type")
-        code = form_data.get("code")
-        redirect_uri = form_data.get("redirect_uri")
-        client_id = form_data.get("client_id")
-        client_secret = form_data.get("client_secret")
-        
-        print(f"🎫 令牌请求: grant_type={grant_type}, client_id={client_id}")
-        
-        # 验证grant_type
-        if grant_type != "authorization_code":
-            return {
-                "error": "unsupported_grant_type",
-                "error_description": "Only authorization_code grant type is supported"
-            }
-        
-        # 验证必要参数
-        if not code or not redirect_uri or not client_id:
-            return {
-                "error": "invalid_request",
-                "error_description": "Missing required parameters"
-            }
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return {
-                "error": "server_error",
-                "error_description": "Database connection failed"
-            }
-        
-        cursor = conn.cursor()
-        
-        # 验证客户端
-        cursor.execute("""
-            SELECT id, name, app_secret, redirect_uris, scope, is_active
-            FROM apps 
-            WHERE app_key = %s AND is_active = 1
-        """, (client_id,))
-        
-        app_data = cursor.fetchone()
-        
-        if not app_data:
-            cursor.close()
-            conn.close()
-            return {
-                "error": "invalid_client",
-                "error_description": "Invalid client credentials"
-            }
-        
-        app_id, app_name, stored_secret, redirect_uris_json, scope_json, is_active = app_data
-        
-        # 验证客户端密钥(如果提供了)
-        if client_secret and client_secret != stored_secret:
-            cursor.close()
-            conn.close()
-            return {
-                "error": "invalid_client",
-                "error_description": "Invalid client credentials"
-            }
-        
-        # 验证redirect_uri
-        redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
-        if redirect_uri not in redirect_uris:
-            cursor.close()
-            conn.close()
-            return {
-                "error": "invalid_grant",
-                "error_description": "Invalid redirect_uri"
-            }
-        
-        # TODO: 验证授权码
-        # 这里简化处理,实际应该:
-        # 1. 从数据库查找授权码
-        # 2. 验证授权码是否有效且未过期
-        # 3. 验证授权码是否已被使用
-        # 4. 获取关联的用户ID
-        
-        # 模拟用户ID(实际应该从授权码记录中获取)
-        user_id = "ed6a79d3-0083-4d81-8b48-fc522f686f74"  # admin用户ID
-        
-        # 生成访问令牌
-        token_data = {
-            "sub": user_id,
-            "client_id": client_id,
-            "scope": "profile email"
-        }
-        
-        access_token = create_access_token(token_data)
-        refresh_token = secrets.token_urlsafe(32)
-        
-        # TODO: 将令牌存储到数据库
-        
-        cursor.close()
-        conn.close()
-        
-        # 返回令牌响应
-        token_response = {
-            "access_token": access_token,
-            "token_type": "Bearer",
-            "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
-            "refresh_token": refresh_token,
-            "scope": "profile email"
-        }
-        
-        print(f"✅ 令牌生成成功: {access_token[:50]}...")
-        
-        return token_response
-        
-    except Exception as e:
-        print(f"❌ 令牌生成错误: {e}")
-        return {
-            "error": "server_error",
-            "error_description": "Internal server error"
-        }
-
-@app.get("/oauth/userinfo")
-async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """OAuth2用户信息端点"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return {
-                "error": "invalid_token",
-                "error_description": "Invalid or expired access token"
-            }
-        
-        user_id = payload.get("sub")
-        client_id = payload.get("client_id")
-        scope = payload.get("scope", "").split()
-        
-        print(f"👤 用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return {
-                "error": "server_error",
-                "error_description": "Database connection failed"
-            }
-        
-        cursor = conn.cursor()
-        
-        # 查找用户信息
-        cursor.execute("""
-            SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active,
-                   p.real_name, p.company, p.department, p.position
-            FROM users u
-            LEFT JOIN user_profiles p ON u.id = p.user_id
-            WHERE u.id = %s AND u.is_active = 1
-        """, (user_id,))
-        
-        user_data = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        if not user_data:
-            return {
-                "error": "invalid_token",
-                "error_description": "User not found or inactive"
-            }
-        
-        # 构建用户信息响应(根据scope过滤)
-        user_info = {"sub": user_data[0]}
-        
-        if "profile" in scope:
-            user_info.update({
-                "username": user_data[1],
-                "avatar_url": user_data[4],
-                "real_name": user_data[6],
-                "company": user_data[7],
-                "department": user_data[8],
-                "position": user_data[9]
-            })
-        
-        if "email" in scope:
-            user_info["email"] = user_data[2]
-        
-        if "phone" in scope:
-            user_info["phone"] = user_data[3]
-        
-        print(f"✅ 返回用户信息: {user_info}")
-        
-        return user_info
-        
-    except Exception as e:
-        print(f"❌ 获取用户信息错误: {e}")
-        return {
-            "error": "server_error",
-            "error_description": "Internal server error"
-        }
-
-@app.get("/api/v1/apps")
-async def get_apps(
-    page: int = 1,
-    page_size: int = 20,
-    keyword: str = "",
-    status: str = "",
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """获取应用列表"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查用户角色,决定是否显示所有应用
-        cursor.execute("""
-            SELECT COUNT(*) FROM user_roles ur
-            JOIN roles r ON ur.role_id = r.id
-            WHERE ur.user_id = %s AND r.name IN ('super_admin', 'admin', 'app_manager') AND ur.is_active = 1
-        """, (user_id,))
-        
-        is_app_manager = cursor.fetchone()[0] > 0
-        
-        # 构建查询条件
-        where_conditions = []
-        params = []
-        
-        # 如果不是应用管理员,只显示自己创建的应用
-        if not is_app_manager:
-            where_conditions.append("created_by = %s")
-            params.append(user_id)
-        
-        if keyword:
-            where_conditions.append("(name LIKE %s OR description LIKE %s)")
-            params.extend([f"%{keyword}%", f"%{keyword}%"])
-        
-        if status == "active":
-            where_conditions.append("is_active = 1")
-        elif status == "inactive":
-            where_conditions.append("is_active = 0")
-        
-        where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-        
-        # 查询总数
-        cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
-        total = cursor.fetchone()[0]
-        
-        # 查询应用列表
-        offset = (page - 1) * page_size
-        cursor.execute(f"""
-            SELECT id, name, app_key, description, icon_url, redirect_uris, scope,
-                   is_active, is_trusted, access_token_expires, refresh_token_expires,
-                   created_at, updated_at
-            FROM apps 
-            WHERE {where_clause}
-            ORDER BY created_at DESC
-            LIMIT %s OFFSET %s
-        """, params + [page_size, offset])
-        
-        apps = []
-        for row in cursor.fetchall():
-            app = {
-                "id": row[0],
-                "name": row[1],
-                "app_key": row[2],
-                "description": row[3],
-                "icon_url": row[4],
-                "redirect_uris": json.loads(row[5]) if row[5] else [],
-                "scope": json.loads(row[6]) if row[6] else [],
-                "is_active": bool(row[7]),
-                "is_trusted": bool(row[8]),
-                "access_token_expires": row[9],
-                "refresh_token_expires": row[10],
-                "created_at": row[11].isoformat() if row[11] else None,
-                "updated_at": row[12].isoformat() if row[12] else None,
-                # 模拟统计数据
-                "today_requests": secrets.randbelow(1000),
-                "active_users": secrets.randbelow(100)
-            }
-            apps.append(app)
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取应用列表成功",
-            data={
-                "items": apps,
-                "total": total,
-                "page": page,
-                "page_size": page_size
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取应用列表错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.get("/api/v1/apps/{app_id}")
-async def get_app_detail(
-    app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """获取应用详情(包含密钥)"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 查询应用详情(包含密钥)
-        cursor.execute("""
-            SELECT id, name, app_key, app_secret, description, icon_url, 
-                   redirect_uris, scope, is_active, is_trusted,
-                   access_token_expires, refresh_token_expires,
-                   created_at, updated_at
-            FROM apps 
-            WHERE id = %s AND created_by = %s
-        """, (app_id, user_id))
-        
-        app_data = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        if not app_data:
-            return ApiResponse(
-                code=200001,
-                message="应用不存在或无权限",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        app_detail = {
-            "id": app_data[0],
-            "name": app_data[1],
-            "app_key": app_data[2],
-            "app_secret": app_data[3],
-            "description": app_data[4],
-            "icon_url": app_data[5],
-            "redirect_uris": json.loads(app_data[6]) if app_data[6] else [],
-            "scope": json.loads(app_data[7]) if app_data[7] else [],
-            "is_active": bool(app_data[8]),
-            "is_trusted": bool(app_data[9]),
-            "access_token_expires": app_data[10],
-            "refresh_token_expires": app_data[11],
-            "created_at": app_data[12].isoformat() if app_data[12] else None,
-            "updated_at": app_data[13].isoformat() if app_data[13] else None
-        }
-        
-        return ApiResponse(
-            code=0,
-            message="获取应用详情成功",
-            data=app_detail,
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取应用详情错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.post("/api/v1/apps")
-async def create_app(
-    request: Request,
-    app_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """创建应用"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 验证必要字段
-        if not app_data.get('name') or not app_data.get('redirect_uris'):
-            return ApiResponse(
-                code=100001,
-                message="缺少必要参数",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 生成应用ID和密钥
-        app_id = str(uuid.uuid4())
-        app_key = generate_random_string(32)
-        app_secret = generate_random_string(64)
-        
-        # 插入应用记录
-        cursor.execute("""
-            INSERT INTO apps (
-                id, name, app_key, app_secret, description, icon_url,
-                redirect_uris, scope, is_active, is_trusted,
-                access_token_expires, refresh_token_expires, created_by,
-                created_at, updated_at
-            ) VALUES (
-                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
-            )
-        """, (
-            app_id,
-            app_data['name'],
-            app_key,
-            app_secret,
-            app_data.get('description', ''),
-            app_data.get('icon_url', ''),
-            json.dumps(app_data['redirect_uris']),
-            json.dumps(app_data.get('scope', ['profile'])),
-            True,
-            app_data.get('is_trusted', False),
-            app_data.get('access_token_expires', 7200),
-            app_data.get('refresh_token_expires', 2592000),
-            user_id
-        ))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        # 返回创建的应用信息
-        app_info = {
-            "id": app_id,
-            "name": app_data['name'],
-            "app_key": app_key,
-            "app_secret": app_secret,
-            "description": app_data.get('description', ''),
-            "redirect_uris": app_data['redirect_uris'],
-            "scope": app_data.get('scope', ['profile']),
-            "is_active": True,
-            "is_trusted": app_data.get('is_trusted', False)
-        }
-        
-        return ApiResponse(
-            code=0,
-            message="应用创建成功",
-            data=app_info,
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"创建应用错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.put("/api/v1/apps/{app_id}/status")
-async def toggle_app_status(
-    app_id: str,
-    status_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """切换应用状态"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        is_active = status_data.get('is_active')
-        
-        if is_active is None:
-            return ApiResponse(
-                code=100001,
-                message="缺少必要参数",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查应用是否存在且属于当前用户
-        cursor.execute("""
-            SELECT id, name FROM apps 
-            WHERE id = %s AND created_by = %s
-        """, (app_id, user_id))
-        
-        app_data = cursor.fetchone()
-        
-        if not app_data:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="应用不存在或无权限",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 更新应用状态
-        cursor.execute("""
-            UPDATE apps 
-            SET is_active = %s, updated_at = NOW()
-            WHERE id = %s
-        """, (is_active, app_id))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        action = "启用" if is_active else "禁用"
-        print(f"✅ 应用状态已更新: {app_data[1]} -> {action}")
-        
-        return ApiResponse(
-            code=0,
-            message=f"应用已{action}",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"切换应用状态错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.put("/api/v1/apps/{app_id}")
-async def update_app(
-    app_id: str,
-    app_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """更新应用信息"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 验证必要参数
-        name = app_data.get('name', '').strip()
-        if not name:
-            return ApiResponse(
-                code=100001,
-                message="应用名称不能为空",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查应用是否存在且属于当前用户
-        cursor.execute("""
-            SELECT id, name FROM apps 
-            WHERE id = %s AND created_by = %s
-        """, (app_id, user_id))
-        
-        existing_app = cursor.fetchone()
-        
-        if not existing_app:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="应用不存在或无权限",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 检查应用名称是否已被其他应用使用
-        cursor.execute("""
-            SELECT id FROM apps 
-            WHERE name = %s AND created_by = %s AND id != %s
-        """, (name, user_id, app_id))
-        
-        if cursor.fetchone():
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="应用名称已存在",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 准备更新数据
-        description = (app_data.get('description') or '').strip()
-        icon_url = (app_data.get('icon_url') or '').strip()
-        redirect_uris = app_data.get('redirect_uris', [])
-        scope = app_data.get('scope', ['profile', 'email'])
-        is_trusted = app_data.get('is_trusted', False)
-        access_token_expires = app_data.get('access_token_expires', 7200)
-        refresh_token_expires = app_data.get('refresh_token_expires', 2592000)
-        
-        # 验证回调URL
-        if not redirect_uris or not isinstance(redirect_uris, list):
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=100001,
-                message="至少需要一个回调URL",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 验证权限范围
-        if not scope or not isinstance(scope, list):
-            scope = ['profile', 'email']
-        
-        # 更新应用信息
-        cursor.execute("""
-            UPDATE apps 
-            SET name = %s, description = %s, icon_url = %s, 
-                redirect_uris = %s, scope = %s, is_trusted = %s,
-                access_token_expires = %s, refresh_token_expires = %s,
-                updated_at = NOW()
-            WHERE id = %s
-        """, (
-            name, description, icon_url,
-            json.dumps(redirect_uris), json.dumps(scope), is_trusted,
-            access_token_expires, refresh_token_expires, app_id
-        ))
-        
-        conn.commit()
-        
-        # 获取更新后的应用信息
-        cursor.execute("""
-            SELECT id, name, app_key, description, icon_url, 
-                   redirect_uris, scope, is_active, is_trusted,
-                   access_token_expires, refresh_token_expires,
-                   created_at, updated_at
-            FROM apps 
-            WHERE id = %s
-        """, (app_id,))
-        
-        app_info = cursor.fetchone()
-        cursor.close()
-        conn.close()
-        
-        if app_info:
-            app_result = {
-                "id": app_info[0],
-                "name": app_info[1],
-                "app_key": app_info[2],
-                "description": app_info[3],
-                "icon_url": app_info[4],
-                "redirect_uris": json.loads(app_info[5]) if app_info[5] else [],
-                "scope": json.loads(app_info[6]) if app_info[6] else [],
-                "is_active": bool(app_info[7]),
-                "is_trusted": bool(app_info[8]),
-                "access_token_expires": app_info[9],
-                "refresh_token_expires": app_info[10],
-                "created_at": app_info[11].isoformat() if app_info[11] else None,
-                "updated_at": app_info[12].isoformat() if app_info[12] else None
-            }
-            
-            print(f"✅ 应用已更新: {name}")
-            
-            return ApiResponse(
-                code=0,
-                message="应用更新成功",
-                data=app_result,
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        else:
-            return ApiResponse(
-                code=500001,
-                message="获取更新后的应用信息失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-    except Exception as e:
-        print(f"更新应用错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.delete("/api/v1/apps/{app_id}")
-async def delete_app(
-    app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """删除应用"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查应用是否存在且属于当前用户
-        cursor.execute("""
-            SELECT id, name FROM apps 
-            WHERE id = %s AND created_by = %s
-        """, (app_id, user_id))
-        
-        app_data = cursor.fetchone()
-        
-        if not app_data:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="应用不存在或无权限",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 删除应用(级联删除相关数据)
-        cursor.execute("DELETE FROM apps WHERE id = %s", (app_id,))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        print(f"✅ 应用已删除: {app_data[1]}")
-        
-        return ApiResponse(
-            code=0,
-            message="应用已删除",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"删除应用错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.post("/api/v1/apps/{app_id}/reset-secret")
-async def reset_app_secret(
-    app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """重置应用密钥"""
-    try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(
-                code=200002,
-                message="无效的访问令牌",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        user_id = payload.get("sub")
-        
-        # 获取数据库连接
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500001,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查应用是否存在且属于当前用户
-        cursor.execute("""
-            SELECT id, name FROM apps 
-            WHERE id = %s AND created_by = %s
-        """, (app_id, user_id))
-        
-        app_data = cursor.fetchone()
-        
-        if not app_data:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=200001,
-                message="应用不存在或无权限",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 生成新的应用密钥
-        new_secret = generate_random_string(64)
-        
-        # 更新应用密钥
-        cursor.execute("""
-            UPDATE apps 
-            SET app_secret = %s, updated_at = NOW()
-            WHERE id = %s
-        """, (new_secret, app_id))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        print(f"✅ 应用密钥已重置: {app_data[1]}")
-        
-        return ApiResponse(
-            code=0,
-            message="应用密钥已重置",
-            data={"app_secret": new_secret},
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"重置应用密钥错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-def generate_random_string(length=32):
-    """生成随机字符串"""
-    import secrets
-    import string
-    alphabet = string.ascii_letters + string.digits
-    return ''.join(secrets.choice(alphabet) for _ in range(length))
-    """获取验证码"""
-    try:
-        # 生成验证码
-        captcha_text, captcha_image = generate_captcha()
-        
-        # 这里应该将验证码文本存储到缓存中(Redis或内存)
-        # 为了简化,我们暂时返回固定的验证码
-        captcha_id = secrets.token_hex(16)
-        
-        return ApiResponse(
-            code=0,
-            message="获取验证码成功",
-            data={
-                "captcha_id": captcha_id,
-                "captcha_image": captcha_image,
-                "captcha_text": captcha_text  # 生产环境中不应该返回这个
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"生成验证码错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="生成验证码失败",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-def generate_captcha():
-    """生成验证码"""
-    try:
-        from PIL import Image, ImageDraw, ImageFont
-        import io
-        import base64
-        import random
-        import string
-        
-        # 生成随机验证码文本
-        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
-        
-        # 创建图片
-        width, height = 120, 40
-        image = Image.new('RGB', (width, height), color='white')
-        draw = ImageDraw.Draw(image)
-        
-        # 尝试使用系统字体,如果失败则使用默认字体
-        try:
-            # Windows系统字体
-            font = ImageFont.truetype("arial.ttf", 20)
-        except:
-            try:
-                # 备用字体
-                font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20)
-            except:
-                # 使用默认字体
-                font = ImageFont.load_default()
-        
-        # 绘制验证码文本
-        text_width = draw.textlength(captcha_text, font=font)
-        text_height = 20
-        x = (width - text_width) // 2
-        y = (height - text_height) // 2
-        
-        # 添加一些随机颜色
-        colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
-        text_color = random.choice(colors)
-        
-        draw.text((x, y), captcha_text, fill=text_color, font=font)
-        
-        # 添加一些干扰线
-        for _ in range(3):
-            x1 = random.randint(0, width)
-            y1 = random.randint(0, height)
-            x2 = random.randint(0, width)
-            y2 = random.randint(0, height)
-            draw.line([(x1, y1), (x2, y2)], fill=random.choice(colors), width=1)
-        
-        # 添加一些干扰点
-        for _ in range(20):
-            x = random.randint(0, width)
-            y = random.randint(0, height)
-            draw.point((x, y), fill=random.choice(colors))
-        
-        # 转换为base64
-        buffer = io.BytesIO()
-        image.save(buffer, format='PNG')
-        image_data = buffer.getvalue()
-        image_base64 = base64.b64encode(image_data).decode('utf-8')
-        
-        return captcha_text, f"data:image/png;base64,{image_base64}"
-        
-    except ImportError:
-        # 如果PIL不可用,返回简单的文本验证码
-        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
-        # 创建一个简单的SVG验证码
-        svg_captcha = f"""
-        <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
-            <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
-            <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
-        </svg>
-        """
-        svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
-        return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
-    except Exception as e:
-        print(f"生成验证码图片失败: {e}")
-        # 返回默认验证码
-        return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
-
-# RBAC权限管理API
-@app.get("/api/v1/user/menus")
-async def api_get_user_menus(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")
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查用户是否是超级管理员
-        cursor.execute("""
-            SELECT COUNT(*) FROM user_roles ur
-            JOIN roles r ON ur.role_id = r.id
-            WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
-        """, (user_id,))
-        
-        is_super_admin = cursor.fetchone()[0] > 0
-        
-        if is_super_admin:
-            # 超级管理员返回所有活跃菜单
-            cursor.execute("""
-                SELECT m.id, m.parent_id, m.name, m.title, m.path, 
-                       m.component, m.icon, m.sort_order, m.menu_type, 
-                       m.is_hidden, m.is_active
-                FROM menus m
-                WHERE m.is_active = 1
-                ORDER BY m.sort_order
-            """)
-        else:
-            # 普通用户根据角色权限获取菜单
-            cursor.execute("""
-                SELECT m.id, m.parent_id, m.name, m.title, m.path, 
-                       m.component, m.icon, m.sort_order, m.menu_type, 
-                       m.is_hidden, m.is_active
-                FROM menus m
-                JOIN role_menus rm ON m.id = rm.menu_id
-                JOIN user_roles ur ON rm.role_id = ur.role_id
-                WHERE ur.user_id = %s 
-                AND ur.is_active = 1
-                AND m.is_active = 1
-                GROUP BY m.id, m.parent_id, m.name, m.title, m.path, 
-                         m.component, m.icon, m.sort_order, m.menu_type, 
-                         m.is_hidden, m.is_active
-                ORDER BY m.sort_order
-            """, (user_id,))
-        
-        menus = []
-        for row in cursor.fetchall():
-            menu_id = str(row[0])
-            menu_name = str(row[2])
-            menu_title = str(row[3])
-            menu_path = str(row[4])
-            
-            # 只过滤掉明确不想要的“文档处理中心”
-            # 保留数据库中原本就有的“文档管理中心” (/admin/documents)
-            if "文档处理中心" in menu_title:
-                continue
-                
-            menu = {
-                "id": row[0],
-                "parent_id": row[1],
-                "name": row[2],
-                "title": row[3],
-                "path": row[4],
-                "component": row[5],
-                "icon": row[6],
-                "sort_order": row[7],
-                "menu_type": row[8],
-                "is_hidden": bool(row[9]),
-                "is_active": bool(row[10]),
-                "children": []
-            }
-            menus.append(menu)
-        
-        # 构建菜单树前,过滤掉 button 类型的项,侧边栏只显示 menu 类型
-        sidebar_menus = [m for m in menus if m.get("menu_type") == "menu"]
-        menu_tree = build_menu_tree(sidebar_menus)
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取用户菜单成功",
-            data=menu_tree,
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取用户菜单错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-def build_menu_tree(menus):
-    """构建菜单树结构"""
-    menu_map = {menu["id"]: menu for menu in menus}
-    tree = []
-    
-    for menu in menus:
-        if menu["parent_id"] is None:
-            tree.append(menu)
-        else:
-            parent = menu_map.get(menu["parent_id"])
-            if parent:
-                parent["children"].append(menu)
-    
-    return tree
-
-@app.get("/api/v1/admin/menus")
-async def api_get_all_menus(
-    page: int = 1,
-    page_size: int = 1000,  # 增大默认页面大小,确保返回所有菜单
-    keyword: Optional[str] = None,
-    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()
-        
-        # 简化权限检查 - 只检查是否为管理员
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(
-                code=403,
-                message="权限不足",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 构建查询条件
-        where_conditions = []
-        params = []
-        
-        if keyword:
-            where_conditions.append("(m.title LIKE %s OR m.name LIKE %s)")
-            params.extend([f"%{keyword}%", f"%{keyword}%"])
-        
-        where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-        
-        # 查询总数
-        cursor.execute(f"SELECT COUNT(*) FROM menus m WHERE {where_clause}", params)
-        total = cursor.fetchone()[0]
-        
-        # 查询菜单列表 - 修改排序逻辑以支持树形结构
-        cursor.execute(f"""
-            SELECT m.id, m.parent_id, m.name, m.title, m.path, m.component,
-                   m.icon, m.sort_order, m.menu_type, m.is_hidden, m.is_active,
-                   m.description, m.created_at, m.updated_at,
-                   pm.title as parent_title
-            FROM menus m
-            LEFT JOIN menus pm ON m.parent_id = pm.id
-            WHERE {where_clause}
-            ORDER BY 
-                CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END,
-                m.sort_order, 
-                CASE WHEN m.menu_type = 'menu' THEN 0 ELSE 1 END,
-                m.created_at
-            LIMIT %s OFFSET %s
-        """, params + [page_size, (page - 1) * page_size])
-        
-        menus = []
-        for row in cursor.fetchall():
-            menu = {
-                "id": row[0],
-                "parent_id": row[1],
-                "name": row[2],
-                "title": row[3],
-                "path": row[4],
-                "component": row[5],
-                "icon": row[6],
-                "sort_order": row[7],
-                "menu_type": row[8],
-                "is_hidden": bool(row[9]),
-                "is_active": bool(row[10]),
-                "description": row[11],
-                "created_at": row[12].isoformat() if row[12] else None,
-                "updated_at": row[13].isoformat() if row[13] else None,
-                "parent_title": row[14]
-            }
-            menus.append(menu)
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取菜单列表成功",
-            data={
-                "items": menus,
-                "total": total,
-                "page": page,
-                "page_size": page_size
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取菜单列表错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.get("/api/v1/admin/roles")
-async def api_get_all_roles(
-    page: int = 1,
-    page_size: int = 20,
-    keyword: Optional[str] = None,
-    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()
-        
-        # 简化权限检查 - 只检查是否为管理员
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(
-                code=403,
-                message="权限不足",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 构建查询条件
-        where_conditions = []
-        params = []
-        
-        if keyword:
-            where_conditions.append("(r.display_name LIKE %s OR r.name LIKE %s)")
-            params.extend([f"%{keyword}%", f"%{keyword}%"])
-        
-        where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-        
-        # 查询总数
-        cursor.execute(f"SELECT COUNT(*) FROM roles r WHERE {where_clause}", params)
-        total = cursor.fetchone()[0]
-        
-        # 查询角色列表
-        offset = (page - 1) * page_size
-        cursor.execute(f"""
-            SELECT r.id, r.name, r.display_name, r.description, r.is_active,
-                   r.is_system, r.created_at, r.updated_at,
-                   COUNT(ur.user_id) as user_count
-            FROM roles r
-            LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.is_active = 1
-            WHERE {where_clause}
-            GROUP BY r.id
-            ORDER BY r.is_system DESC, r.created_at
-            LIMIT %s OFFSET %s
-        """, params + [page_size, offset])
-        
-        roles = []
-        for row in cursor.fetchall():
-            role = {
-                "id": row[0],
-                "name": row[1],
-                "display_name": row[2],
-                "description": row[3],
-                "is_active": bool(row[4]),
-                "is_system": bool(row[5]),
-                "created_at": row[6].isoformat() if row[6] else None,
-                "updated_at": row[7].isoformat() if row[7] else None,
-                "user_count": row[8]
-            }
-            roles.append(role)
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取角色列表成功",
-            data={
-                "items": roles,
-                "total": total,
-                "page": page,
-                "page_size": page_size
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取角色列表错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.get("/api/v1/user/permissions")
-async def api_get_user_permissions(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")
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 获取用户权限
-        cursor.execute("""
-            SELECT DISTINCT p.name, p.resource, p.action
-            FROM permissions p
-            JOIN role_permissions rp ON p.id = rp.permission_id
-            JOIN user_roles ur ON rp.role_id = ur.role_id
-            WHERE ur.user_id = %s 
-            AND ur.is_active = 1
-            AND p.is_active = 1
-        """, (user_id,))
-        
-        permissions = []
-        for row in cursor.fetchall():
-            permissions.append({
-                "name": row[0],
-                "resource": row[1],
-                "action": row[2]
-            })
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取用户权限成功",
-            data=permissions,
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取用户权限错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-# 用户管理API
-@app.get("/api/v1/admin/users")
-async def get_users(
-    page: int = 1,
-    page_size: int = 20,
-    keyword: Optional[str] = None,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 构建查询条件
-        where_conditions = []
-        params = []
-        
-        if keyword:
-            where_conditions.append("(u.username LIKE %s OR u.email LIKE %s OR up.real_name LIKE %s)")
-            params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
-        
-        where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-        
-        # 查询总数
-        cursor.execute(f"SELECT COUNT(*) FROM users u LEFT JOIN user_profiles up ON u.id = up.user_id WHERE {where_clause}", params)
-        total = cursor.fetchone()[0]
-        
-        # 查询用户列表
-        offset = (page - 1) * page_size
-        cursor.execute(f"""
-            SELECT u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
-                   u.last_login_at, u.created_at, up.real_name, up.company, up.department,
-                   GROUP_CONCAT(r.display_name) as roles
-            FROM users u
-            LEFT JOIN user_profiles up ON u.id = up.user_id
-            LEFT JOIN user_roles ur ON u.id = ur.user_id AND ur.is_active = 1
-            LEFT JOIN roles r ON ur.role_id = r.id
-            WHERE {where_clause}
-            GROUP BY u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
-                     u.last_login_at, u.created_at, up.real_name, up.company, up.department
-            ORDER BY u.created_at DESC
-            LIMIT %s OFFSET %s
-        """, params + [page_size, offset])
-        
-        users = []
-        for row in cursor.fetchall():
-            users.append({
-                "id": row[0],
-                "username": row[1],
-                "email": row[2],
-                "phone": row[3],
-                "is_active": bool(row[4]),
-                "is_superuser": bool(row[5]),
-                "last_login_at": row[6].isoformat() if row[6] else None,
-                "created_at": row[7].isoformat() if row[7] else None,
-                "real_name": row[8],
-                "company": row[9],
-                "department": row[10],
-                "roles": row[11].split(',') if row[11] else []
-            })
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="获取用户列表成功",
-            data={"items": users, "total": total, "page": page, "page_size": page_size},
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取用户列表错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.post("/api/v1/admin/users")
-async def create_user(
-    user_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查用户名和邮箱是否已存在
-        cursor.execute("SELECT id FROM users WHERE username = %s OR email = %s", 
-                      (user_data['username'], user_data['email']))
-        if cursor.fetchone():
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="用户名或邮箱已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 生成用户ID
-        user_id = str(uuid.uuid4())
-        
-        # 创建密码哈希
-        password_hash = hash_password_simple(user_data['password'])
-        
-        # 插入用户
-        cursor.execute("""
-            INSERT INTO users (id, username, email, phone, password_hash, is_active, is_superuser, created_at, updated_at)
-            VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
-        """, (user_id, user_data['username'], user_data['email'], user_data.get('phone'), 
-              password_hash, user_data.get('is_active', True), user_data.get('is_superuser', False)))
-        
-        # 插入用户详情
-        if any(key in user_data for key in ['real_name', 'company', 'department']):
-            profile_id = str(uuid.uuid4())
-            cursor.execute("""
-                INSERT INTO user_profiles (id, user_id, real_name, company, department, created_at, updated_at)
-                VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
-            """, (profile_id, user_id, user_data.get('real_name'), user_data.get('company'), user_data.get('department')))
-        
-        # 分配角色
-        if 'role_ids' in user_data and user_data['role_ids']:
-            for role_id in user_data['role_ids']:
-                role_assignment_id = str(uuid.uuid4())
-                cursor.execute("""
-                    INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
-                    VALUES (%s, %s, %s, %s, NOW())
-                """, (role_assignment_id, user_id, role_id, payload.get("sub")))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="用户创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"创建用户错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.put("/api/v1/admin/users/{user_id}")
-async def update_user(
-    user_id: str,
-    user_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 更新用户基本信息
-        update_fields = []
-        update_values = []
-        
-        for field in ['email', 'phone', 'is_active', 'is_superuser']:
-            if field in user_data:
-                update_fields.append(f'{field} = %s')
-                update_values.append(user_data[field])
-        
-        if update_fields:
-            update_values.append(user_id)
-            cursor.execute(f"""
-                UPDATE users 
-                SET {', '.join(update_fields)}, updated_at = NOW()
-                WHERE id = %s
-            """, update_values)
-        
-        # 更新用户详情
-        profile_fields = ['real_name', 'company', 'department']
-        profile_updates = {k: v for k, v in user_data.items() if k in profile_fields}
-        
-        if profile_updates:
-            # 检查是否已有记录
-            cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
-            profile_exists = cursor.fetchone()
-            
-            if profile_exists:
-                update_fields = []
-                update_values = []
-                for field, value in profile_updates.items():
-                    update_fields.append(f'{field} = %s')
-                    update_values.append(value)
-                
-                update_values.append(user_id)
-                cursor.execute(f"""
-                    UPDATE user_profiles 
-                    SET {', '.join(update_fields)}, updated_at = NOW()
-                    WHERE user_id = %s
-                """, update_values)
-            else:
-                profile_id = str(uuid.uuid4())
-                fields = ['id', 'user_id'] + list(profile_updates.keys())
-                values = [profile_id, user_id] + list(profile_updates.values())
-                placeholders = ', '.join(['%s'] * len(values))
-                
-                cursor.execute(f"""
-                    INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
-                    VALUES ({placeholders}, NOW(), NOW())
-                """, values)
-        
-        # 更新用户角色
-        if 'role_ids' in user_data:
-            # 删除现有角色
-            cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
-            
-            # 添加新角色
-            for role_id in user_data['role_ids']:
-                assignment_id = str(uuid.uuid4())
-                cursor.execute("""
-                    INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
-                    VALUES (%s, %s, %s, %s, NOW())
-                """, (assignment_id, user_id, role_id, payload.get("sub")))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="用户更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"更新用户错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.delete("/api/v1/admin/users/{user_id}")
-async def delete_user(
-    user_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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 不能删除自己
-        if user_id == payload.get("sub"):
-            return ApiResponse(code=400, message="不能删除自己", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查是否为超级管理员
-        cursor.execute("""
-            SELECT COUNT(*) FROM user_roles ur
-            JOIN roles r ON ur.role_id = r.id
-            WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
-        """, (user_id,))
-        
-        if cursor.fetchone()[0] > 0:
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="不能删除超级管理员", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 删除相关数据
-        cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
-        cursor.execute("DELETE FROM user_profiles WHERE user_id = %s", (user_id,))
-        cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="用户删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"删除用户错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-# 角色管理API
-@app.post("/api/v1/admin/roles")
-async def create_role(
-    role_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查角色名是否已存在
-        cursor.execute("SELECT id FROM roles WHERE name = %s", (role_data['name'],))
-        if cursor.fetchone():
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="角色名已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 创建角色
-        role_id = str(uuid.uuid4())
-        cursor.execute("""
-            INSERT INTO roles (id, name, display_name, description, is_active, is_system, created_at, updated_at)
-            VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
-        """, (role_id, role_data['name'], role_data['display_name'], role_data.get('description'), 
-              role_data.get('is_active', True), False))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="角色创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"创建角色错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.put("/api/v1/admin/roles/{role_id}")
-async def update_role(
-    role_id: str,
-    role_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查是否为系统角色
-        cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
-        role = cursor.fetchone()
-        if not role:
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        if role[0]:  # is_system
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="不能修改系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 更新角色
-        update_fields = []
-        update_values = []
-        
-        for field in ['display_name', 'description', 'is_active']:
-            if field in role_data:
-                update_fields.append(f'{field} = %s')
-                update_values.append(role_data[field])
-        
-        if update_fields:
-            update_values.append(role_id)
-            cursor.execute(f"""
-                UPDATE roles 
-                SET {', '.join(update_fields)}, updated_at = NOW()
-                WHERE id = %s
-            """, update_values)
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="角色更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"更新角色错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.delete("/api/v1/admin/roles/{role_id}")
-async def delete_role(
-    role_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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查是否为系统角色
-        cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
-        role = cursor.fetchone()
-        if not role:
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        if role[0]:  # is_system
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="不能删除系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 检查是否有用户使用此角色
-        cursor.execute("SELECT COUNT(*) FROM user_roles WHERE role_id = %s", (role_id,))
-        if cursor.fetchone()[0] > 0:
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="该角色正在被使用,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 删除角色相关数据
-        cursor.execute("DELETE FROM role_permissions WHERE role_id = %s", (role_id,))
-        cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
-        cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="角色删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"删除角色错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-# 角色菜单权限管理API
-@app.get("/api/v1/admin/roles/{role_id}/menus")
-async def get_role_menus(
-    role_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()
-        
-        # 检查管理员权限
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(
-                code=403,
-                message="权限不足",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查角色是否存在
-        cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
-        role = cursor.fetchone()
-        
-        if not role:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=404,
-                message="角色不存在",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 检查是否为超级管理员角色
-        role_name = role[1]
-        is_super_admin_role = role_name == "super_admin"
-        
-        if is_super_admin_role:
-            # 超级管理员默认拥有所有菜单权限
-            cursor.execute("""
-                SELECT id, name, title, parent_id, menu_type
-                FROM menus
-                WHERE is_active = 1
-                ORDER BY sort_order
-            """)
-            menu_permissions = cursor.fetchall()
-        else:
-            # 普通角色查询已分配的菜单权限
-            cursor.execute("""
-                SELECT m.id, m.name, m.title, m.parent_id, m.menu_type
-                FROM role_menus rm
-                JOIN menus m ON rm.menu_id = m.id
-                WHERE rm.role_id = %s AND m.is_active = 1
-                ORDER BY m.sort_order
-            """, (role_id,))
-            menu_permissions = cursor.fetchall()
-        
-        cursor.close()
-        conn.close()
-        
-        # 构建返回数据
-        menu_ids = [menu[0] for menu in menu_permissions]
-        menu_details = []
-        
-        for menu in menu_permissions:
-            menu_details.append({
-                "id": menu[0],
-                "name": menu[1],
-                "title": menu[2],
-                "parent_id": menu[3],
-                "menu_type": menu[4]
-            })
-        
-        return ApiResponse(
-            code=0,
-            message="获取角色菜单权限成功",
-            data={
-                "role_id": role_id,
-                "role_name": role[1],
-                "menu_ids": menu_ids,
-                "menu_details": menu_details,
-                "total": len(menu_ids)
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"获取角色菜单权限错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-@app.put("/api/v1/admin/roles/{role_id}/menus")
-async def update_role_menus(
-    role_id: str,
-    request: Request,
-    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()
-        
-        # 检查管理员权限
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(
-                code=403,
-                message="权限不足",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 获取请求数据
-        body = await request.json()
-        menu_ids = body.get("menu_ids", [])
-        
-        if not isinstance(menu_ids, list):
-            return ApiResponse(
-                code=400,
-                message="菜单ID列表格式错误",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(
-                code=500,
-                message="数据库连接失败",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查角色是否存在
-        cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
-        role = cursor.fetchone()
-        
-        if not role:
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=404,
-                message="角色不存在",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 检查是否为超级管理员角色
-        role_name = role[1]
-        is_super_admin_role = role_name == "super_admin"
-        
-        if is_super_admin_role:
-            # 超级管理员角色不允许修改权限,始终拥有全部权限
-            cursor.close()
-            conn.close()
-            return ApiResponse(
-                code=400,
-                message="超级管理员角色拥有全部权限,无需修改",
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-        
-        # 验证菜单ID是否存在
-        if menu_ids:
-            placeholders = ','.join(['%s'] * len(menu_ids))
-            cursor.execute(f"""
-                SELECT id FROM menus 
-                WHERE id IN ({placeholders}) AND is_active = 1
-            """, menu_ids)
-            
-            valid_menu_ids = [row[0] for row in cursor.fetchall()]
-            invalid_menu_ids = set(menu_ids) - set(valid_menu_ids)
-            
-            if invalid_menu_ids:
-                cursor.close()
-                conn.close()
-                return ApiResponse(
-                    code=400,
-                    message=f"无效的菜单ID: {', '.join(invalid_menu_ids)}",
-                    timestamp=datetime.now(timezone.utc).isoformat()
-                ).model_dump()
-        
-        # 开始事务
-        cursor.execute("START TRANSACTION")
-        
-        try:
-            # 删除角色现有的菜单权限
-            cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
-            
-            # 添加新的菜单权限
-            if menu_ids:
-                values = [(role_id, menu_id) for menu_id in menu_ids]
-                cursor.executemany("""
-                    INSERT INTO role_menus (role_id, menu_id, created_at)
-                    VALUES (%s, %s, NOW())
-                """, values)
-            
-            # 提交事务
-            conn.commit()
-            
-            cursor.close()
-            conn.close()
-            
-            return ApiResponse(
-                code=0,
-                message="角色菜单权限更新成功",
-                data={
-                    "role_id": role_id,
-                    "role_name": role[1],
-                    "menu_ids": menu_ids,
-                    "updated_count": len(menu_ids)
-                },
-                timestamp=datetime.now(timezone.utc).isoformat()
-            ).model_dump()
-            
-        except Exception as e:
-            # 回滚事务
-            conn.rollback()
-            cursor.close()
-            conn.close()
-            raise e
-            
-    except Exception as e:
-        print(f"更新角色菜单权限错误: {e}")
-        return ApiResponse(
-            code=500,
-            message="服务器内部错误",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-# 菜单管理API
-@app.post("/api/v1/admin/menus")
-async def create_menu(
-    menu_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查菜单名是否已存在
-        cursor.execute("SELECT id FROM menus WHERE name = %s", (menu_data['name'],))
-        if cursor.fetchone():
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="菜单标识已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 创建菜单
-        menu_id = str(uuid.uuid4())
-        cursor.execute("""
-            INSERT INTO menus (id, parent_id, name, title, path, component, icon, 
-                             sort_order, menu_type, is_hidden, is_active, description, created_at, updated_at)
-            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
-        """, (
-            menu_id, menu_data.get('parent_id'), menu_data['name'], menu_data['title'],
-            menu_data.get('path'), menu_data.get('component'), menu_data.get('icon'),
-            menu_data.get('sort_order', 0), menu_data.get('menu_type', 'menu'),
-            menu_data.get('is_hidden', False), menu_data.get('is_active', True),
-            menu_data.get('description')
-        ))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="菜单创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"创建菜单错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.put("/api/v1/admin/menus/{menu_id}")
-async def update_menu(
-    menu_id: str,
-    menu_data: dict,
-    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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 更新菜单
-        update_fields = []
-        update_values = []
-        
-        for field in ['parent_id', 'title', 'path', 'component', 'icon', 'sort_order', 
-                     'menu_type', 'is_hidden', 'is_active', 'description']:
-            if field in menu_data:
-                update_fields.append(f'{field} = %s')
-                update_values.append(menu_data[field])
-        
-        if update_fields:
-            update_values.append(menu_id)
-            cursor.execute(f"""
-                UPDATE menus 
-                SET {', '.join(update_fields)}, updated_at = NOW()
-                WHERE id = %s
-            """, update_values)
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="菜单更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"更新菜单错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.delete("/api/v1/admin/menus/{menu_id}")
-async def delete_menu(
-    menu_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()
-        
-        is_superuser = payload.get("is_superuser", False)
-        if not is_superuser:
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 检查是否有子菜单
-        cursor.execute("SELECT COUNT(*) FROM menus WHERE parent_id = %s", (menu_id,))
-        if cursor.fetchone()[0] > 0:
-            cursor.close()
-            conn.close()
-            return ApiResponse(code=400, message="该菜单下有子菜单,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 删除菜单相关数据
-        cursor.execute("DELETE FROM role_menus WHERE menu_id = %s", (menu_id,))
-        cursor.execute("DELETE FROM menus WHERE id = %s", (menu_id,))
-        
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="菜单删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"删除菜单错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-# 获取所有角色(用于下拉选择)
-@app.get("/api/v1/roles/all")
-async def get_all_roles_simple(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()
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        cursor.execute("""
-            SELECT id, name, display_name, is_system, is_active
-            FROM roles 
-            WHERE is_active = 1
-            ORDER BY is_system DESC, display_name
-        """)
-        
-        roles = []
-        for row in cursor.fetchall():
-            roles.append({
-                "id": row[0],
-                "name": row[1],
-                "display_name": row[2],
-                "is_system": bool(row[3]),
-                "is_active": bool(row[4])
-            })
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(code=0, message="获取角色列表成功", data=roles, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-    except Exception as e:
-        print(f"获取角色列表错误: {e}")
-        return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-import httpx
-from fastapi.responses import HTMLResponse
-
-class BatchEnterRequest(BaseModel):
-    ids: list[Union[int, str]]
-    table_type: Optional[str] = None
-
-class BatchDeleteRequest(BaseModel):
-    ids: list[Union[int, str]]
-    table_type: Optional[str] = None
-
-class ConvertRequest(BaseModel):
-    id: Union[int, str]
-    table_type: Optional[str] = None
-
-# --- 文档管理中心 API ---
-
-@app.get("/api/v1/documents/proxy-view")
-async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
-    """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
-    try:
-        # 优先从 Header 获取,如果没有则从参数获取
-        actual_token = None
-        if credentials:
-            actual_token = credentials.credentials
-        elif token:
-            actual_token = token
-            
-        if not actual_token:
-            return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-        payload = verify_token(actual_token)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        # 增加超时时间,支持大文件下载
-        async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
-            headers = {
-                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
-            }
-            response = await client.get(url, headers=headers)
-            response.raise_for_status()
-            
-            content_type = response.headers.get("content-type", "").lower()
-            
-            # 如果是 PDF 或其他二进制文件
-            if "application/pdf" in content_type or any(ext in url.lower() for ext in [".pdf", ".png", ".jpg", ".jpeg", ".gif"]):
-                return Response(
-                    content=response.content,
-                    media_type=content_type,
-                    headers={"Content-Disposition": "inline"}
-                )
-            
-            # 默认处理为 HTML
-            try:
-                content = response.text
-                
-                # 简单的注入一些基础样式,确保内容在 iframe 中显示良好
-                base_style = """
-                <style>
-                    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px; line-height: 1.6; color: #333; }
-                    img { max-width: 100%; height: auto; }
-                </style>
-                """
-                if "</head>" in content:
-                    content = content.replace("</head>", f"{base_style}</head>")
-                else:
-                    content = f"{base_style}{content}"
-                    
-                return HTMLResponse(content=content)
-            except Exception:
-                # 如果文本解析失败,返回原始字节
-                return Response(content=response.content, media_type=content_type)
-                
-    except Exception as e:
-        error_msg = f"<html><body><h3>无法加载内容</h3><p>错误原因: {str(e)}</p><p>URL: {url}</p></body></html>"
-        return HTMLResponse(content=error_msg, status_code=500)
-
-@app.post("/api/v1/documents/batch-enter")
-async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """批量将文档加入知识库"""
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 1. 批量更新主表 t_document_main
-        # 只更新尚未入库的数据 (whether_to_enter = 0),同时更新修改时间和修改人(如果需要)
-        placeholders = ', '.join(['%s'] * len(req.ids))
-        username = payload.get("username", "admin")
-        
-        # 首先更新主表
-        sql_main = f"""
-            UPDATE t_document_main 
-            SET whether_to_enter = 1, updated_time = NOW() 
-            WHERE id IN ({placeholders}) AND whether_to_enter = 0
-        """
-        cursor.execute(sql_main, req.ids)
-        affected_rows = cursor.rowcount
-        
-        # 2. 尝试更新对应的子表以保持同步
-        try:
-            # 查询这些 ID 对应的 source_type 和 source_id
-            cursor.execute(f"SELECT id, source_type, source_id FROM t_document_main WHERE id IN ({placeholders})", req.ids)
-            docs = cursor.fetchall()
-            
-            for doc_row in docs:
-                d_id, s_type, s_id = doc_row
-                if s_type and s_id:
-                    sub_table = get_table_name(s_type)
-                    if sub_table:
-                        # 更新子表中的 whether_to_enter 字段(如果存在)
-                        # 注意:子表中的主键可能是 id 且值为 s_id
-                        sub_sql = f"UPDATE {sub_table} SET whether_to_enter = 1, updated_at = NOW(), updated_by = %s WHERE id = %s"
-                        try:
-                            cursor.execute(sub_sql, (username, s_id))
-                        except Exception as sub_e:
-                            print(f"更新子表 {sub_table} 失败 (可能字段不存在): {sub_e}")
-        except Exception as sync_e:
-            print(f"同步更新子表失败: {sync_e}")
-
-        conn.commit()
-        cursor.close()
-        conn.close()
-        
-        message = f"成功将 {affected_rows} 条数据加入知识库"
-        if affected_rows < len(req.ids):
-            message += f"(跳过了 {len(req.ids) - affected_rows} 条已入库数据或未找到数据)"
-            
-        return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-    except Exception as e:
-        print(f"批量操作失败: {e}")
-        return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.post("/api/v1/documents/batch-delete")
-async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """批量删除文档"""
-    conn = None
-    cursor = None
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        if not req.ids:
-            return ApiResponse(code=400, message="未指定要删除的文档 ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        placeholders = ', '.join(['%s'] * len(req.ids))
-        
-        # 1. 尝试同步删除子表中的数据
-        try:
-            # 查询这些 ID 对应的 source_type 和 source_id
-            cursor.execute(f"SELECT source_type, source_id FROM t_document_main WHERE id IN ({placeholders})", req.ids)
-            docs = cursor.fetchall()
-            
-            for doc_row in docs:
-                s_type, s_id = doc_row
-                if s_type and s_id:
-                    sub_table = get_table_name(s_type)
-                    if sub_table:
-                        # 删除子表数据
-                        sub_sql = f"DELETE FROM {sub_table} WHERE id = %s"
-                        try:
-                            cursor.execute(sub_sql, (s_id,))
-                        except Exception as sub_e:
-                            print(f"删除子表 {sub_table} 数据失败: {sub_e}")
-        except Exception as sync_e:
-            print(f"同步删除子表数据失败: {sync_e}")
-
-        # 2. 删除主表 t_document_main 中的数据
-        sql_main = f"DELETE FROM t_document_main WHERE id IN ({placeholders})"
-        cursor.execute(sql_main, req.ids)
-        affected_rows = cursor.rowcount
-        
-        conn.commit()
-        
-        return ApiResponse(
-            code=0, 
-            message=f"成功删除 {affected_rows} 条文档数据", 
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-    except Exception as e:
-        print(f"批量删除失败: {e}")
-        return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-    finally:
-        if cursor:
-            cursor.close()
-        if conn:
-            conn.close()
-
-async def simulate_conversion(doc_id: str):
-    """模拟文档转换过程"""
-    import time
-    conn = None
-    try:
-        conn = get_db_connection()
-        cursor = conn.cursor()
-        
-        # 1. 模拟开始 (10%)
-        cursor.execute("UPDATE t_document_main SET conversion_status = 1, conversion_progress = 10 WHERE id = %s", (doc_id,))
-        conn.commit()
-        time.sleep(2)
-        
-        # 2. 模拟进行中 (40%)
-        cursor.execute("UPDATE t_document_main SET conversion_progress = 40 WHERE id = %s", (doc_id,))
-        conn.commit()
-        time.sleep(3)
-        
-        # 3. 模拟进行中 (75%)
-        cursor.execute("UPDATE t_document_main SET conversion_progress = 75 WHERE id = %s", (doc_id,))
-        conn.commit()
-        time.sleep(2)
-        
-        # 4. 模拟完成 (100%)
-        cursor.execute("""
-            UPDATE t_document_main 
-            SET conversion_status = 2, conversion_progress = 100, 
-                converted_file_name = CONCAT(title, '_已转换.pdf') 
-            WHERE id = %s
-        """, (doc_id,))
-        conn.commit()
-        
-    except Exception as e:
-        print(f"模拟转换出错: {e}")
-        if conn:
-            cursor = conn.cursor()
-            cursor.execute("UPDATE t_document_main SET conversion_status = 3, conversion_error = %s WHERE id = %s", (str(e), doc_id))
-            conn.commit()
-    finally:
-        if conn:
-            conn.close()
-
-@app.post("/api/v1/documents/convert")
-async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """启动文档转换 (支持真实脚本与模拟逻辑)"""
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        table_type = req.table_type
-        # 如果没有提供 table_type,从主表查询
-        if not table_type:
-            try:
-                conn = get_db_connection()
-                if conn:
-                    cursor = conn.cursor()
-                    cursor.execute("SELECT source_type FROM t_document_main WHERE id = %s", (req.id,))
-                    res = cursor.fetchone()
-                    if res:
-                        table_type = res[0]
-                    cursor.close()
-                    conn.close()
-            except Exception as e:
-                print(f"从主表获取 source_type 失败: {e}")
-
-        # 1. 优先尝试启动真实转换脚本
-        script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts", "miner_u.py"))
-        if os.path.exists(script_path):
-            import subprocess
-            python_exe = sys.executable
-            # 传递 table_type 和 id 给脚本
-            subprocess.Popen([python_exe, script_path, str(table_type or "basis"), str(req.id)], 
-                             stdout=subprocess.DEVNULL, 
-                             stderr=subprocess.DEVNULL,
-                             creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
-            return ApiResponse(code=0, message="转换任务已在后台启动", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        # 2. 如果脚本不存在,则启动模拟转换逻辑
-        background_tasks.add_task(simulate_conversion, str(req.id))
-        
-        return ApiResponse(
-            code=0, 
-            message="转换任务已启动 (模拟模式)", 
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-    except Exception as e:
-        print(f"启动转换失败: {e}")
-        return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.post("/api/v1/documents/add")
-async def add_document(doc: DocumentAdd, 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("username", "admin")
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        cursor = conn.cursor()
-        doc_id = str(uuid.uuid4())
-        source_id = str(uuid.uuid4())
-        table_name = TABLE_MAP.get(doc.table_type, "t_basis_of_preparation")
-        
-        try:
-            # 1. 插入子表
-            if doc.table_type == 'basis':
-                cursor.execute(
-                    f"INSERT INTO {table_name} (id, chinese_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc.title, user_id)
-                )
-            elif doc.table_type == 'work':
-                cursor.execute(
-                    f"INSERT INTO {table_name} (id, plan_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc.title, user_id)
-                )
-            elif doc.table_type == 'job':
-                cursor.execute(
-                    f"INSERT INTO {table_name} (id, file_name, created_by) VALUES (%s, %s, %s)",
-                    (source_id, doc.title, user_id)
-                )
-                
-            # 2. 插入主表
-            cursor.execute("""
-                INSERT INTO t_document_main 
-                (id, title, content, created_by, source_type, source_id, whether_to_enter, primary_category_id, secondary_category_id, year, file_url, file_extension) 
-                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
-            """, (doc_id, doc.title, doc.content, user_id, doc.table_type, source_id, 0, doc.primary_category_id, doc.secondary_category_id, doc.year, doc.file_url, doc.file_extension))
-            
-            conn.commit()
-            return ApiResponse(code=0, message="文档添加成功", data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        except Exception as e:
-            conn.rollback()
-            raise e
-        finally:
-            cursor.close()
-            conn.close()
-    except Exception as e:
-        print(f"添加文档失败: {e}")
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/documents/detail/{doc_id}")
-async def get_document_detail(doc_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """获取文档详情 (关联查询子表)"""
-    print(f"🔍 正在获取文档详情: {doc_id}")
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        cursor = conn.cursor()
-        try:
-            # 1. 查询主表
-            cursor.execute("SELECT * FROM t_document_main WHERE id = %s", (doc_id,))
-            main_row = cursor.fetchone()
-            if not main_row:
-                print(f"❌ 文档不存在: {doc_id}")
-                return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-                
-            columns = [desc[0] for desc in cursor.description]
-            doc = dict(zip(columns, main_row))
-            print(f"✅ 找到主表数据: {doc.get('title')}")
-            
-            # 2. 查询子表
-            source_type = doc.get('source_type')
-            source_id = doc.get('source_id')
-            table_name = TABLE_MAP.get(source_type)
-            
-            if table_name and source_id:
-                cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (source_id,))
-                sub_row = cursor.fetchone()
-                if sub_row:
-                    sub_columns = [desc[0] for desc in cursor.description]
-                    sub_data = dict(zip(sub_columns, sub_row))
-                    
-                    # 将子表字段映射到通用字段名,方便前端处理
-                    if source_type == 'basis':
-                        doc['standard_no'] = sub_data.get('standard_number')
-                        doc['issuing_authority'] = sub_data.get('issuing_authority')
-                        doc['release_date'] = str(sub_data.get('release_date')) if sub_data.get('release_date') else None
-                        doc['document_type'] = sub_data.get('document_type')
-                        doc['professional_field'] = sub_data.get('professional_field')
-                        doc['validity'] = sub_data.get('validity')
-                    elif source_type == 'work':
-                        doc['project_name'] = sub_data.get('project_name')
-                        doc['project_section'] = sub_data.get('project_section')
-                        doc['issuing_authority'] = sub_data.get('compiling_unit')
-                        doc['release_date'] = str(sub_data.get('compiling_date')) if sub_data.get('compiling_date') else None
-                    elif source_type == 'job':
-                        doc['issuing_authority'] = sub_data.get('issuing_department')
-                        doc['document_type'] = sub_data.get('document_type')
-                        doc['release_date'] = str(sub_data.get('publish_date')) if sub_data.get('publish_date') else None
-            
-            # 格式化主表时间
-            if doc.get('created_time'):
-                doc['created_time'] = doc['created_time'].isoformat()
-            if doc.get('updated_time'):
-                doc['updated_time'] = doc['updated_time'].isoformat()
-            if doc.get('release_date') and not isinstance(doc['release_date'], str):
-                doc['release_date'] = doc['release_date'].isoformat()
-
-            return ApiResponse(code=0, message="获取详情成功", data=doc, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        finally:
-            cursor.close()
-            conn.close()
-    except Exception as e:
-        print(f"获取文档详情失败: {e}")
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/documents/list")
-async def get_document_list(
-    whether_to_enter: Optional[int] = None,
-    keyword: Optional[str] = None,
-    table_type: Optional[str] = None,
-    page: int = 1, 
-    size: int = 50,
-    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()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        cursor = conn.cursor()
-        where_clauses = []
-        params = []
-        
-        if table_type:
-            where_clauses.append("source_type = %s")
-            params.append(table_type)
-        if whether_to_enter is not None:
-            where_clauses.append("whether_to_enter = %s")
-            params.append(whether_to_enter)
-        if keyword:
-            where_clauses.append("(title LIKE %s OR content LIKE %s)")
-            params.extend([f"%{keyword}%", f"%{keyword}%"])
-            
-        where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
-        offset = (page - 1) * size
-        
-        sql = f"SELECT * FROM t_document_main {where_sql} ORDER BY created_time DESC LIMIT %s OFFSET %s"
-        params.extend([size, offset])
-        
-        cursor.execute(sql, tuple(params))
-        columns = [desc[0] for desc in cursor.description]
-        items = []
-        for row in cursor.fetchall():
-            item = dict(zip(columns, row))
-            # 格式化时间
-            for key in ['created_time', 'updated_time', 'release_date']:
-                if item.get(key) and hasattr(item[key], 'isoformat'):
-                    item[key] = item[key].isoformat()
-            items.append(item)
-            
-        # 总数
-        count_sql = f"SELECT COUNT(*) FROM t_document_main {where_sql}"
-        cursor.execute(count_sql, tuple(params[:-2]))
-        total = cursor.fetchone()[0]
-        
-        # 统计数据
-        cursor.execute("SELECT COUNT(*) FROM t_document_main")
-        all_total = cursor.fetchone()[0]
-        cursor.execute("SELECT COUNT(*) FROM t_document_main WHERE whether_to_enter = 1")
-        total_entered = cursor.fetchone()[0]
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0, 
-            message="查询成功", 
-            data={
-                "items": items, 
-                "total": total, 
-                "page": page, 
-                "size": size,
-                "all_total": all_total,
-                "total_entered": total_entered
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-    except Exception as e:
-        print(f"获取文档列表失败: {e}")
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.post("/api/v1/documents/edit")
-async def edit_document(doc: DocumentAdd, 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()
-            
-        if not doc.id or not doc.source_id:
-            return ApiResponse(code=400, message="缺少ID参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        cursor = conn.cursor()
-        table_name = TABLE_MAP.get(doc.table_type, "t_basis_of_preparation")
-        
-        try:
-            # 1. 更新子表内容
-            if doc.table_type == 'basis':
-                cursor.execute(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
-                    WHERE id = %s
-                """, (doc.title, doc.standard_no, doc.issuing_authority, doc.release_date, 
-                      doc.document_type, doc.professional_field, doc.validity, doc.source_id))
-            elif doc.table_type == 'work':
-                cursor.execute(f"""
-                    UPDATE {table_name} 
-                    SET plan_name = %s, project_name = %s, project_section = %s, 
-                        compiling_unit = %s, compiling_date = %s
-                    WHERE id = %s
-                """, (doc.title, doc.project_name, doc.project_section, doc.issuing_authority, 
-                      doc.release_date, doc.source_id))
-            elif doc.table_type == 'job':
-                cursor.execute(f"""
-                    UPDATE {table_name} 
-                    SET file_name = %s, issuing_department = %s, document_type = %s, publish_date = %s
-                    WHERE id = %s
-                """, (doc.title, doc.issuing_authority, doc.document_type, doc.release_date, doc.source_id))
-                
-            # 2. 更新主表内容
-            cursor.execute("""
-                UPDATE t_document_main 
-                SET title = %s, content = %s, updated_time = NOW(),
-                    primary_category_id = %s, secondary_category_id = %s, year = %s,
-                    file_url = %s, file_extension = %s
-                WHERE id = %s
-            """, (doc.title, doc.content, doc.primary_category_id, doc.secondary_category_id, doc.year, 
-                  doc.file_url, doc.file_extension, doc.id))
-            
-            conn.commit()
-            return ApiResponse(code=0, message="文档更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        except Exception as e:
-            conn.rollback()
-            raise e
-        finally:
-            cursor.close()
-            conn.close()
-    except Exception as e:
-        print(f"编辑文档失败: {e}")
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.post("/api/v1/documents/enter")
-async def enter_document(data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """文档入库"""
-    try:
-        doc_id = data.get("id")
-        if not doc_id:
-            return ApiResponse(code=400, message="缺少ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        payload = verify_token(credentials.credentials)
-        username = payload.get("username", "admin") if payload else "admin"
-        
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        cursor = conn.cursor()
-        
-        # 1. 更新主表
-        cursor.execute("UPDATE t_document_main SET whether_to_enter = 1, updated_time = NOW() WHERE id = %s", (doc_id,))
-        
-        # 2. 尝试同步更新子表
-        try:
-            cursor.execute("SELECT source_type, source_id FROM t_document_main WHERE id = %s", (doc_id,))
-            res = cursor.fetchone()
-            if res and res[0] and res[1]:
-                s_type, s_id = res
-                sub_table = get_table_name(s_type)
-                if sub_table:
-                    sub_sql = f"UPDATE {sub_table} SET whether_to_enter = 1, updated_at = NOW(), updated_by = %s WHERE id = %s"
-                    try:
-                        cursor.execute(sub_sql, (username, s_id))
-                    except Exception as sub_e:
-                        print(f"入库同步子表 {sub_table} 失败: {sub_e}")
-        except Exception as sync_e:
-            print(f"入库同步子表异常: {sync_e}")
-            
-        conn.commit()
-        cursor.close()
-        conn.close()
-        return ApiResponse(code=0, message="入库成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-    except Exception as e:
-        print(f"入库失败: {e}")
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/basic-info/list")
-async def get_basic_info_list(
-    type: str,
-    page: int = 1,
-    size: int = 50,
-    keyword: Optional[str] = None,
-    title: Optional[str] = None,
-    standard_no: Optional[str] = None,
-    document_type: Optional[str] = None,
-    professional_field: Optional[str] = None,
-    validity: Optional[str] = None,
-    issuing_authority: Optional[str] = None,
-    release_date_start: Optional[str] = None,
-    release_date_end: Optional[str] = None,
-    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()
-            
-        conn = get_db_connection()
-        if not conn:
-            return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        cursor = conn.cursor()
-        
-        # 根据类型选择表名和字段映射
-        if type == 'basis':
-            table_name = "t_basis_of_preparation"
-            fields = "id, chinese_name as title, standard_number as standard_no, issuing_authority, release_date, document_type, professional_field, validity, created_by, created_time as created_at"
-            # 字段名映射供过滤使用
-            field_map = {
-                'title': 'chinese_name',
-                'standard_no': 'standard_number',
-                'issuing_authority': 'issuing_authority',
-                'release_date': 'release_date',
-                'document_type': 'document_type',
-                'professional_field': 'professional_field',
-                'validity': 'validity'
-            }
-        elif type == 'work':
-            table_name = "t_work_of_preparation"
-            fields = "id, plan_name as title, NULL as standard_no, compiling_unit as issuing_authority, compiling_date as release_date, NULL as document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at"
-            field_map = {
-                'title': 'plan_name',
-                'issuing_authority': 'compiling_unit',
-                'release_date': 'compiling_date'
-            }
-        elif type == 'job':
-            table_name = "t_job_of_preparation"
-            fields = "id, file_name as title, NULL as standard_no, issuing_department as issuing_authority, publish_date as release_date, document_type, NULL as professional_field, NULL as validity, created_by, created_time as created_at"
-            field_map = {
-                'title': 'file_name',
-                'issuing_authority': 'issuing_department',
-                'release_date': 'publish_date',
-                'document_type': 'document_type'
-            }
-        else:
-            return ApiResponse(code=400, message="无效的类型", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        where_clauses = []
-        params = []
-        
-        # 1. 统一关键字搜索 (保持兼容)
-        if keyword:
-            if type == 'basis':
-                where_clauses.append("(chinese_name LIKE %s OR standard_number LIKE %s)")
-                params.extend([f"%{keyword}%", f"%{keyword}%"])
-            elif type == 'work':
-                where_clauses.append("plan_name LIKE %s")
-                params.append(f"%{keyword}%")
-            elif type == 'job':
-                where_clauses.append("file_name LIKE %s")
-                params.append(f"%{keyword}%")
-
-        # 2. 精细化检索
-        if title and 'title' in field_map:
-            where_clauses.append(f"{field_map['title']} LIKE %s")
-            params.append(f"%{title}%")
-        
-        if standard_no and 'standard_no' in field_map:
-            where_clauses.append(f"{field_map['standard_no']} LIKE %s")
-            params.append(f"%{standard_no}%")
-            
-        if document_type and 'document_type' in field_map:
-            where_clauses.append(f"{field_map['document_type']} = %s")
-            params.append(document_type)
-            
-        if professional_field and 'professional_field' in field_map:
-            where_clauses.append(f"{field_map['professional_field']} = %s")
-            params.append(professional_field)
-            
-        if validity and 'validity' in field_map:
-            where_clauses.append(f"{field_map['validity']} = %s")
-            params.append(validity)
-            
-        if issuing_authority and 'issuing_authority' in field_map:
-            where_clauses.append(f"{field_map['issuing_authority']} LIKE %s")
-            params.append(f"%{issuing_authority}%")
-            
-        if release_date_start and 'release_date' in field_map:
-            where_clauses.append(f"{field_map['release_date']} >= %s")
-            params.append(release_date_start)
-            
-        if release_date_end and 'release_date' in field_map:
-            where_clauses.append(f"{field_map['release_date']} <= %s")
-            params.append(release_date_end)
-                
-        where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
-        
-        # 分页查询
-        offset = (page - 1) * size
-        sql = f"SELECT {fields} FROM {table_name}{where_sql} ORDER BY created_at DESC LIMIT %s OFFSET %s"
-        params.extend([size, offset])
-        
-        cursor.execute(sql, tuple(params))
-        columns = [desc[0] for desc in cursor.description]
-        items = []
-        for row in cursor.fetchall():
-            item = dict(zip(columns, row))
-            # 格式化日期
-            for key in ['release_date', 'created_at']:
-                if item.get(key) and hasattr(item[key], 'isoformat'):
-                    item[key] = item[key].isoformat()
-                elif item.get(key):
-                    item[key] = str(item[key])
-            items.append(item)
-            
-        # 总数查询
-        count_sql = f"SELECT COUNT(*) FROM {table_name}{where_sql}"
-        cursor.execute(count_sql, tuple(params[:-2]))
-        total = cursor.fetchone()[0]
-        
-        cursor.close()
-        conn.close()
-        
-        return ApiResponse(
-            code=0,
-            message="查询成功",
-            data={"items": items, "total": total, "page": page, "size": size},
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"查询基本信息失败: {e}")
-        return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/documents/categories/primary")
-async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """获取所有一级分类(仅保留指定的分类)"""
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        # 仅保留用户要求的分类
-        default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
-        categories = [{"id": name, "name": name} for name in default_categories]
-        return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-    except Exception as e:
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/documents/categories/secondary")
-async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
-    """根据一级分类获取二级分类(仅保留指定的分类)"""
-    try:
-        payload = verify_token(credentials.credentials)
-        if not payload or not payload.get("is_superuser"):
-            return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-            
-        # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类
-        categories = []
-        if primaryId == "办公制度":
-            secondary_names = ["采购", "报销", "审批"]
-            categories = [{"id": name, "name": name} for name in secondary_names]
-        
-        return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-    except Exception as e:
-        return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-
-@app.get("/api/v1/documents/search")
-async def search_documents(
-    keyword: str, 
-    primaryCategoryId: Optional[str] = None,
-    secondaryCategoryId: Optional[str] = None,
-    year: Optional[int] = None,
-    whether_to_enter: Optional[int] = None,
-    table_type: Optional[str] = "basis",
-    page: int = 1, 
-    size: int = 50,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
-):
-    """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
-    return await get_document_list(
-        primaryCategoryId=primaryCategoryId,
-        secondaryCategoryId=secondaryCategoryId,
-        year=year,
-        whether_to_enter=whether_to_enter,
-        keyword=keyword,
-        table_type=table_type,
-        page=page,
-        size=size,
-        credentials=credentials
-    )
-
-if __name__ == "__main__":
-    import uvicorn
-    
-    # 查找可用端口
-    port = find_available_port()
-    
-    if port is None:
-        print("❌ 无法找到可用端口 (8000-8010)")
-        print("请手动停止占用这些端口的进程")
-        sys.exit(1)
-    
-    print("=" * 60)
-    print("🚀 SSO认证中心完整服务器")
-    print("=" * 60)
-    print(f"✅ 找到可用端口: {port}")
-    print(f"🌐 访问地址: http://localhost:{port}")
-    print(f"📚 API文档: http://localhost:{port}/docs")
-    print(f"❤️  健康检查: http://localhost:{port}/health")
-    print(f"🔐 登录API: http://localhost:{port}/api/v1/auth/login")
-    print("=" * 60)
-    print("📝 前端配置:")
-    print(f"   VITE_API_BASE_URL=http://localhost:{port}")
-    print("=" * 60)
-    print("👤 测试账号:")
-    print("   用户名: admin")
-    print("   密码: Admin123456")
-    print("=" * 60)
-    print("按 Ctrl+C 停止服务器")
-    print()
-    
-    try:
-        uvicorn.run(
-            app,
-            host="0.0.0.0",
-            port=port,
-            log_level="info"
-        )
-    except KeyboardInterrupt:
-        print("\n👋 服务器已停止")
-    except Exception as e:
-        print(f"❌ 启动失败: {e}")
-        sys.exit(1)
-
-@app.get("/api/v1/auth/captcha")
-async def get_captcha():
-    """获取验证码"""
-    try:
-        # 生成验证码
-        captcha_text, captcha_image = generate_captcha()
-        
-        # 这里应该将验证码文本存储到缓存中(Redis或内存)
-        # 为了简化,我们暂时返回固定的验证码
-        captcha_id = secrets.token_hex(16)
-        
-        return ApiResponse(
-            code=0,
-            message="获取验证码成功",
-            data={
-                "captcha_id": captcha_id,
-                "captcha_image": captcha_image,
-                "captcha_text": captcha_text  # 生产环境中不应该返回这个
-            },
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-        
-    except Exception as e:
-        print(f"生成验证码错误: {e}")
-        return ApiResponse(
-            code=500001,
-            message="生成验证码失败",
-            timestamp=datetime.now(timezone.utc).isoformat()
-        ).model_dump()
-
-def generate_captcha():
-    """生成验证码"""
-    try:
-        import random
-        import string
-        import base64
-        
-        # 生成随机验证码文本
-        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
-        
-        # 创建一个简单的SVG验证码
-        svg_captcha = f"""
-        <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
-            <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
-            <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
-        </svg>
-        """
-        svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
-        return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
-        
-    except Exception as e:
-        print(f"生成验证码失败: {e}")
-        # 返回默认验证码
-        return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="

+ 0 - 116
test/run_server.py

@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-"""
-LQAdminPlatform 服务启动脚本
-用于启动 FastAPI 应用服务器
-"""
-import sys
-import os
-import socket
-import logging
-
-# 添加项目根目录到Python路径
-current_dir = os.path.dirname(os.path.abspath(__file__))
-project_root = os.path.dirname(current_dir)
-sys.path.insert(0, project_root)
-
-# 导入配置
-from src.app.core.config import config_handler
-
-
-def check_port(host: str, port: int) -> bool:
-    """
-    检查端口是否可用
-    
-    Args:
-        host: 主机地址
-        port: 端口号
-        
-    Returns:
-        bool: 端口可用返回True,否则返回False
-    """
-    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-        try:
-            s.bind((host, port))
-            return True
-        except OSError:
-            return False
-
-
-def find_available_port(host: str, start_port: int = 8000, max_port: int = 8010) -> int:
-    """
-    查找可用端口
-    
-    Args:
-        host: 主机地址
-        start_port: 起始端口
-        max_port: 最大端口
-        
-    Returns:
-        int: 可用端口号,如果没有找到返回None
-    """
-    for port in range(start_port, max_port + 1):
-        if check_port(host, port):
-            return port
-    return None
-
-
-def main():
-    """主函数:启动服务器"""
-    import uvicorn
-    
-    # 配置日志
-    logging.basicConfig(
-        level=logging.INFO,
-        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-    )
-    logger = logging.getLogger(__name__)
-    
-    # 获取配置
-    host = config_handler.get("admin_app", "HOST", "0.0.0.0")
-    port = config_handler.get_int("admin_app", "PORT", 8000)
-    reload = config_handler.get_bool("admin_app", "RELOAD", True)
-    debug = config_handler.get_bool("admin_app", "DEBUG", True)
-    
-    # 检查端口是否可用
-    if not check_port(host, port):
-        logger.warning(f"⚠️  端口 {port} 已被占用,正在查找可用端口...")
-        available_port = find_available_port(host, port, port + 10)
-        if available_port:
-            port = available_port
-            logger.info(f"✅ 找到可用端口: {port}")
-        else:
-            logger.error(f"❌ 未找到可用端口 (尝试范围: {port}-{port+10})")
-            logger.error("请检查是否有其他服务占用了端口,或手动指定其他端口")
-            sys.exit(1)
-    
-    # 打印启动信息
-    logger.info("=" * 70)
-    logger.info(f"🚀 正在启动 {config_handler.get('admin_app', 'APP_NAME', '后台管理')} v{config_handler.get('admin_app', 'APP_VERSION', '1.0.0')}")
-    logger.info("=" * 70)
-    logger.info(f"📍 服务地址: http://{host}:{port}")
-    logger.info(f"📚 API文档: http://{host}:{port}/docs")
-    logger.info(f"📖 ReDoc文档: http://{host}:{port}/redoc")
-    logger.info(f"🔧 调试模式: {'开启' if debug else '关闭'}")
-    logger.info(f"🔄 热重载: {'开启' if reload else '关闭'}")
-    logger.info("=" * 70)
-    
-    # 启动服务器
-    try:
-        uvicorn.run(
-            "src.app.server.app:app",
-            host=host,
-            port=port,
-            reload=reload,
-            log_level=config_handler.get("admin_app", "LOG_LEVEL", "INFO").lower(),
-            access_log=True,
-            use_colors=True
-        )
-    except KeyboardInterrupt:
-        logger.info("\n👋 服务已停止")
-    except Exception as e:
-        logger.error(f"❌ 服务启动失败: {e}", exc_info=True)
-        sys.exit(1)
-
-
-if __name__ == "__main__":
-    main()

+ 0 - 809
test/system_service.py.backup

@@ -1,809 +0,0 @@
-"""
-系统管理服务层
-从 system_view.py 提取的SQL查询逻辑
-"""
-import logging
-import uuid
-import json
-import secrets
-from typing import Optional, List, Dict, Any, Tuple
-from datetime import datetime, timezone
-from app.base.async_mysql_connection import get_db_connection
-
-logger = logging.getLogger(__name__)
-
-
-def generate_random_string(length: int) -> str:
-    """生成随机字符串"""
-    return secrets.token_urlsafe(length)[:length]
-
-
-class SystemService:
-    """系统管理服务类 - 使用 SQL 查询方式"""
-    
-    def __init__(self):
-        """初始化服务"""
-        pass
-    
-    # ==================== 用户资料管理 ====================
-    
-    async def get_user_profile(self, user_id: str) -> Optional[Dict[str, Any]]:
-        """获取用户资料"""
-        conn = get_db_connection()
-        if not conn:
-            return None
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 查找用户详细信息
-            cursor.execute("""
-                SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
-                       u.last_login_at, u.created_at, u.updated_at,
-                       p.real_name, p.company, p.department, p.position
-                FROM users u
-                LEFT JOIN user_profiles p ON u.id = p.user_id
-                WHERE u.id = %s
-            """, (user_id,))
-            
-            user_data = cursor.fetchone()
-            
-            if not user_data:
-                return None
-            
-            # 获取用户角色
-            cursor.execute("""
-                SELECT r.name
-                FROM user_roles ur
-                JOIN roles r ON ur.role_id = r.id
-                WHERE ur.user_id = %s AND ur.is_active = 1
-            """, (user_id,))
-            
-            roles = [row['name'] for row in cursor.fetchall()]
-            
-            # 构建用户信息
-            user_info = {
-                "id": user_data["id"],
-                "username": user_data["username"],
-                "email": user_data["email"],
-                "phone": user_data["phone"],
-                "avatar_url": user_data["avatar_url"],
-                "is_active": user_data["is_active"],
-                "is_superuser": user_data["is_superuser"],
-                "last_login_at": user_data["last_login_at"].isoformat() if user_data["last_login_at"] else None,
-                "created_at": user_data["created_at"].isoformat() if user_data["created_at"] else None,
-                "updated_at": user_data["updated_at"].isoformat() if user_data["updated_at"] else None,
-                "real_name": user_data["real_name"],
-                "company": user_data["company"],
-                "department": user_data["department"],
-                "position": user_data["position"],
-                "roles": roles
-            }
-            
-            return user_info
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def update_user_profile(self, user_id: str, profile_data: Dict[str, Any]) -> bool:
-        """更新用户资料"""
-        conn = get_db_connection()
-        if not conn:
-            return False
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 更新用户基本信息
-            update_fields = []
-            update_values = []
-            
-            if 'email' in profile_data:
-                update_fields.append('email = %s')
-                update_values.append(profile_data['email'])
-            
-            if 'phone' in profile_data:
-                update_fields.append('phone = %s')
-                update_values.append(profile_data['phone'])
-            
-            if update_fields:
-                update_values.append(user_id)
-                cursor.execute(f"""
-                    UPDATE users 
-                    SET {', '.join(update_fields)}, updated_at = NOW()
-                    WHERE id = %s
-                """, update_values)
-            
-            # 更新或插入用户详情
-            profile_fields = ['real_name', 'company', 'department', 'position']
-            profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
-            
-            if profile_updates:
-                # 检查是否已有记录
-                cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
-                profile_exists = cursor.fetchone()
-                
-                if profile_exists:
-                    # 更新现有记录
-                    update_fields = []
-                    update_values = []
-                    for field, value in profile_updates.items():
-                        update_fields.append(f'{field} = %s')
-                        update_values.append(value)
-                    
-                    update_values.append(user_id)
-                    cursor.execute(f"""
-                        UPDATE user_profiles 
-                        SET {', '.join(update_fields)}, updated_at = NOW()
-                        WHERE user_id = %s
-                    """, update_values)
-                else:
-                    # 插入新记录
-                    fields = ['user_id'] + list(profile_updates.keys())
-                    values = [user_id] + list(profile_updates.values())
-                    placeholders = ', '.join(['%s'] * len(values))
-                    
-                    cursor.execute(f"""
-                        INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
-                        VALUES ({placeholders}, NOW(), NOW())
-                    """, values)
-            
-            conn.commit()
-            return True
-        except Exception as e:
-            logger.exception("更新用户资料错误")
-            conn.rollback()
-            return False
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def verify_and_update_password(self, user_id: str, old_password_hash: str, new_password_hash: str) -> Tuple[bool, str]:
-        """验证旧密码并更新为新密码"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 验证当前密码
-            cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
-            result = cursor.fetchone()
-            
-            if not result:
-                return False, "用户不存在"
-            
-            if result["password_hash"] != old_password_hash:
-                return False, "当前密码错误"
-            
-            # 更新密码
-            cursor.execute("""
-                UPDATE users 
-                SET password_hash = %s, updated_at = NOW()
-                WHERE id = %s
-            """, (new_password_hash, user_id))
-            
-            conn.commit()
-            return True, "密码修改成功"
-        except Exception as e:
-            logger.exception("修改密码错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-
-    
-    # ==================== 菜单管理 ====================
-    
-    async def get_user_menus(self, user_id: str) -> List[Dict[str, Any]]:
-        """获取用户菜单"""
-        conn = get_db_connection()
-        if not conn:
-            return []
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查用户是否是超级管理员
-            cursor.execute("""
-                SELECT COUNT(*) as count FROM user_roles ur
-                JOIN roles r ON ur.role_id = r.id
-                WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
-            """, (user_id,))
-            
-            result = cursor.fetchone()
-            is_super_admin = result['count'] > 0 if result else False
-            
-            if is_super_admin:
-                # 超级管理员返回所有活跃菜单
-                cursor.execute("""
-                    SELECT m.id, m.parent_id, m.name, m.title, m.path, 
-                           m.component, m.icon, m.sort_order, m.menu_type, 
-                           m.is_hidden, m.is_active
-                    FROM menus m
-                    WHERE m.is_active = 1
-                    ORDER BY m.sort_order
-                """)
-            else:
-                # 普通用户根据角色权限获取菜单
-                cursor.execute("""
-                    SELECT m.id, m.parent_id, m.name, m.title, m.path, 
-                           m.component, m.icon, m.sort_order, m.menu_type, 
-                           m.is_hidden, m.is_active
-                    FROM menus m
-                    JOIN role_menus rm ON m.id = rm.menu_id
-                    JOIN user_roles ur ON rm.role_id = ur.role_id
-                    WHERE ur.user_id = %s 
-                    AND ur.is_active = 1
-                    AND m.is_active = 1
-                    GROUP BY m.id, m.parent_id, m.name, m.title, m.path, 
-                             m.component, m.icon, m.sort_order, m.menu_type, 
-                             m.is_hidden, m.is_active
-                    ORDER BY m.sort_order
-                """, (user_id,))
-            
-            menus = []
-            for row in cursor.fetchall():
-                menu = {
-                    "id": row['id'],
-                    "parent_id": row['parent_id'],
-                    "name": row['name'],
-                    "title": row['title'],
-                    "path": row['path'],
-                    "component": row['component'],
-                    "icon": row['icon'],
-                    "sort_order": row['sort_order'],
-                    "menu_type": row['menu_type'],
-                    "is_hidden": bool(row['is_hidden']),
-                    "is_active": bool(row['is_active']),
-                    "children": []
-                }
-                menus.append(menu)
-            
-            return menus
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def get_all_menus(self, page: int, page_size: int, keyword: Optional[str] = None) -> Tuple[List[Dict[str, Any]], int]:
-        """获取所有菜单(管理员)"""
-        conn = get_db_connection()
-        if not conn:
-            return [], 0
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 构建查询条件
-            where_conditions = []
-            params = []
-            
-            if keyword:
-                where_conditions.append("(m.title LIKE %s OR m.name LIKE %s)")
-                params.extend([f"%{keyword}%", f"%{keyword}%"])
-            
-            where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-            
-            # 查询总数
-            cursor.execute(f"SELECT COUNT(*) as count FROM menus m WHERE {where_clause}", params)
-            total = cursor.fetchone()['count']
-            
-            # 查询菜单列表
-            cursor.execute(f"""
-                SELECT m.id, m.parent_id, m.name, m.title, m.path, m.component,
-                       m.icon, m.sort_order, m.menu_type, m.is_hidden, m.is_active,
-                       m.description, m.created_at, m.updated_at,
-                       pm.title as parent_title
-                FROM menus m
-                LEFT JOIN menus pm ON m.parent_id = pm.id
-                WHERE {where_clause}
-                ORDER BY 
-                    CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END,
-                    m.sort_order, 
-                    CASE WHEN m.menu_type = 'menu' THEN 0 ELSE 1 END,
-                    m.created_at
-                LIMIT %s OFFSET %s
-            """, params + [page_size, (page - 1) * page_size])
-            
-            menus = []
-            for row in cursor.fetchall():
-                menu = {
-                    "id": row['id'],
-                    "parent_id": row['parent_id'],
-                    "name": row['name'],
-                    "title": row['title'],
-                    "path": row['path'],
-                    "component": row['component'],
-                    "icon": row['icon'],
-                    "sort_order": row['sort_order'],
-                    "menu_type": row['menu_type'],
-                    "is_hidden": bool(row['is_hidden']),
-                    "is_active": bool(row['is_active']),
-                    "description": row['description'],
-                    "created_at": row['created_at'].isoformat() if row['created_at'] else None,
-                    "updated_at": row['updated_at'].isoformat() if row['updated_at'] else None,
-                    "parent_title": row['parent_title']
-                }
-                menus.append(menu)
-            
-            return menus, total
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def create_menu(self, menu_data: Dict[str, Any]) -> Tuple[bool, str]:
-        """创建菜单"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查菜单名是否已存在
-            cursor.execute("SELECT id FROM menus WHERE name = %s", (menu_data['name'],))
-            if cursor.fetchone():
-                return False, "菜单标识已存在"
-            
-            # 创建菜单
-            menu_id = str(uuid.uuid4())
-            cursor.execute("""
-                INSERT INTO menus (id, parent_id, name, title, path, component, icon, 
-                                 sort_order, menu_type, is_hidden, is_active, description, created_at, updated_at)
-                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
-            """, (
-                menu_id, menu_data.get('parent_id'), menu_data['name'], menu_data['title'],
-                menu_data.get('path'), menu_data.get('component'), menu_data.get('icon'),
-                menu_data.get('sort_order', 0), menu_data.get('menu_type', 'menu'),
-                menu_data.get('is_hidden', False), menu_data.get('is_active', True),
-                menu_data.get('description')
-            ))
-            
-            conn.commit()
-            return True, "菜单创建成功"
-        except Exception as e:
-            logger.exception("创建菜单错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def update_menu(self, menu_id: str, menu_data: Dict[str, Any]) -> Tuple[bool, str]:
-        """更新菜单"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 更新菜单
-            update_fields = []
-            update_values = []
-            
-            for field in ['parent_id', 'title', 'path', 'component', 'icon', 'sort_order', 
-                         'menu_type', 'is_hidden', 'is_active', 'description']:
-                if field in menu_data:
-                    update_fields.append(f'{field} = %s')
-                    update_values.append(menu_data[field])
-            
-            if update_fields:
-                update_values.append(menu_id)
-                cursor.execute(f"""
-                    UPDATE menus 
-                    SET {', '.join(update_fields)}, updated_at = NOW()
-                    WHERE id = %s
-                """, update_values)
-            
-            conn.commit()
-            return True, "菜单更新成功"
-        except Exception as e:
-            logger.exception("更新菜单错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def delete_menu(self, menu_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 menus WHERE parent_id = %s", (menu_id,))
-            if cursor.fetchone()['count'] > 0:
-                return False, "该菜单下有子菜单,无法删除"
-            
-            # 删除菜单相关数据
-            cursor.execute("DELETE FROM role_menus WHERE menu_id = %s", (menu_id,))
-            cursor.execute("DELETE FROM menus WHERE id = %s", (menu_id,))
-            
-            conn.commit()
-            return True, "菜单删除成功"
-        except Exception as e:
-            logger.exception("删除菜单错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-
-    
-    # ==================== 角色管理 ====================
-    
-    async def get_all_roles(self, page: int, page_size: int, keyword: Optional[str] = None) -> Tuple[List[Dict[str, Any]], int]:
-        """获取所有角色"""
-        conn = get_db_connection()
-        if not conn:
-            return [], 0
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 构建查询条件
-            where_conditions = []
-            params = []
-            
-            if keyword:
-                where_conditions.append("(r.display_name LIKE %s OR r.name LIKE %s)")
-                params.extend([f"%{keyword}%", f"%{keyword}%"])
-            
-            where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
-            
-            # 查询总数
-            cursor.execute(f"SELECT COUNT(*) as count FROM roles r WHERE {where_clause}", params)
-            total = cursor.fetchone()['count']
-            
-            # 查询角色列表
-            offset = (page - 1) * page_size
-            cursor.execute(f"""
-                SELECT r.id, r.name, r.display_name, r.description, r.is_active,
-                       r.is_system, r.created_at, r.updated_at,
-                       COUNT(ur.user_id) as user_count
-                FROM roles r
-                LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.is_active = 1
-                WHERE {where_clause}
-                GROUP BY r.id
-                ORDER BY r.is_system DESC, r.created_at
-                LIMIT %s OFFSET %s
-            """, params + [page_size, offset])
-            
-            roles = []
-            for row in cursor.fetchall():
-                role = {
-                    "id": row['id'],
-                    "name": row['name'],
-                    "display_name": row['display_name'],
-                    "description": row['description'],
-                    "is_active": bool(row['is_active']),
-                    "is_system": bool(row['is_system']),
-                    "created_at": row['created_at'].isoformat() if row['created_at'] else None,
-                    "updated_at": row['updated_at'].isoformat() if row['updated_at'] else None,
-                    "user_count": row['user_count']
-                }
-                roles.append(role)
-            
-            return roles, total
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def get_all_roles_simple(self) -> List[Dict[str, Any]]:
-        """获取所有角色(简化版,用于下拉选择)"""
-        conn = get_db_connection()
-        if not conn:
-            return []
-        
-        cursor = conn.cursor()
-        
-        try:
-            cursor.execute("""
-                SELECT id, name, display_name, is_system, is_active
-                FROM roles 
-                WHERE is_active = 1
-                ORDER BY is_system DESC, display_name
-            """)
-            
-            roles = []
-            for row in cursor.fetchall():
-                roles.append({
-                    "id": row['id'],
-                    "name": row['name'],
-                    "display_name": row['display_name'],
-                    "is_system": bool(row['is_system']),
-                    "is_active": bool(row['is_active'])
-                })
-            
-            return roles
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def create_role(self, role_data: Dict[str, Any]) -> Tuple[bool, str]:
-        """创建角色"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查角色名是否已存在
-            cursor.execute("SELECT id FROM roles WHERE name = %s", (role_data['name'],))
-            if cursor.fetchone():
-                return False, "角色名已存在"
-            
-            # 创建角色
-            role_id = str(uuid.uuid4())
-            cursor.execute("""
-                INSERT INTO roles (id, name, display_name, description, is_active, is_system, created_at, updated_at)
-                VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
-            """, (role_id, role_data['name'], role_data['display_name'], role_data.get('description'), 
-                  role_data.get('is_active', True), False))
-            
-            conn.commit()
-            return True, "角色创建成功"
-        except Exception as e:
-            logger.exception("创建角色错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def update_role(self, role_id: str, role_data: Dict[str, Any]) -> Tuple[bool, str]:
-        """更新角色"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查是否为系统角色
-            cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
-            role = cursor.fetchone()
-            if not role:
-                return False, "角色不存在"
-            
-            if role["is_system"]:
-                return False, "不能修改系统角色"
-            
-            # 更新角色
-            update_fields = []
-            update_values = []
-            
-            for field in ['display_name', 'description', 'is_active']:
-                if field in role_data:
-                    update_fields.append(f'{field} = %s')
-                    update_values.append(role_data[field])
-            
-            if update_fields:
-                update_values.append(role_id)
-                cursor.execute(f"""
-                    UPDATE roles 
-                    SET {', '.join(update_fields)}, updated_at = NOW()
-                    WHERE id = %s
-                """, update_values)
-            
-            conn.commit()
-            return True, "角色更新成功"
-        except Exception as e:
-            logger.exception("更新角色错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def delete_role(self, role_id: str) -> Tuple[bool, str]:
-        """删除角色"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败"
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查是否为系统角色
-            cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
-            role = cursor.fetchone()
-            if not role:
-                return False, "角色不存在"
-            
-            if role["is_system"]:
-                return False, "不能删除系统角色"
-            
-            # 检查是否有用户使用此角色
-            cursor.execute("SELECT COUNT(*) as count FROM user_roles WHERE role_id = %s", (role_id,))
-            if cursor.fetchone()['count'] > 0:
-                return False, "该角色正在被使用,无法删除"
-            
-            # 删除角色相关数据
-            cursor.execute("DELETE FROM role_permissions WHERE role_id = %s", (role_id,))
-            cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
-            cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,))
-            
-            conn.commit()
-            return True, "角色删除成功"
-        except Exception as e:
-            logger.exception("删除角色错误")
-            conn.rollback()
-            return False, "服务器内部错误"
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def get_role_menus(self, role_id: str) -> Tuple[Optional[Dict[str, Any]], List[str], List[Dict[str, Any]]]:
-        """获取角色的菜单权限"""
-        conn = get_db_connection()
-        if not conn:
-            return None, [], []
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查角色是否存在
-            cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
-            role = cursor.fetchone()
-            
-            if not role:
-                return None, [], []
-            
-            # 检查是否为超级管理员角色
-            role_name = role["name"]
-            is_super_admin_role = role_name == "super_admin"
-            
-            if is_super_admin_role:
-                # 超级管理员默认拥有所有菜单权限
-                cursor.execute("""
-                    SELECT id, name, title, parent_id, menu_type
-                    FROM menus
-                    WHERE is_active = 1
-                    ORDER BY sort_order
-                """)
-                menu_permissions = cursor.fetchall()
-            else:
-                # 普通角色查询已分配的菜单权限
-                cursor.execute("""
-                    SELECT m.id, m.name, m.title, m.parent_id, m.menu_type
-                    FROM role_menus rm
-                    JOIN menus m ON rm.menu_id = m.id
-                    WHERE rm.role_id = %s AND m.is_active = 1
-                    ORDER BY m.sort_order
-                """, (role_id,))
-                menu_permissions = cursor.fetchall()
-            
-            # 构建返回数据
-            menu_ids = [menu["id"] for menu in menu_permissions]
-            menu_details = []
-            
-            for menu in menu_permissions:
-                menu_details.append({
-                    "id": menu["id"],
-                    "name": menu["name"],
-                    "title": menu["title"],
-                    "parent_id": menu["parent_id"],
-                    "menu_type": menu["menu_type"]
-                })
-            
-            role_info = {
-                "id": role["id"],
-                "name": role["name"]
-            }
-            
-            return role_info, menu_ids, menu_details
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def update_role_menus(self, role_id: str, menu_ids: List[str]) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
-        """更新角色的菜单权限"""
-        conn = get_db_connection()
-        if not conn:
-            return False, "数据库连接失败", None
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 检查角色是否存在
-            cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
-            role = cursor.fetchone()
-            
-            if not role:
-                return False, "角色不存在", None
-            
-            # 检查是否为超级管理员角色
-            role_name = role["name"]
-            is_super_admin_role = role_name == "super_admin"
-            
-            if is_super_admin_role:
-                return False, "超级管理员角色拥有全部权限,无需修改", None
-            
-            # 验证菜单ID是否存在
-            if menu_ids:
-                placeholders = ','.join(['%s'] * len(menu_ids))
-                cursor.execute(f"""
-                    SELECT id FROM menus 
-                    WHERE id IN ({placeholders}) AND is_active = 1
-                """, menu_ids)
-                
-                valid_menu_ids = [row['id'] for row in cursor.fetchall()]
-                invalid_menu_ids = set(menu_ids) - set(valid_menu_ids)
-                
-                if invalid_menu_ids:
-                    return False, f"无效的菜单ID: {', '.join(invalid_menu_ids)}", None
-            
-            # 开始事务
-            cursor.execute("START TRANSACTION")
-            
-            # 删除角色现有的菜单权限
-            cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
-            
-            # 添加新的菜单权限
-            if menu_ids:
-                values = [(role_id, menu_id) for menu_id in menu_ids]
-                cursor.executemany("""
-                    INSERT INTO role_menus (role_id, menu_id, created_at)
-                    VALUES (%s, %s, NOW())
-                """, values)
-            
-            # 提交事务
-            conn.commit()
-            
-            result_data = {
-                "role_id": role_id,
-                "role_name": role["name"],
-                "menu_ids": menu_ids,
-                "updated_count": len(menu_ids)
-            }
-            
-            return True, "角色菜单权限更新成功", result_data
-        except Exception as e:
-            logger.exception("更新角色菜单权限错误")
-            conn.rollback()
-            return False, "服务器内部错误", None
-        finally:
-            cursor.close()
-            conn.close()
-    
-    async def get_user_permissions(self, user_id: str) -> List[Dict[str, Any]]:
-        """获取用户权限"""
-        conn = get_db_connection()
-        if not conn:
-            return []
-        
-        cursor = conn.cursor()
-        
-        try:
-            # 获取用户权限
-            cursor.execute("""
-                SELECT DISTINCT p.name, p.resource, p.action
-                FROM permissions p
-                JOIN role_permissions rp ON p.id = rp.permission_id
-                JOIN user_roles ur ON rp.role_id = ur.role_id
-                WHERE ur.user_id = %s 
-                AND ur.is_active = 1
-                AND p.is_active = 1
-            """, (user_id,))
-            
-            permissions = []
-            for row in cursor.fetchall():
-                permissions.append({
-                    "name": row['name'],
-                    "resource": row['resource'],
-                    "action": row['action']
-                })
-            
-            return permissions
-        finally:
-            cursor.close()
-            conn.close()

+ 0 - 70
test/test_db_connection_detailed.py

@@ -1,70 +0,0 @@
-#!/usr/bin/env python3
-"""
-测试数据库连接
-"""
-import os
-import sys
-
-# 导入配置
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-from app.core.config import config_handler
-
-print("=" * 60)
-print("测试数据库连接")
-print("=" * 60)
-
-# 测试环境变量
-database_url = config_handler.get("admin_app", "DATABASE_URL", "")
-print(f"\n1. DATABASE_URL: {database_url}")
-
-# 解析 URL
-from urllib.parse import urlparse
-parsed = urlparse(database_url)
-print(f"\n2. 解析结果:")
-print(f"   协议: {parsed.scheme}")
-print(f"   主机: {parsed.hostname}")
-print(f"   端口: {parsed.port}")
-print(f"   用户: {parsed.username}")
-print(f"   密码: {'*' * len(parsed.password) if parsed.password else 'None'}")
-print(f"   数据库: {parsed.path[1:] if parsed.path else 'None'}")
-
-# 测试 pymysql 连接
-print(f"\n3. 测试 pymysql 连接...")
-try:
-    import pymysql
-    conn = pymysql.connect(
-        host=parsed.hostname,
-        port=parsed.port,
-        user=parsed.username,
-        password=parsed.password,
-        database=parsed.path[1:] if parsed.path else None,
-        charset='utf8mb4'
-    )
-    print("   ✅ pymysql 连接成功!")
-    
-    # 测试查询
-    cursor = conn.cursor()
-    cursor.execute("SELECT VERSION()")
-    version = cursor.fetchone()
-    print(f"   MySQL 版本: {version[0]}")
-    
-    # 查询用户表
-    cursor.execute("SHOW TABLES LIKE 'users'")
-    result = cursor.fetchone()
-    if result:
-        print(f"   ✅ users 表存在")
-        cursor.execute("SELECT COUNT(*) FROM users")
-        count = cursor.fetchone()
-        print(f"   用户数量: {count[0]}")
-    else:
-        print(f"   ❌ users 表不存在")
-    
-    cursor.close()
-    conn.close()
-    
-except Exception as e:
-    print(f"   ❌ 连接失败: {e}")
-    import traceback
-    traceback.print_exc()
-
-print("\n" + "=" * 60)

+ 0 - 86
test/test_full_login_flow.py

@@ -1,86 +0,0 @@
-#!/usr/bin/env python3
-"""
-完整登录流程测试
-"""
-import requests
-import json
-
-print("=" * 60)
-print("完整登录流程测试")
-print("=" * 60)
-
-base_url = "http://localhost:8000"
-
-# 1. 测试登录
-print("\n1. 测试登录...")
-print("-" * 60)
-
-login_response = requests.post(
-    f"{base_url}/api/v1/auth/login",
-    json={
-        "username": "admin",
-        "password": "Admin123456"
-    }
-)
-
-print(f"状态码: {login_response.status_code}")
-login_data = login_response.json()
-
-if login_data.get("code") == 0:
-    print("✅ 登录成功!")
-    access_token = login_data["data"]["access_token"]
-    refresh_token = login_data["data"]["refresh_token"]
-    print(f"Access Token: {access_token[:50]}...")
-    print(f"Refresh Token: {refresh_token[:50]}...")
-else:
-    print(f"❌ 登录失败: {login_data.get('message')}")
-    exit(1)
-
-# 2. 测试获取用户信息
-print("\n2. 测试获取用户信息...")
-print("-" * 60)
-
-userinfo_response = requests.get(
-    f"{base_url}/api/v1/auth/userinfo",
-    headers={
-        "Authorization": f"Bearer {access_token}"
-    }
-)
-
-print(f"状态码: {userinfo_response.status_code}")
-userinfo_data = userinfo_response.json()
-
-if userinfo_data.get("code") == 0:
-    print("✅ 获取用户信息成功!")
-    user = userinfo_data["data"]
-    print(f"用户ID: {user.get('id')}")
-    print(f"用户名: {user.get('username')}")
-    print(f"邮箱: {user.get('email')}")
-    print(f"是否激活: {user.get('is_active')}")
-else:
-    print(f"❌ 获取用户信息失败: {userinfo_data.get('message')}")
-
-# 3. 测试刷新令牌
-print("\n3. 测试刷新令牌...")
-print("-" * 60)
-
-refresh_response = requests.post(
-    f"{base_url}/api/v1/auth/refresh",
-    json={
-        "refresh_token": refresh_token
-    }
-)
-
-print(f"状态码: {refresh_response.status_code}")
-refresh_data = refresh_response.json()
-
-if refresh_data.get("code") == 0:
-    print("✅ 刷新令牌成功!")
-    new_access_token = refresh_data["data"]["access_token"]
-    print(f"新 Access Token: {new_access_token[:50]}...")
-else:
-    print(f"❌ 刷新令牌失败: {refresh_data.get('message')}")
-
-print("\n" + "=" * 60)
-print("✅ 所有测试通过!登录功能正常工作。")
-print("=" * 60)

+ 0 - 26
test/test_quick_login.py

@@ -1,26 +0,0 @@
-#!/usr/bin/env python3
-"""
-快速登录测试
-"""
-import requests
-import json
-
-print("测试登录API...")
-
-try:
-    response = requests.post(
-        "http://localhost:8000/api/v1/auth/login",
-        json={
-            "username": "admin",
-            "password": "Admin123456"
-        },
-        timeout=5
-    )
-    
-    print(f"状态码: {response.status_code}")
-    print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
-    
-except Exception as e:
-    print(f"错误: {e}")
-    import traceback
-    traceback.print_exc()