feat: add template upload endpoint
This commit is contained in:
@@ -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",
|
||||||
|
}
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 | ⏳ 未完成 |
|
||||||
|
|||||||
@@ -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 已在任务清单中标注为已完成。
|
||||||
|
|||||||
Reference in New Issue
Block a user