refactor: 重构后端结构并迁移前端到 Vue3

This commit is contained in:
2026-05-15 11:46:21 +08:00
parent 9107378fcc
commit 9c5a5a54c6
165 changed files with 5057 additions and 62710 deletions
+2
View File
@@ -0,0 +1,2 @@
"""CapacityReport backend package."""
+2
View File
@@ -0,0 +1,2 @@
"""API package."""
+2
View File
@@ -0,0 +1,2 @@
"""API router package."""
+36
View File
@@ -0,0 +1,36 @@
from fastapi import APIRouter, Body
from fastapi.responses import JSONResponse
from app.auth import create_jwt_token, get_auth_config, save_auth_password
router = APIRouter(tags=["auth"])
@router.post("/api/login")
async def login(
username: str = Body(..., embed=True),
password: str = Body(..., embed=True),
):
auth = get_auth_config()
if username != auth["username"] or password != auth["password"]:
return JSONResponse(status_code=401, content={"detail": "账号或密码错误"})
token = create_jwt_token({"user": username})
return {"success": True, "token": token}
@router.post("/api/change-password")
async def change_password(
current_password: str = Body(..., embed=True),
new_password: str = Body(..., embed=True),
):
auth = get_auth_config()
if current_password != auth["password"]:
return JSONResponse(status_code=400, content={"detail": "当前密码错误"})
if len(new_password) < 4:
return JSONResponse(status_code=400, content={"detail": "新密码长度不能少于 4 位"})
save_auth_password(new_password)
return {"success": True, "message": "密码修改成功"}
+44
View File
@@ -0,0 +1,44 @@
from fastapi import APIRouter
from app.config import CACHE_DIR
from app.utils.files import format_size, get_dir_size
router = APIRouter(tags=["cache"])
@router.get("/api/cache/size")
async def get_cache_size():
if not CACHE_DIR.exists():
return {
"success": True,
"size_bytes": 0,
"size_formatted": "0 B",
"file_count": 0,
"dir_count": 0,
}
total_size = 0
file_count = 0
dir_count = 0
try:
for item in CACHE_DIR.iterdir():
if item.name == "history.json":
continue
if item.is_dir():
dir_count += 1
elif item.is_file():
file_count += 1
total_size += get_dir_size(item)
except (PermissionError, OSError) as exc:
return {"success": False, "error": str(exc), "size_formatted": "计算失败"}
return {
"success": True,
"size_bytes": total_size,
"size_formatted": format_size(total_size),
"file_count": file_count,
"dir_count": dir_count,
}
+99
View File
@@ -0,0 +1,99 @@
import json
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Body, HTTPException, UploadFile, File
from fastapi.responses import FileResponse
from app import state
from app.config import CONFIG_FILE
router = APIRouter(tags=["config"])
@router.get("/api/config")
async def get_config():
return state.config.to_dict()
@router.get("/api/config/full")
async def get_config_full():
return state.config.to_dict_full()
@router.post("/api/config/mysql")
async def update_mysql_config(
host: str = Body(...),
port: int = Body(...),
user: str = Body(...),
passwd: str = Body(...),
dbname: str = Body(...),
):
state.config.mysql.host = host
state.config.mysql.port = port
state.config.mysql.user = user
state.config.mysql.passwd = passwd
state.config.mysql.dbname = dbname
state.config.save()
return {"success": True, "message": "数据库配置已更新", "update": state.config.update}
@router.post("/api/config/sheet-filter")
async def update_sheet_filter(filters: list[str] = Body(...)):
state.config.sheet_filter = filters
state.config.save()
return {"success": True, "message": "Sheet 过滤规则已更新", "update": state.config.update}
@router.post("/api/config/extract-fields")
async def update_extract_fields(fields: list[dict[str, Any]] = Body(...)):
state.config.extract_fields = fields
state.config.save()
return {"success": True, "message": "字段映射配置已更新", "update": state.config.update}
@router.get("/api/config/download")
async def download_config():
if not CONFIG_FILE.exists():
raise HTTPException(status_code=404, detail="配置文件不存在")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"Configure_{timestamp}.json"
return FileResponse(path=str(CONFIG_FILE), filename=filename, media_type="application/json")
@router.post("/api/config/upload")
async def upload_config(file: UploadFile = File(...)):
if not file.filename or not file.filename.endswith(".json"):
raise HTTPException(status_code=400, detail="只支持 JSON 格式的配置文件")
try:
data = json.loads((await file.read()).decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("配置文件格式错误:必须是 JSON 对象")
_apply_config_data(data)
state.config.save()
return {"success": True, "message": "配置文件上传成功", "update": state.config.update}
except json.JSONDecodeError as exc:
raise HTTPException(status_code=400, detail="配置文件格式错误:不是有效的 JSON 文件") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"上传失败: {exc}") from exc
def _apply_config_data(data: dict[str, Any]) -> None:
mysql_data = data.get("MySQL_DBInfo")
if isinstance(mysql_data, dict):
for key in ("host", "port", "user", "passwd", "dbname"):
if key in mysql_data:
setattr(state.config.mysql, key, mysql_data[key])
if "SheetFilter" in data:
state.config.sheet_filter = data["SheetFilter"] if isinstance(data["SheetFilter"], list) else []
if "ExtractField" in data:
state.config.extract_fields = data["ExtractField"] if isinstance(data["ExtractField"], list) else []
+185
View File
@@ -0,0 +1,185 @@
from datetime import datetime
from typing import Optional
import pandas as pd
from fastapi import APIRouter, Body, HTTPException
from fastapi.responses import FileResponse
from app import state
from app.config import CACHE_DIR
from app.database import DatabaseManager
router = APIRouter(tags=["database"])
@router.post("/api/database/test")
async def test_database():
db = DatabaseManager(state.config)
try:
success, message = db.test_connection()
return {"success": success, "message": message}
finally:
db.dispose()
@router.get("/api/database/info")
async def get_database_info():
db = DatabaseManager(state.config)
try:
return {"success": True, **db.get_server_info()}
except Exception as exc:
return {"success": False, "error": str(exc)}
finally:
db.dispose()
@router.get("/api/database/tables")
@router.post("/api/database/tables")
async def get_tables():
db = DatabaseManager(state.config)
try:
return {"tables": db.get_tables()}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/info")
async def get_table_info(table_name: str = Body(..., embed=True)):
db = DatabaseManager(state.config)
try:
return db.get_table_info(table_name)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/data")
async def query_table_data(
table_name: str = Body(..., embed=True),
page: int = Body(1),
page_size: int = Body(50),
order_by: Optional[str] = Body(None),
order_dir: str = Body("ASC"),
):
db = DatabaseManager(state.config)
try:
return db.query_table(table_name, page, page_size, order_by=order_by, order_dir=order_dir)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/query")
async def query_table_with_filter(
table_name: str = Body(..., embed=True),
page: int = Body(1),
page_size: int = Body(50),
filters: Optional[dict[str, str]] = Body(None),
order_by: Optional[str] = Body(None),
order_dir: str = Body("ASC"),
):
db = DatabaseManager(state.config)
try:
return db.query_table(
table_name,
page,
page_size,
filters=filters or {},
order_by=order_by,
order_dir=order_dir,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/truncate")
async def truncate_table(table_name: str = Body(..., embed=True)):
db = DatabaseManager(state.config)
try:
db.truncate_table(table_name)
return {"success": True, "message": f"表 {table_name} 已清空"}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/drop")
async def drop_table(table_name: str = Body(..., embed=True)):
db = DatabaseManager(state.config)
try:
db.drop_table(table_name)
return {"success": True, "message": f"表 {table_name} 已删除"}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/table/drop-all")
async def drop_all_tables():
db = DatabaseManager(state.config)
try:
result = db.drop_all_tables()
return {
"success": True,
"message": f"已删除 {result['dropped_count']} 个表",
"dropped_count": result["dropped_count"],
"tables": result["tables"],
}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/database/execute")
async def execute_sql(sql: str = Body(..., embed=True)):
db = DatabaseManager(state.config)
try:
success, result = db.execute_sql(sql)
if success:
return {"success": True, "result": result}
raise HTTPException(status_code=400, detail=result)
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
@router.post("/api/download")
async def download_table(
table_name: str = Body(..., embed=True),
file_format: str = Body("csv", alias="format"),
):
db = DatabaseManager(state.config)
try:
result = db.query_table(table_name, page=1, page_size=1000000)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
db.dispose()
df = pd.DataFrame(result["data"])
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{table_name}_{timestamp}.{file_format}"
filepath = CACHE_DIR / filename
if file_format == "csv":
df.to_csv(filepath, index=False, encoding="utf-8-sig")
media_type = "text/csv"
else:
df.to_excel(filepath, index=False)
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
return FileResponse(path=str(filepath), filename=filename, media_type=media_type)
+42
View File
@@ -0,0 +1,42 @@
import os
from datetime import datetime
from fastapi import APIRouter
from app import state
from app.database import DatabaseManager
router = APIRouter(tags=["health"])
@router.get("/health")
async def health_check():
checks = {
"app": {"status": "ok"},
"database": {"status": "unknown"},
}
try:
db_manager = DatabaseManager(state.config)
try:
server_info = db_manager.get_server_info()
checks["database"] = {
"status": "ok",
"version": server_info.get("version", "unknown"),
"load_data_infile": server_info.get("load_data_infile", False),
}
finally:
db_manager.dispose()
except Exception as exc:
checks["database"] = {"status": "error", "message": str(exc)}
is_healthy = all(check.get("status") == "ok" for check in checks.values())
return {
"status": "healthy" if is_healthy else "unhealthy",
"timestamp": datetime.now().isoformat(),
"version": "2.0.2",
"uptime_pid": os.getpid(),
"checks": checks,
}
+53
View File
@@ -0,0 +1,53 @@
from pathlib import Path
from fastapi import APIRouter, Body, HTTPException
from app import state
from app.utils.files import format_size, get_dir_size
router = APIRouter(tags=["history"])
@router.post("/api/history")
async def get_history(limit: int = Body(50, embed=True)):
return {"records": state.history_manager.list(limit)}
@router.post("/api/history/delete")
async def delete_history(record_id: str = Body(..., embed=True)):
if state.history_manager.delete(record_id):
return {"success": True, "message": "删除成功"}
raise HTTPException(status_code=404, detail="记录不存在")
@router.post("/api/history/clear")
async def clear_history():
count = state.history_manager.clear()
return {"success": True, "deleted": count}
@router.post("/api/history/detail")
async def get_history_detail(record_id: str = Body(..., embed=True)):
record = state.history_manager.get(record_id)
if not record:
raise HTTPException(status_code=404, detail="记录不存在")
result = record.to_dict()
result["logs"] = state.history_manager.get_logs(record_id)
return result
@router.post("/api/history/size")
async def get_history_size(record_id: str = Body(..., embed=True)):
record = state.history_manager.get(record_id)
if not record:
raise HTTPException(status_code=404, detail="记录不存在")
work_dir = Path(record.work_dir)
if not work_dir.exists():
return {"success": True, "size": 0, "size_formatted": "0 B"}
size = get_dir_size(work_dir)
return {"success": True, "size": size, "size_formatted": format_size(size)}
+99
View File
@@ -0,0 +1,99 @@
import shutil
import uuid
from datetime import datetime
from pathlib import Path
from threading import Thread
from fastapi import APIRouter, Body, HTTPException
from app import state
from app.config import CACHE_DIR, SQL_SCRIPT
from app.processor import DataProcessor, ProcessLogger
router = APIRouter(tags=["script"])
@router.get("/api/script/content")
async def get_script_content():
try:
if not SQL_SCRIPT.exists():
return {
"success": True,
"content": "# SQL 脚本文件不存在,请在此编写脚本\n",
"modified": None,
"path": str(SQL_SCRIPT),
}
modified = datetime.fromtimestamp(SQL_SCRIPT.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return {
"success": True,
"content": SQL_SCRIPT.read_text(encoding="utf-8"),
"modified": modified,
"path": str(SQL_SCRIPT),
}
except Exception as exc:
return {"success": False, "error": str(exc)}
@router.post("/api/script/execute")
async def execute_script():
if state.global_task_lock["locked"]:
raise HTTPException(status_code=409, detail="已有任务在运行,请等待完成")
task_id = f"script_{uuid.uuid4().hex[:8]}"
state.global_task_lock.update(
{
"locked": True,
"task_id": task_id,
"stage": "processing",
"started_at": datetime.now().isoformat(),
}
)
logs: list[str] = []
def log_callback(message: str) -> None:
logs.append(message)
state.processing_tasks[task_id] = {"logs": logs.copy(), "status": "processing"}
logger = ProcessLogger(log_file=None, callback=log_callback)
state.processing_tasks[task_id] = {"logs": [], "status": "processing"}
thread = Thread(target=_run_script, args=(task_id, logger, logs), daemon=True)
thread.start()
return {"success": True, "message": "脚本执行任务已启动", "task_id": task_id}
@router.post("/api/script/save")
async def save_script_content(content: str = Body(..., embed=True)):
try:
if SQL_SCRIPT.exists():
shutil.copy(SQL_SCRIPT, SQL_SCRIPT.with_suffix(".sql.bak"))
SQL_SCRIPT.write_text(content, encoding="utf-8")
modified = datetime.fromtimestamp(SQL_SCRIPT.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return {"success": True, "message": "脚本保存成功", "modified": modified}
except Exception as exc:
return {"success": False, "error": str(exc)}
def _run_script(task_id: str, logger: ProcessLogger, logs: list[str]) -> None:
temp_work_dir: Path | None = None
try:
logger.info("开始执行 SQL 脚本...")
temp_work_dir = CACHE_DIR / task_id
temp_work_dir.mkdir(parents=True, exist_ok=True)
processor = DataProcessor(state.config, temp_work_dir, logger)
processor._execute_sql_script()
logger.success("SQL 脚本执行完成")
state.processing_tasks[task_id] = {"logs": logs.copy(), "status": "completed"}
except Exception as exc:
logger.error(f"SQL 脚本执行失败: {exc}")
state.processing_tasks[task_id] = {"logs": logs.copy(), "status": "failed"}
finally:
if temp_work_dir and temp_work_dir.exists():
shutil.rmtree(temp_work_dir, ignore_errors=True)
state.reset_task_lock()
+70
View File
@@ -0,0 +1,70 @@
import os
import platform
import subprocess
import time
from threading import Thread
from fastapi import APIRouter
from app.services.runtime import (
is_supervisor_running,
restart_via_supervisor,
terminate_current_process,
)
router = APIRouter(tags=["service"])
@router.post("/api/service/restart")
async def restart_service():
if is_supervisor_running():
success, message = restart_via_supervisor()
return {"success": success, "message": message, "method": "supervisor"}
if platform.system() != "Windows":
try:
result = subprocess.run(
["supervisorctl", "restart", "fastapi"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
return {
"success": True,
"message": "服务正在通过 supervisor 重启...",
"method": "supervisor",
}
except FileNotFoundError:
pass
except subprocess.TimeoutExpired:
return {
"success": False,
"message": "重启操作超时,请检查 supervisor 状态",
"method": "supervisor",
}
except Exception:
pass
thread = Thread(target=_delayed_exit, daemon=True)
thread.start()
return {"success": True, "message": "服务正在重启,请稍后刷新页面...", "method": "signal"}
@router.get("/api/service/status")
async def get_service_status():
return {
"status": "running",
"version": "2.0.2",
"platform": platform.system(),
"supervisor": is_supervisor_running(),
"pid": os.getpid(),
"python_version": platform.python_version(),
}
def _delayed_exit() -> None:
time.sleep(1)
terminate_current_process()
+167
View File
@@ -0,0 +1,167 @@
from datetime import datetime
from pathlib import Path
from threading import Thread
from typing import Any
from fastapi import APIRouter, Body, HTTPException
from app import state
from app.processor import DataProcessor, ProcessLogger
router = APIRouter(tags=["tasks"])
@router.get("/api/task/status")
async def get_global_task_status():
if state.global_task_lock["locked"]:
task_id = state.global_task_lock["task_id"]
if task_id and _task_finished(task_id):
state.reset_task_lock()
return {"has_active": False}
return {
"has_active": True,
"task_id": task_id,
"stage": state.global_task_lock["stage"],
"started_at": state.global_task_lock["started_at"],
"logs": [],
}
active_tasks = {
task_id: task
for task_id, task in state.processing_tasks.items()
if task.get("status") == "processing"
}
if active_tasks:
task_id = next(iter(active_tasks))
return {
"has_active": True,
"task_id": task_id,
"stage": "processing",
"logs": active_tasks[task_id].get("logs", []),
}
return {"has_active": False}
@router.post("/api/task/lock")
async def lock_task(task_id: str = Body(..., embed=True)):
if state.global_task_lock["locked"]:
raise HTTPException(status_code=409, detail="已有任务在运行")
state.global_task_lock.update(
{
"locked": True,
"task_id": task_id,
"stage": "uploading",
"started_at": datetime.now().isoformat(),
}
)
return {"success": True, "message": "任务已锁定"}
@router.post("/api/task/unlock")
async def unlock_task(task_id: str | None = Body(None, embed=True)):
if task_id and state.global_task_lock["task_id"] != task_id:
raise HTTPException(status_code=403, detail="无权解锁此任务")
state.reset_task_lock()
return {"success": True, "message": "任务已解锁"}
@router.get("/api/process/active")
async def get_active_task():
return await get_global_task_status()
@router.post("/api/process/start")
async def start_processing(task_id: str = Body(..., embed=True)):
record = state.history_manager.get(task_id)
if not record:
raise HTTPException(status_code=404, detail="任务不存在")
if record.status == "processing":
raise HTTPException(status_code=400, detail="任务正在处理中")
work_dir = Path(record.work_dir)
if not work_dir.exists():
raise HTTPException(status_code=400, detail="工作目录不存在")
logs: list[str] = []
def log_callback(message: str) -> None:
logs.append(message)
state.processing_tasks[task_id] = {"logs": logs.copy(), "status": "processing"}
logger = ProcessLogger(log_file=work_dir / "log.txt", callback=log_callback)
state.history_manager.update(task_id, status="processing")
state.processing_tasks[task_id] = {"logs": [], "status": "processing"}
state.global_task_lock.update(
{
"locked": True,
"task_id": task_id,
"stage": "processing",
"started_at": datetime.now().isoformat(),
}
)
thread = Thread(target=_run_processing, args=(task_id, work_dir, logger), daemon=True)
thread.start()
return {"success": True, "message": "处理任务已启动", "task_id": task_id}
@router.post("/api/process/status")
async def get_processing_status(task_id: str = Body(..., embed=True)):
if task_id in state.processing_tasks:
task_info = state.processing_tasks[task_id]
logs = task_info.get("logs") or state.history_manager.get_logs(task_id)
return {"task_id": task_id, "status": task_info["status"], "logs": logs}
record = state.history_manager.get(task_id)
if not record:
raise HTTPException(status_code=404, detail="任务不存在")
return {
"task_id": task_id,
"status": record.status,
"logs": state.history_manager.get_logs(task_id),
"elapsed_time": record.elapsed_time,
"error": record.error,
}
def _task_finished(task_id: str) -> bool:
record = state.history_manager.get(task_id)
if record and record.status in {"completed", "failed"}:
return True
task_info: dict[str, Any] | None = state.processing_tasks.get(task_id)
return bool(task_info and task_info.get("status") in {"completed", "failed"})
def _run_processing(task_id: str, work_dir: Path, logger: ProcessLogger) -> None:
try:
processor = DataProcessor(state.config, work_dir, logger)
result = processor.process()
status = "completed" if result.get("success") else "failed"
state.history_manager.update(
task_id,
status=status,
elapsed_time=result.get("elapsed_time", 0),
error=result.get("error"),
result_tables=["4G_结果表", "5G_结果表"],
)
state.processing_tasks[task_id] = {
"logs": state.history_manager.get_logs(task_id),
"status": status,
}
except Exception as exc:
state.history_manager.update(task_id, status="failed", error=str(exc))
state.processing_tasks[task_id] = {
"logs": state.history_manager.get_logs(task_id),
"status": "failed",
}
finally:
state.reset_task_lock()
+110
View File
@@ -0,0 +1,110 @@
from datetime import datetime
from typing import Any, Optional
from fastapi import APIRouter, File, HTTPException, UploadFile
from app import state
from app.config import CACHE_DIR
router = APIRouter(tags=["upload"])
@router.post("/api/upload/create")
async def create_upload_session():
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
work_dir = CACHE_DIR / session_id
work_dir.mkdir(parents=True, exist_ok=True)
state.upload_sessions[session_id] = {
"work_dir": work_dir,
"files": [],
"created_at": datetime.now().isoformat(),
}
return {"success": True, "session_id": session_id, "work_dir": str(work_dir)}
@router.post("/api/upload")
async def upload_files(
files: list[UploadFile] = File(...),
session_id: Optional[str] = None,
):
if not files:
raise HTTPException(status_code=400, detail="没有上传文件")
is_new_session = False
if not session_id or session_id not in state.upload_sessions:
if state.global_task_lock["locked"]:
raise HTTPException(status_code=409, detail="已有任务在运行,请等待当前任务完成")
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
work_dir = CACHE_DIR / session_id
work_dir.mkdir(parents=True, exist_ok=True)
state.global_task_lock.update(
{
"locked": True,
"task_id": session_id,
"stage": "uploading",
"started_at": datetime.now().isoformat(),
}
)
is_new_session = True
state.upload_sessions[session_id] = {
"work_dir": work_dir,
"files": [],
"created_at": datetime.now().isoformat(),
}
else:
if state.global_task_lock["locked"] and state.global_task_lock["task_id"] != session_id:
raise HTTPException(status_code=409, detail="已有其他任务在运行")
session: dict[str, Any] = state.upload_sessions[session_id]
work_dir = session["work_dir"]
try:
saved_files: list[str] = []
for file in files:
if not file.filename:
continue
file_path = work_dir / file.filename
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_bytes(await file.read())
saved_files.append(file.filename)
session["files"].append(file.filename)
record = state.history_manager.get(session_id)
if record:
state.history_manager.update(session_id, file_count=len(session["files"]))
else:
state.history_manager.create(work_dir, len(session["files"]), record_id=session_id)
return {
"success": True,
"task_id": session_id,
"session_id": session_id,
"work_dir": str(work_dir),
"file_count": len(saved_files),
"total_files": len(session["files"]),
"files": saved_files,
}
except Exception as exc:
if is_new_session:
state.reset_task_lock()
raise HTTPException(status_code=500, detail=f"上传失败: {exc}") from exc
@router.post("/api/upload/complete/{session_id}")
async def complete_upload_session(session_id: str):
if session_id not in state.upload_sessions:
raise HTTPException(status_code=404, detail="上传会话不存在")
session = state.upload_sessions[session_id]
state.history_manager.update(session_id, file_count=len(session["files"]))
return {"success": True, "session_id": session_id, "total_files": len(session["files"])}
+84
View File
@@ -0,0 +1,84 @@
import base64
import configparser
import hashlib
import hmac
import json
import time
from pathlib import Path
from typing import Optional
from app.config import BASE_DIR
SECRET_KEY = "CapaReportSecretKey2026"
AUTH_INI_PATH = BASE_DIR / "auth.ini"
DEFAULT_USERNAME = "root"
DEFAULT_PASSWORD = "Capacity"
def _ensure_auth_ini() -> None:
if AUTH_INI_PATH.exists():
return
cfg = configparser.ConfigParser()
cfg["auth"] = {"username": DEFAULT_USERNAME, "password": DEFAULT_PASSWORD}
with AUTH_INI_PATH.open("w", encoding="utf-8") as file:
cfg.write(file)
def get_auth_config() -> dict[str, str]:
_ensure_auth_ini()
cfg = configparser.ConfigParser()
cfg.read(AUTH_INI_PATH, encoding="utf-8")
return {
"username": cfg.get("auth", "username", fallback=DEFAULT_USERNAME),
"password": cfg.get("auth", "password", fallback=DEFAULT_PASSWORD),
}
def save_auth_password(new_password: str) -> None:
_ensure_auth_ini()
cfg = configparser.ConfigParser()
cfg.read(AUTH_INI_PATH, encoding="utf-8")
cfg.set("auth", "password", new_password)
with AUTH_INI_PATH.open("w", encoding="utf-8") as file:
cfg.write(file)
def create_jwt_token(data: dict, expires_in: int = 86400 * 30) -> str:
header = _encode_json({"alg": "HS256", "typ": "JWT"})
payload_data = data.copy()
payload_data["exp"] = int(time.time()) + expires_in
payload = _encode_json(payload_data)
signature = _sign(header, payload)
return f"{header}.{payload}.{signature}"
def verify_jwt_token(token: str) -> Optional[dict]:
try:
header, payload, signature = token.split(".")
if not hmac.compare_digest(signature, _sign(header, payload)):
return None
payload_padded = payload + "=" * ((4 - len(payload) % 4) % 4)
data = json.loads(base64.urlsafe_b64decode(payload_padded).decode())
if data.get("exp", 0) < int(time.time()):
return None
return data
except Exception:
return None
def _encode_json(data: dict) -> str:
raw = json.dumps(data, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
def _sign(header: str, payload: str) -> str:
raw = hmac.new(
SECRET_KEY.encode(),
f"{header}.{payload}".encode(),
hashlib.sha256,
).digest()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
+5 -4
View File
@@ -2,12 +2,13 @@
数据库连接与操作模块 - 性能优化版
"""
import pymysql
import sqlalchemy
from sqlalchemy import create_engine, text, event
from sqlalchemy.pool import QueuePool
from urllib.parse import quote
from typing import Any, Dict, List, Optional, Tuple
from contextlib import contextmanager
from typing import Any, Dict, List, Optional, Tuple
import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
from app.config import AppConfig
+88 -1352
View File
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -1,19 +1,18 @@
"""
数据处理核心模块 - 性能优化版
"""
import chardet
import os
import re
import shutil
import tempfile
import time
import zipfile
import multiprocessing
import numpy as np
import pandas as pd
import sqlparse
from pathlib import Path
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from io import StringIO
from app.config import AppConfig, SQL_SCRIPT
from app.database import DatabaseManager
@@ -259,7 +258,6 @@ class DataProcessor:
def _detect_encoding(self, file_path: Path) -> str:
"""快速检测文件编码(只读取前 8KB)"""
import chardet
with open(file_path, 'rb') as f:
# 只读取前 8KB,足够检测编码,比 64KB 快很多
result = chardet.detect(f.read(8192))
@@ -377,7 +375,6 @@ class DataProcessor:
temp_dir = self.work_dir / '.temp'
if temp_dir.exists():
try:
import shutil
shutil.rmtree(temp_dir)
except Exception:
pass
@@ -405,8 +402,7 @@ class DataProcessor:
如果失败则自动回退到 bulk_insert 方式
临时文件放在工作目录的 .temp 子目录中
"""
import tempfile
# 检测是否支持 LOAD DATA INFILE
if not self._check_load_data_support():
# 不支持,直接使用 bulk_insert
+2
View File
@@ -0,0 +1,2 @@
"""Service helpers."""
+61
View File
@@ -0,0 +1,61 @@
import os
import platform
import signal
import subprocess
from pathlib import Path
def is_supervisor_running() -> bool:
if os.environ.get("SUPERVISOR_ENABLED") == "1":
return True
supervisor_sock = Path("/var/run/supervisor.sock")
if supervisor_sock.exists() and _command_succeeds(["supervisorctl", "status"], timeout=5):
return True
try:
import psutil # pyright: ignore[reportMissingModuleSource]
parent = psutil.Process().parent()
if parent and "supervisor" in parent.name().lower():
return True
except Exception:
pass
return _command_succeeds(["pgrep", "-f", "supervisord"], timeout=2)
def restart_via_supervisor() -> tuple[bool, str]:
try:
result = subprocess.run(
["supervisorctl", "restart", "fastapi"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
return True, "服务正在通过 supervisor 重启..."
error_msg = result.stderr or result.stdout or "未知错误"
return False, f"重启失败: {error_msg}"
except subprocess.TimeoutExpired:
return False, "重启操作超时"
except FileNotFoundError:
return False, "找不到 supervisorctl 命令"
except Exception as exc:
return False, f"重启异常: {exc}"
def terminate_current_process() -> None:
if platform.system() == "Windows":
os._exit(0)
os.kill(os.getpid(), signal.SIGTERM)
def _command_succeeds(command: list[str], timeout: int) -> bool:
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
return result.returncode == 0 and bool(result.stdout.strip() or command[0] == "supervisorctl")
except Exception:
return False
+25
View File
@@ -0,0 +1,25 @@
from typing import Any
from app.config import AppConfig
from app.history import HistoryManager
config = AppConfig.load()
history_manager = HistoryManager()
processing_tasks: dict[str, dict[str, Any]] = {}
upload_sessions: dict[str, dict[str, Any]] = {}
global_task_lock: dict[str, Any] = {
"locked": False,
"task_id": None,
"stage": None,
"started_at": None,
}
def reset_task_lock() -> None:
global_task_lock["locked"] = False
global_task_lock["task_id"] = None
global_task_lock["stage"] = None
global_task_lock["started_at"] = None
+2
View File
@@ -0,0 +1,2 @@
"""Utility helpers."""
+29
View File
@@ -0,0 +1,29 @@
from pathlib import Path
def get_dir_size(path: Path) -> int:
total_size = 0
try:
if path.is_file():
return path.stat().st_size
if path.is_dir():
for item in path.iterdir():
total_size += get_dir_size(item)
except (PermissionError, OSError):
return total_size
return total_size
def format_size(size_bytes: int) -> str:
if size_bytes == 0:
return "0 B"
value = float(size_bytes)
for unit in ["B", "KB", "MB", "GB", "TB"]:
if value < 1024.0:
return f"{value:.2f} {unit}"
value /= 1024.0
return f"{value:.2f} PB"