diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..8d9f1eb --- /dev/null +++ b/app/database.py @@ -0,0 +1,15 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from .config import settings + +engine = create_engine(settings.database_url) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# 获取数据库连接的依赖项 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py index ed51cae..66bec8f 100644 --- a/app/main.py +++ b/app/main.py @@ -1,58 +1,22 @@ import uuid -import boto3 from fastapi import FastAPI, HTTPException, Depends -from sqlalchemy import create_engine, Column, String, ForeignKey -from sqlalchemy.orm import sessionmaker, Session, declarative_base, relationship +from sqlalchemy.orm import Session -from app.config import settings - -# --- 数据库初始化 --- -engine = create_engine(settings.database_url) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() - -# --- S3 客户端初始化 --- -s3_client = boto3.client( - 's3', - aws_access_key_id=settings.s3_access_key, - aws_secret_access_key=settings.s3_secret_key, - endpoint_url=settings.s3_endpoint, - region_name=settings.s3_region -) - -# --- 数据模型 --- -class Project(Base): - __tablename__ = "projects" - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - name = Column(String, nullable=False) - # cascade="all, delete-orphan" 确保在 db.delete(project) 时自动删除关联的 Markdown 记录 - markdowns = relationship("Markdown", back_populates="project", cascade="all, delete-orphan") - -class Markdown(Base): - __tablename__ = "markdowns" - id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - title = Column(String, nullable=False) - s3_key = Column(String, nullable=False) - project_id = Column(String, ForeignKey("projects.id", ondelete="CASCADE")) - project = relationship("Project", back_populates="markdowns") +from .database import engine, Base, get_db +from .models import Project, Markdown +from .s3 import upload_to_s3, delete_from_s3 +# 启动时创建表 Base.metadata.create_all(bind=engine) -app = FastAPI() - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -# --- API 接口 --- +app = FastAPI(title="AI Notion Backend") @app.get("/health") def health(): return {"status": "ok"} +# --- Project 接口 --- + @app.post("/projects") def create_project(name: str, db: Session = Depends(get_db)): project = Project(name=name) @@ -61,43 +25,40 @@ def create_project(name: str, db: Session = Depends(get_db)): db.refresh(project) return project -@app.post("/projects/{project_id}/markdowns") -def upload_markdown(project_id: str, title: str, content: str, db: Session = Depends(get_db)): - project = db.query(Project).filter(Project.id == project_id).first() - if not project: - raise HTTPException(status_code=404, detail="Project not found") - - # 1. 存入 S3 - s3_key = f"projects/{project_id}/{uuid.uuid4()}.md" - s3_client.put_object( - Bucket=settings.s3_bucket_name, - Key=s3_key, - Body=content, - ContentType="text/markdown" - ) - - # 2. 存入数据库 - md_record = Markdown(title=title, s3_key=s3_key, project_id=project_id) - db.add(md_record) - db.commit() - return {"id": md_record.id, "s3_key": s3_key} - @app.delete("/projects/{project_id}") def delete_project(project_id: str, db: Session = Depends(get_db)): project = db.query(Project).filter(Project.id == project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") - # 1. 清理 S3 中的文件 - markdowns = db.query(Markdown).filter(Markdown.project_id == project_id).all() - if markdowns: - keys = [{'Key': md.s3_key} for md in markdowns] - s3_client.delete_objects( - Bucket=settings.s3_bucket_name, - Delete={'Objects': keys} - ) + # 1. 获取该项目下所有 Markdown 的 S3 keys 并从 S3 删除 + s3_keys = [md.s3_key for md in project.markdowns] + delete_from_s3(s3_keys) - # 2. 从数据库删除项目 (SQLAlchemy 会自动处理 markdowns 表的级联删除) + # 2. 从数据库删除项目(SQLAlchemy 会自动级联删除对应的 Markdown 记录) db.delete(project) db.commit() - return {"message": f"Project {project_id} and its files have been purged."} \ No newline at end of file + return {"message": f"Project {project_id} and associated files purged."} + +# --- Markdown 接口 --- + +@app.post("/projects/{project_id}/markdowns") +def upload_markdown(project_id: str, title: str, content: str, db: Session = Depends(get_db)): + # 验证项目是否存在 + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # 生成 S3 路径 + s3_key = f"projects/{project_id}/{uuid.uuid4()}.md" + + # 1. 业务逻辑分离:上传 S3 + upload_to_s3(s3_key, content) + + # 2. 存入数据库记录 + md_record = Markdown(title=title, s3_key=s3_key, project_id=project_id) + db.add(md_record) + db.commit() + db.refresh(md_record) + + return md_record \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..6490ea7 --- /dev/null +++ b/app/models.py @@ -0,0 +1,19 @@ +import uuid +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.orm import relationship +from .database import Base + +class Project(Base): + __tablename__ = "projects" + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + name = Column(String, nullable=False) + # 级联删除 + markdowns = relationship("Markdown", back_populates="project", cascade="all, delete-orphan") + +class Markdown(Base): + __tablename__ = "markdowns" + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + title = Column(String, nullable=False) + s3_key = Column(String, nullable=False) + project_id = Column(String, ForeignKey("projects.id", ondelete="CASCADE")) + project = relationship("Project", back_populates="markdowns") \ No newline at end of file diff --git a/app/s3.py b/app/s3.py new file mode 100644 index 0000000..575d573 --- /dev/null +++ b/app/s3.py @@ -0,0 +1,26 @@ +import boto3 +from .config import settings + +s3_client = boto3.client( + 's3', + aws_access_key_id=settings.s3_access_key, + aws_secret_access_key=settings.s3_secret_key, + endpoint_url=settings.s3_endpoint, + region_name=settings.s3_region +) + +def upload_to_s3(key: str, content: str): + s3_client.put_object( + Bucket=settings.s3_bucket_name, + Key=key, + Body=content, + ContentType="text/markdown" + ) + +def delete_from_s3(keys: list[str]): + if not keys: + return + s3_client.delete_objects( + Bucket=settings.s3_bucket_name, + Delete={'Objects': [{'Key': k} for k in keys]} + ) \ No newline at end of file