feat: add template upload endpoint

This commit is contained in:
zwt13703
2026-07-01 20:27:15 +08:00
parent beec8cf5bb
commit 95c507536a
6 changed files with 82 additions and 15 deletions
+35
View File
@@ -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",
}
+4
View File
@@ -1,6 +1,8 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from .api.templates import router as templates_router
app = FastAPI(title="AI 文档模板生成系统", version="0.1.0") app = FastAPI(title="AI 文档模板生成系统", version="0.1.0")
app.add_middleware( app.add_middleware(
@@ -11,6 +13,8 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(templates_router)
@app.get("/health") @app.get("/health")
def health(): def health():
+4 -11
View File
@@ -1,9 +1,6 @@
import os import os
from io import BytesIO from io import BytesIO
from typing import Any
from minio import Minio
from minio.error import S3Error
STORAGE_BASE = os.getenv("LOCAL_STORAGE_PATH", "./data/files") STORAGE_BASE = os.getenv("LOCAL_STORAGE_PATH", "./data/files")
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000")
@@ -29,7 +26,9 @@ def _read_local(bucket: str, file_path: str) -> bytes:
return f.read() return f.read()
def get_minio_client() -> Minio: def get_minio_client() -> Any:
from minio import Minio
return Minio( return Minio(
MINIO_ENDPOINT, MINIO_ENDPOINT,
access_key=MINIO_ACCESS_KEY, access_key=MINIO_ACCESS_KEY,
@@ -45,8 +44,6 @@ def ensure_bucket(bucket_name: str) -> bool:
if not client.bucket_exists(bucket_name): if not client.bucket_exists(bucket_name):
client.make_bucket(bucket_name) client.make_bucket(bucket_name)
return True return True
except S3Error:
return False
except Exception: except Exception:
return False 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", content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
) )
return f"minio://{bucket}/{file_path}" return f"minio://{bucket}/{file_path}"
except S3Error:
pass
except Exception: except Exception:
pass pass
@@ -83,8 +78,6 @@ def download_file(bucket: str, file_path: str) -> bytes:
finally: finally:
response.close() response.close()
response.release_conn() response.release_conn()
except S3Error:
pass
except Exception: except Exception:
pass pass
+24
View File
@@ -1,15 +1,24 @@
"""模板相关业务逻辑""" """模板相关业务逻辑"""
from datetime import datetime
from pathlib import Path
from uuid import uuid4
from fastapi import UploadFile from fastapi import UploadFile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..models.template import Template from ..models.template import Template
from .storage import upload_file
ALLOWED_EXTENSIONS = {".docx"} ALLOWED_EXTENSIONS = {".docx"}
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
TEMPLATE_BUCKET = "templates"
def validate_file(file: UploadFile): def validate_file(file: UploadFile):
"""校验文件格式和大小""" """校验文件格式和大小"""
if not file.filename:
raise ValueError("文件名不能为空")
ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else "" ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
if ext not in ALLOWED_EXTENSIONS: if ext not in ALLOWED_EXTENSIONS:
raise ValueError(f"不支持的文件格式: {ext},仅支持 .docx") raise ValueError(f"不支持的文件格式: {ext},仅支持 .docx")
@@ -21,6 +30,21 @@ def validate_file(file: UploadFile):
return content 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: 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) tmpl = Template(name=name, type=type, original_file_path=file_path)
@@ -27,10 +27,10 @@
| 013 | 创建 template_block 表 + SQLAlchemy Model | 后端 | 0.25d | ✅ 已完成 | | 013 | 创建 template_block 表 + SQLAlchemy Model | 后端 | 0.25d | ✅ 已完成 |
| 014 | 创建 block_config 表 + SQLAlchemy Model(含 unique 约束) | 后端 | 0.25d | ✅ 已完成 | | 014 | 创建 block_config 表 + SQLAlchemy Model(含 unique 约束) | 后端 | 0.25d | ✅ 已完成 |
| | **模板上传** | | | | | | **模板上传** | | | |
| 015 | 实现文件接收 + .docx 格式校验 + 大小校验 | 后端 | 0.25d | ⏳ 未完成 | | 015 | 实现文件接收 + .docx 格式校验 + 大小校验 | 后端 | 0.25d | ✅ 已完成 |
| 016 | 实现文件存储到 MinIO | 后端 | 0.25d | ⏳ 未完成 | | 016 | 实现文件存储到 MinIO | 后端 | 0.25d | ✅ 已完成 |
| 017 | 实现 template 表 insert | 后端 | 0.25d | ⏳ 未完成 | | 017 | 实现 template 表 insert | 后端 | 0.25d | ✅ 已完成 |
| 018 | 组装上传接口 `POST /api/templates/upload` | 后端 | 0.25d | ⏳ 未完成 | | 018 | 组装上传接口 `POST /api/templates/upload` | 后端 | 0.25d | ✅ 已完成 |
| | **Word 解析** | | | | | | **Word 解析** | | | |
| 019 | 实现 docx 文件打开 + 逐段落遍历 | 后端 | 0.25d | ⏳ 未完成 | | 019 | 实现 docx 文件打开 + 逐段落遍历 | 后端 | 0.25d | ⏳ 未完成 |
| 020 | 实现标题识别(Heading 1-6)→ type=heading | 后端 | 0.25d | ⏳ 未完成 | | 020 | 实现标题识别(Heading 1-6)→ type=heading | 后端 | 0.25d | ⏳ 未完成 |
+11
View File
@@ -12,3 +12,14 @@
6. 新增基础模板中心页面,用于验证 Ant Design Vue 按钮、路由渲染、Pinia 状态和全局样式变量接入。 6. 新增基础模板中心页面,用于验证 Ant Design Vue 按钮、路由渲染、Pinia 状态和全局样式变量接入。
7. 执行后端 Python 语法检查、前端依赖安装、前端生产构建与 Vite 服务可达性检查。 7. 执行后端 Python 语法检查、前端依赖安装、前端生产构建与 Vite 服务可达性检查。
- **执行结果**: 完成任务 003、004、006、007、008、009、010、011 的基础实现;`npm run build` 通过,`http://localhost:5173/` 返回 200,前端开发服务器已启动。 - **执行结果**: 完成任务 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 已在任务清单中标注为已完成。