@
feat: shadcn主题 + 文件关系追踪 + 处理流程修复 前端: - 全站应用 shadcn/ui 主题 (zinc灰调, Inter字体, 1px细边框, 无硬阴影) - 重写 global.css / Dashboard.vue / Login.vue / Layout.vue 样式 - 新增文件处理子页面: 采购单(Orders), 表格处理(Tables), 图片处理(Images) - 侧边栏使用 el-sub-menu 组织文件处理导航 后端: - 新增 file_relations 表追踪 input→output→result 链路 - 新增 /files/relations, /files/stats/detailed 等关系查询API - 新增 ocr-single, excel-single, pipeline-single, merge-batch 端点 - 处理流程增加跳过逻辑 (已处理文件自动跳过) - 全流程不再自动合并, 合并仅在采购单页面手动触发 Bug修复: - TaskManager: asyncio.create_task 在线程池中无事件循环 → 改用 _schedule() 调度 - PurchaseOrderMerger 缺少 config 参数 → 传入 ConfigManager() - FastAPI regex= 弃用 → 改为 pattern= - merger.process() 接收 Path 对象 → 转为字符串 @
This commit is contained in:
@@ -12,7 +12,11 @@ from pydantic import BaseModel
|
||||
|
||||
from ..auth.dependencies import get_current_user, get_current_user_flexible
|
||||
from ..config import MAX_UPLOAD_SIZE, ALLOWED_EXTENSIONS
|
||||
from ..services.db_schema import insert_file_metadata, query_file_history, query_file_stats
|
||||
from ..services.db_schema import (
|
||||
insert_file_metadata, query_file_history, query_file_stats,
|
||||
query_file_relations, delete_file_relations, sync_file_relations,
|
||||
query_file_relations_stats,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,6 +58,7 @@ def _record_file_action(filename: str, directory: str, size: int, action: str, u
|
||||
@router.post("/upload", response_model=UploadResponse)
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
target: str = Query("input", pattern="^(input|output)$"),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
_ensure_dirs()
|
||||
@@ -68,10 +73,13 @@ async def upload_file(
|
||||
if len(content) > MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(400, f"文件过大,最大 {MAX_UPLOAD_SIZE // 1024 // 1024}MB")
|
||||
|
||||
# Choose target directory
|
||||
target_dir = _output_dir if target == "output" else _input_dir
|
||||
|
||||
# Save with secure name
|
||||
from werkzeug.utils import secure_filename
|
||||
safe_name = secure_filename(file.filename) or file.filename
|
||||
dest = _input_dir / safe_name
|
||||
dest = target_dir / safe_name
|
||||
|
||||
# Avoid overwrite: add suffix if exists
|
||||
counter = 0
|
||||
@@ -79,10 +87,10 @@ async def upload_file(
|
||||
suffix = Path(safe_name).suffix
|
||||
while dest.exists():
|
||||
counter += 1
|
||||
dest = _input_dir / f"{stem}_{counter}{suffix}"
|
||||
dest = target_dir / f"{stem}_{counter}{suffix}"
|
||||
|
||||
dest.write_bytes(content)
|
||||
_record_file_action(dest.name, "input", len(content), "upload", current_user.get("username"))
|
||||
_record_file_action(dest.name, target, len(content), "upload", current_user.get("username"))
|
||||
|
||||
return UploadResponse(
|
||||
filename=dest.name,
|
||||
@@ -154,6 +162,10 @@ async def delete_file(
|
||||
size = file_path.stat().st_size
|
||||
file_path.unlink()
|
||||
_record_file_action(filename, directory, size, "delete", current_user.get("username"))
|
||||
|
||||
# Cascade: clean up relation table
|
||||
_cleanup_relation_for_deleted_file(directory, filename)
|
||||
|
||||
return {"message": f"已删除 {filename}"}
|
||||
|
||||
|
||||
@@ -202,3 +214,88 @@ async def get_file_stats(
|
||||
):
|
||||
"""Return file storage statistics per directory."""
|
||||
return {"directories": query_file_stats()}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File relations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class RelationDeleteRequest(BaseModel):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
def _cleanup_relation_for_deleted_file(directory: str, filename: str):
|
||||
"""Clean up relation table when a file is deleted."""
|
||||
import sqlite3
|
||||
from ..services.db_schema import _db_path
|
||||
try:
|
||||
conn = sqlite3.connect(_db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
if directory == "input":
|
||||
row = conn.execute("SELECT id FROM file_relations WHERE input_image = ?", (filename,)).fetchone()
|
||||
if row:
|
||||
conn.execute("UPDATE file_relations SET input_image = NULL, updated_at = datetime('now') WHERE id = ?", (row['id'],))
|
||||
# Delete if no other fields
|
||||
check = conn.execute("SELECT * FROM file_relations WHERE id = ?", (row['id'],)).fetchone()
|
||||
if check and not check['output_excel'] and not check['result_purchase']:
|
||||
conn.execute("DELETE FROM file_relations WHERE id = ?", (row['id'],))
|
||||
elif directory == "output":
|
||||
row = conn.execute("SELECT id FROM file_relations WHERE output_excel = ?", (filename,)).fetchone()
|
||||
if row:
|
||||
conn.execute("UPDATE file_relations SET output_excel = NULL, updated_at = datetime('now') WHERE id = ?", (row['id'],))
|
||||
check = conn.execute("SELECT * FROM file_relations WHERE id = ?", (row['id'],)).fetchone()
|
||||
if check and not check['input_image'] and not check['result_purchase']:
|
||||
conn.execute("DELETE FROM file_relations WHERE id = ?", (row['id'],))
|
||||
elif directory == "result":
|
||||
row = conn.execute("SELECT id FROM file_relations WHERE result_purchase = ?", (filename,)).fetchone()
|
||||
if row:
|
||||
conn.execute("UPDATE file_relations SET result_purchase = NULL, updated_at = datetime('now') WHERE id = ?", (row['id'],))
|
||||
check = conn.execute("SELECT * FROM file_relations WHERE id = ?", (row['id'],)).fetchone()
|
||||
if check and not check['input_image'] and not check['output_excel']:
|
||||
conn.execute("DELETE FROM file_relations WHERE id = ?", (row['id'],))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
logger.debug("Failed to cleanup relation for %s/%s", directory, filename, exc_info=True)
|
||||
|
||||
|
||||
@router.get("/relations")
|
||||
async def get_file_relations(
|
||||
view: Optional[str] = Query(None, pattern="^(orders|tables|images)$"),
|
||||
status: Optional[str] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Query file relations with optional view filter."""
|
||||
items, total = query_file_relations(view=view, status=status, page=page, page_size=page_size)
|
||||
return {"items": items, "total": total}
|
||||
|
||||
|
||||
@router.get("/stats/detailed")
|
||||
async def get_detailed_stats(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get detailed file statistics for Dashboard."""
|
||||
return query_file_relations_stats()
|
||||
|
||||
|
||||
@router.post("/relations/sync")
|
||||
async def sync_relations(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Scan directories and rebuild file_relations table."""
|
||||
sync_file_relations()
|
||||
return {"message": "文件关系表已重建"}
|
||||
|
||||
|
||||
@router.delete("/relations")
|
||||
async def delete_relations(
|
||||
body: RelationDeleteRequest,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Delete file relation records by IDs."""
|
||||
delete_file_relations(body.ids)
|
||||
return {"message": f"已删除 {len(body.ids)} 条关系记录"}
|
||||
|
||||
Reference in New Issue
Block a user