init project

This commit is contained in:
zwt13703
2026-07-01 19:50:29 +08:00
commit 685d69ebdc
21 changed files with 1002 additions and 0 deletions
View File
View File
+26
View File
@@ -0,0 +1,26 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_USER = os.getenv("DB_USER", "root")
DB_PASS = os.getenv("DB_PASS", "root123")
DB_NAME = os.getenv("DB_NAME", "ai_doc_template")
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
+17
View File
@@ -0,0 +1,17 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="AI 文档模板生成系统", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health():
return {"status": "ok", "service": "ai-doc-template"}
View File
+24
View File
@@ -0,0 +1,24 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint
from ..database import Base
class BlockConfig(Base):
__tablename__ = "block_config"
__table_args__ = (
UniqueConstraint("template_id", "block_id", name="uq_template_block"),
)
id = Column(Integer, primary_key=True, autoincrement=True)
template_id = Column(Integer, ForeignKey("template.id", ondelete="CASCADE"), nullable=False)
block_id = Column(String(50), nullable=False)
region_name = Column(String(200), nullable=True, comment="区域名称")
region_type = Column(String(30), default="ai_generate", comment="ai_generate/manual/fixed/table")
data_sources = Column(Text, nullable=True, comment="数据来源 JSON 数组")
prompt = Column(Text, nullable=True, comment="提示词")
output_format = Column(String(30), default="formal_paragraph", comment="输出格式")
need_review = Column(Integer, default=1, comment="1=需要审核")
remark = Column(Text, nullable=True, comment="备注")
enabled = Column(Integer, default=1, comment="1=启用")
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+17
View File
@@ -0,0 +1,17 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from ..database import Base
class Template(Base):
__tablename__ = "template"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False, comment="模板名称")
type = Column(String(50), default="report", comment="报告类/公文类/总结类/合同类")
version = Column(String(20), default="v1.0.0", comment="版本号")
original_file_path = Column(String(500), nullable=False, comment="原始 Word 文件路径")
status = Column(Integer, default=1, comment="1=启用 0=停用")
created_by = Column(String(50), comment="创建人")
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+20
View File
@@ -0,0 +1,20 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from ..database import Base
class TemplateBlock(Base):
__tablename__ = "template_block"
id = Column(Integer, primary_key=True, autoincrement=True)
template_id = Column(Integer, ForeignKey("template.id", ondelete="CASCADE"), nullable=False, comment="所属模板")
block_id = Column(String(50), nullable=False, comment="唯一标识 block_001")
parent_block_id = Column(String(50), nullable=True, comment="父级 block_id")
block_type = Column(String(20), nullable=False, comment="title/heading/paragraph/table")
block_name = Column(String(200), nullable=True, comment="区域名称")
text_preview = Column(String(500), nullable=True, comment="文本预览")
level = Column(Integer, default=0, comment="0=文档标题 1=一级 2=二级")
sort_order = Column(Integer, default=0, comment="排序号")
table_rows = Column(Integer, nullable=True, comment="表格行数")
table_cols = Column(Integer, nullable=True, comment="表格列数")
created_at = Column(DateTime, default=datetime.now)
View File
View File
+86
View File
@@ -0,0 +1,86 @@
"""Word 文档解析引擎"""
from docx import Document
from docx.paragraph import Paragraph
from docx.table import Table as DocxTable
from typing import List, Dict, Any
def parse_document(file_path: str) -> List[Dict[str, Any]]:
"""
解析 Word 文档,返回 block 列表(不含表格数据,仅标记位置)。
每个 block 包含:type, text, level, style
"""
doc = Document(file_path)
blocks = []
for para in doc.paragraphs:
block = classify_paragraph(para)
if block:
blocks.append(block)
return blocks
def classify_paragraph(para: Paragraph) -> Dict[str, Any] | None:
"""识别段落类型"""
text = para.text.strip()
style_name = para.style.name if para.style else ""
if not text and "Heading" not in style_name:
return None # 跳过空段落
if "Heading" in style_name:
level = int(style_name.replace("Heading", "").strip()) if style_name.replace("Heading", "").strip().isdigit() else 1
return {"type": "heading", "text": text, "level": level, "style": style_name}
else:
return {"type": "paragraph", "text": text, "level": 0, "style": style_name}
def parse_tables(doc: Document) -> List[Dict[str, Any]]:
"""解析 Word 表格"""
tables = []
for i, table in enumerate(doc.tables):
headers = [cell.text.strip() for cell in table.rows[0].cells] if table.rows else []
tables.append({
"type": "table",
"text": " | ".join(headers),
"level": 0,
"rows": len(table.rows),
"cols": len(table.columns),
"headers": headers,
"table_index": i,
})
return tables
def generate_block_id(index: int) -> str:
"""生成 block_id,格式:block_001"""
return f"block_{index:03d}"
def build_tree(blocks: List[Dict]) -> Dict:
"""构建层级树结构"""
tree = []
stack = [] # 存父级节点
for block in blocks:
node = {"block_id": block["block_id"], "text": block["text"][:50], "children": []}
level = block.get("level", 0)
# 回退栈
while stack and stack[-1]["level"] >= level:
stack.pop()
if stack:
parent = stack[-1]["node"]
parent["children"].append(node)
block["parent_block_id"] = parent["block_id"]
else:
tree.append(node)
block["parent_block_id"] = None
# 非叶子节点入栈
if level < 2 or block["type"] == "heading":
stack.append({"level": level, "node": node})
return {"blocks": tree}
+27
View File
@@ -0,0 +1,27 @@
"""HTML 预览生成服务"""
from typing import List, Dict, Any
def generate_preview_html(blocks: List[Dict[str, Any]]) -> str:
"""
将 block 列表转为带 data-block-id 的 HTML 预览字符串。
每个元素都会带上 data-block-id 属性供前端点击交互。
"""
html_parts = ['<div class="doc-preview-content">']
for block in blocks:
block_id = block.get("block_id", "")
block_type = block.get("type", "")
text = block.get("text", "")
if block_type == "heading":
html = f'<h2 data-block-id="{block_id}" style="font-size:16px;font-weight:600;margin:20px 0 10px;padding-bottom:6px;border-bottom:2px solid #1a1d24">{text}</h2>'
elif block_type == "table":
html = f'<div data-block-id="{block_id}" style="margin:12px 0;padding:8px;background:#f7f8fa;border:1px dashed #ccc;border-radius:4px;color:#5b626e">📊 {text[:80]}</div>'
else:
html = f'<p data-block-id="{block_id}" style="text-indent:2em;margin:8px 0;line-height:1.8;text-align:justify">{text}</p>'
html_parts.append(html)
html_parts.append('</div>')
return "\n".join(html_parts)
+28
View File
@@ -0,0 +1,28 @@
import os
from io import BytesIO
# 先实现本地文件存储,MinIO 作为选项
STORAGE_BASE = os.getenv("LOCAL_STORAGE_PATH", "./data/files")
def save_file(content: bytes, file_path: str) -> str:
"""保存文件到本地存储,返回完整路径"""
full_path = os.path.join(STORAGE_BASE, file_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "wb") as f:
f.write(content)
return full_path
def read_file(file_path: str) -> bytes:
"""读取文件内容"""
full_path = os.path.join(STORAGE_BASE, file_path)
with open(full_path, "rb") as f:
return f.read()
def delete_file(file_path: str):
"""删除文件"""
full_path = os.path.join(STORAGE_BASE, file_path)
if os.path.exists(full_path):
os.remove(full_path)
+30
View File
@@ -0,0 +1,30 @@
"""模板相关业务逻辑"""
from fastapi import UploadFile
from sqlalchemy.orm import Session
from ..models.template import Template
ALLOWED_EXTENSIONS = {".docx"}
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
def validate_file(file: UploadFile):
"""校验文件格式和大小"""
ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
if ext not in ALLOWED_EXTENSIONS:
raise ValueError(f"不支持的文件格式: {ext},仅支持 .docx")
# 读取文件头校验大小
content = file.file.read()
file.file.seek(0)
if len(content) > MAX_FILE_SIZE:
raise ValueError(f"文件大小超过限制 (50MB)")
return 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)
db.add(tmpl)
db.commit()
db.refresh(tmpl)
return tmpl