From 95c507536a7e0694df8d293180f952627d872329 Mon Sep 17 00:00:00 2001 From: zwt13703 Date: Wed, 1 Jul 2026 20:27:15 +0800 Subject: [PATCH] feat: add template upload endpoint --- backend/app/api/templates.py | 35 +++++++++++++++++++ backend/app/main.py | 4 +++ backend/app/services/storage.py | 15 +++----- backend/app/services/template_service.py | 24 +++++++++++++ .../AI文档模板系统-像素级任务清单-20260701.md | 8 ++--- docs/tasks/task_detail_2026_07_01.md | 11 ++++++ 6 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 backend/app/api/templates.py diff --git a/backend/app/api/templates.py b/backend/app/api/templates.py new file mode 100644 index 0000000..b44df7c --- /dev/null +++ b/backend/app/api/templates.py @@ -0,0 +1,35 @@ +from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile +from sqlalchemy.orm import Session + +from ..database import get_db +from ..services.template_service import create_template_record, store_template_file + + +router = APIRouter(prefix="/api/templates", tags=["templates"]) + + +@router.post("/upload") +def upload_template( + file: UploadFile = File(...), + name: str = Form(...), + type: str = Form("report"), + db: Session = Depends(get_db), +): + """上传 Word 模板并创建模板记录。""" + try: + stored_path, _ = store_template_file(file) + template = create_template_record(db, name=name, file_path=stored_path, type=type) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: + db.rollback() + raise HTTPException(status_code=500, detail="模板上传失败") from exc + + return { + "data": { + "template_id": template.id, + "name": template.name, + "status": "uploaded", + }, + "message": "ok", + } diff --git a/backend/app/main.py b/backend/app/main.py index e7a98c8..8d3b017 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from .api.templates import router as templates_router + app = FastAPI(title="AI 文档模板生成系统", version="0.1.0") app.add_middleware( @@ -11,6 +13,8 @@ app.add_middleware( allow_headers=["*"], ) +app.include_router(templates_router) + @app.get("/health") def health(): diff --git a/backend/app/services/storage.py b/backend/app/services/storage.py index ef95c12..1b56f11 100644 --- a/backend/app/services/storage.py +++ b/backend/app/services/storage.py @@ -1,9 +1,6 @@ import os from io import BytesIO - -from minio import Minio -from minio.error import S3Error - +from typing import Any STORAGE_BASE = os.getenv("LOCAL_STORAGE_PATH", "./data/files") MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") @@ -29,7 +26,9 @@ def _read_local(bucket: str, file_path: str) -> bytes: return f.read() -def get_minio_client() -> Minio: +def get_minio_client() -> Any: + from minio import Minio + return Minio( MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, @@ -45,8 +44,6 @@ def ensure_bucket(bucket_name: str) -> bool: if not client.bucket_exists(bucket_name): client.make_bucket(bucket_name) return True - except S3Error: - return False except Exception: return False @@ -64,8 +61,6 @@ def upload_file(bucket: str, file_path: str, content: bytes) -> str: content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) return f"minio://{bucket}/{file_path}" - except S3Error: - pass except Exception: pass @@ -83,8 +78,6 @@ def download_file(bucket: str, file_path: str) -> bytes: finally: response.close() response.release_conn() - except S3Error: - pass except Exception: pass diff --git a/backend/app/services/template_service.py b/backend/app/services/template_service.py index ca65e43..3591673 100644 --- a/backend/app/services/template_service.py +++ b/backend/app/services/template_service.py @@ -1,15 +1,24 @@ """模板相关业务逻辑""" +from datetime import datetime +from pathlib import Path +from uuid import uuid4 + from fastapi import UploadFile from sqlalchemy.orm import Session from ..models.template import Template +from .storage import upload_file ALLOWED_EXTENSIONS = {".docx"} MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB +TEMPLATE_BUCKET = "templates" def validate_file(file: UploadFile): """校验文件格式和大小""" + if not file.filename: + raise ValueError("文件名不能为空") + ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else "" if ext not in ALLOWED_EXTENSIONS: raise ValueError(f"不支持的文件格式: {ext},仅支持 .docx") @@ -21,6 +30,21 @@ def validate_file(file: UploadFile): return content +def build_template_file_path(filename: str) -> str: + """生成模板文件存储路径。""" + suffix = Path(filename).suffix.lower() + today = datetime.now().strftime("%Y%m%d") + return f"{today}/{uuid4().hex}{suffix}" + + +def store_template_file(file: UploadFile) -> tuple[str, bytes]: + """校验并保存上传的模板文件,返回存储路径和文件内容。""" + content = validate_file(file) + file_path = build_template_file_path(file.filename or "template.docx") + stored_path = upload_file(TEMPLATE_BUCKET, file_path, content) + return stored_path, content + + def create_template_record(db: Session, name: str, file_path: str, type: str = "report") -> Template: """创建模板记录""" tmpl = Template(name=name, type=type, original_file_path=file_path) diff --git a/docs/AI文档模板系统-像素级任务清单-20260701.md b/docs/AI文档模板系统-像素级任务清单-20260701.md index 5254631..01881e2 100644 --- a/docs/AI文档模板系统-像素级任务清单-20260701.md +++ b/docs/AI文档模板系统-像素级任务清单-20260701.md @@ -27,10 +27,10 @@ | 013 | 创建 template_block 表 + SQLAlchemy Model | 后端 | 0.25d | ✅ 已完成 | | 014 | 创建 block_config 表 + SQLAlchemy Model(含 unique 约束) | 后端 | 0.25d | ✅ 已完成 | | | **模板上传** | | | | -| 015 | 实现文件接收 + .docx 格式校验 + 大小校验 | 后端 | 0.25d | ⏳ 未完成 | -| 016 | 实现文件存储到 MinIO | 后端 | 0.25d | ⏳ 未完成 | -| 017 | 实现 template 表 insert | 后端 | 0.25d | ⏳ 未完成 | -| 018 | 组装上传接口 `POST /api/templates/upload` | 后端 | 0.25d | ⏳ 未完成 | +| 015 | 实现文件接收 + .docx 格式校验 + 大小校验 | 后端 | 0.25d | ✅ 已完成 | +| 016 | 实现文件存储到 MinIO | 后端 | 0.25d | ✅ 已完成 | +| 017 | 实现 template 表 insert | 后端 | 0.25d | ✅ 已完成 | +| 018 | 组装上传接口 `POST /api/templates/upload` | 后端 | 0.25d | ✅ 已完成 | | | **Word 解析** | | | | | 019 | 实现 docx 文件打开 + 逐段落遍历 | 后端 | 0.25d | ⏳ 未完成 | | 020 | 实现标题识别(Heading 1-6)→ type=heading | 后端 | 0.25d | ⏳ 未完成 | diff --git a/docs/tasks/task_detail_2026_07_01.md b/docs/tasks/task_detail_2026_07_01.md index 016f750..1d222ee 100644 --- a/docs/tasks/task_detail_2026_07_01.md +++ b/docs/tasks/task_detail_2026_07_01.md @@ -12,3 +12,14 @@ 6. 新增基础模板中心页面,用于验证 Ant Design Vue 按钮、路由渲染、Pinia 状态和全局样式变量接入。 7. 执行后端 Python 语法检查、前端依赖安装、前端生产构建与 Vite 服务可达性检查。 - **执行结果**: 完成任务 003、004、006、007、008、009、010、011 的基础实现;`npm run build` 通过,`http://localhost:5173/` 返回 200,前端开发服务器已启动。 + +## 会话 ID: 20260701-upload-api +- [2026-07-01 20:26:57] +- **执行原因**: 用户要求将已完成任务在任务列表标注清楚、提交代码,并继续逐个完成后续任务。 +- **执行过程**: + 1. 将任务清单总表增加状态列,标注 001-014 为已完成,其余任务为未完成。 + 2. 提交基础设施与前端初始化成果,提交号为 `beec8cf`。 + 3. 继续实现 015-018,新增模板上传 API 路由并挂载到 FastAPI 应用。 + 4. 复用并完善文件校验、MinIO/本地降级存储与 template 表插入逻辑。 + 5. 执行 Python 语法检查与 FastAPI 路由导入检查,确认 `/api/templates/upload` 已注册。 +- **执行结果**: 完成模板上传接口基础链路,任务 015-018 已在任务清单中标注为已完成。