Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from windup_app.server.project.model import Project # noqa: F401
from windup_app.server.user.model import User # noqa: F401
from windup_app.web.api.auth import router as auth_router
from windup_app.web.api.character import router as character_router
from windup_app.web.api.generation import router as generation_router
from windup_app.web.api.media import router as media_router
from windup_app.web.api.project import router as project_router
from windup_app.web.handler.exception_handlers import register_exception_handlers
from windup_app.web.middleware.auth import AuthMiddleware
from windup_app.web.middleware.ratelimit import RateLimitMiddleware
Expand Down Expand Up @@ -81,6 +83,8 @@ def create_app() -> FastAPI:
app.add_middleware(AuthMiddleware)
app.add_middleware(RateLimitMiddleware)
app.include_router(auth_router)
app.include_router(project_router)
app.include_router(character_router)
app.include_router(media_router)
app.include_router(generation_router)
register_exception_handlers(app)
Expand Down
6 changes: 4 additions & 2 deletions backend/packages/app/src/windup_app/server/character/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from datetime import datetime, timezone

from pydantic import BaseModel, Field
from sqlalchemy import BigInteger, DateTime, Integer, JSON, SmallInteger, Text
from sqlalchemy import BigInteger, DateTime, Integer, JSON, SmallInteger, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

Expand All @@ -60,7 +60,9 @@ class Character(Base):

project_id: Mapped[int] = mapped_column(BigInteger, nullable=False)

workflow_run_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
workflow_run_id: Mapped[int] = mapped_column(BigInteger, nullable=False)

name: Mapped[str | None] = mapped_column(String(20), nullable=True)

description: Mapped[str | None] = mapped_column(Text, nullable=True)

Expand Down
69 changes: 69 additions & 0 deletions backend/packages/app/src/windup_app/server/character/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""角色领域服务的 SQLAlchemy 实现。

:class:`SqlAlchemyCharacterService` 继承 :class:`CharacterService` 接口,用同步
SQLAlchemy session 落库。无状态:``session`` 由调用方按请求传入,本对象可作
模块级单例(:data:`service`)。

事务边界由 ``windup_framework.db.get_session`` 依赖负责--成功 commit、异常
rollback,故本实现只 ``flush``(把变更发到当前事务、取回生成的主键),不 commit。
"""

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from windup_app.server.character.interface import CharacterService
from windup_app.server.character.model import Character


class SqlAlchemyCharacterService(CharacterService):
"""基于 SQLAlchemy session 的角色 CRUD 实现。"""

def create_character(self, session: Session, **fields) -> Character:
character = Character(**fields)
session.add(character)
session.flush()
return character

def get_character(self, session: Session, character_id: int) -> Character | None:
return session.get(Character, character_id)

def list_characters(
self, session: Session, *, project_id: int, page: int, page_size: int,
) -> tuple[list[Character], int]:
count_stmt = (
select(func.count())
.select_from(Character)
.where(Character.project_id == project_id)
)
stmt = (
select(Character)
.where(Character.project_id == project_id)
.order_by(Character.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
total = session.scalar(count_stmt) or 0
items = list(session.scalars(stmt))
return items, total

def update_character(
self, session: Session, character_id: int, **fields,
) -> Character | None:
character = session.get(Character, character_id)
if character is None:
return None
for key, value in fields.items():
setattr(character, key, value)
session.flush()
return character

def delete_character(self, session: Session, character_id: int) -> bool:
character = session.get(Character, character_id)
if character is None:
return False
session.delete(character)
session.flush()
return True


service = SqlAlchemyCharacterService()
24 changes: 17 additions & 7 deletions backend/packages/app/src/windup_app/server/project/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,44 @@

项目 API 只依赖本模块定义的抽象接口。数据库、缓存或其他具体实现应在
应用装配层继承 :class:`ProjectService` 后通过依赖注入提供。

约定为 session-per-call:``session`` 由调用方(FastAPI 的 ``get_session`` 依赖)
按请求传入,具体实现(如 :mod:`windup_app.server.project.service`)保持无状态,
可作为模块级单例。
"""

from abc import ABC, abstractmethod

from sqlalchemy.orm import Session

from windup_app.server.project.model import Project


class ProjectService(ABC):
"""项目 CRUD 用例的抽象边界。"""

@abstractmethod
def create_project(self, project: Project) -> Project:
"""创建项目。"""
def create_project(self, session: Session, **fields) -> Project:
"""创建项目。

``fields`` 为项目字段(对齐 ``ProjectCreate`` 的字段集),由实现组装成
:class:`Project` 后持久化。
"""

@abstractmethod
def project_name_exists(self, *, user_id: int, project_name: str) -> bool:
def project_name_exists(self, session: Session, *, user_id: int, project_name: str) -> bool:
"""判断用户下的项目名称是否已存在。"""

@abstractmethod
def get_project(self, project_id: int) -> Project | None:
def get_project(self, session: Session, project_id: int) -> Project | None:
"""按 ID 查询项目。"""

@abstractmethod
def list_projects(
self, *, page: int, page_size: int, user_id: int | None = None
self, session: Session, *, page: int, page_size: int, user_id: int | None = None
) -> tuple[list[Project], int]:
"""分页查询项目。"""
"""分页查询项目,返回 (当前页数据, 总数)。"""

@abstractmethod
def delete_project(self, project_id: int) -> bool:
def delete_project(self, session: Session, project_id: int) -> bool:
"""删除项目并返回是否找到。"""
10 changes: 8 additions & 2 deletions backend/packages/app/src/windup_app/server/project/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from datetime import datetime, timezone

from sqlalchemy import BigInteger, DateTime, SmallInteger, String, Text, UniqueConstraint
from sqlalchemy import BigInteger, DateTime, Integer, SmallInteger, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column

from windup_framework.db import Base
Expand All @@ -16,7 +16,13 @@ class Project(Base):
UniqueConstraint("user_id", "project_name", name="uq_windup_project_user_name"),
)

id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
# Postgres 上 BigInteger 自增;variant 到 Integer 让 SQLite(测试库)走
# INTEGER PRIMARY KEY 自增(SQLite 仅对该声明自动分配 rowid)。
id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
primary_key=True,
autoincrement=True,
)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
workflow_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
project_name: Mapped[str] = mapped_column(String(20), nullable=False)
Expand Down
60 changes: 60 additions & 0 deletions backend/packages/app/src/windup_app/server/project/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""项目领域服务的 SQLAlchemy 实现。

:class:`SqlAlchemyProjectService` 继承 :class:`ProjectService` 接口,用同步
SQLAlchemy session 落库。无状态:``session`` 由调用方按请求传入,本对象可作
模块级单例(:data:`service`)。

事务边界由 ``windup_framework.db.get_session`` 依赖负责--成功 commit、异常
rollback,故本实现只 ``flush``(把变更发到当前事务、取回生成的主键),不 commit。
"""

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from windup_app.server.project.interface import ProjectService
from windup_app.server.project.model import Project


class SqlAlchemyProjectService(ProjectService):
"""基于 SQLAlchemy session 的项目 CRUD 实现。"""

def create_project(self, session: Session, **fields) -> Project:
project = Project(**fields)
session.add(project)
session.flush() # 取回自增主键 id 与 Python 侧默认值(create_at/update_at)
return project

def project_name_exists(self, session: Session, *, user_id: int, project_name: str) -> bool:
stmt = (
select(Project.id)
.where(Project.user_id == user_id, Project.project_name == project_name)
.limit(1)
)
return session.scalar(stmt) is not None

def get_project(self, session: Session, project_id: int) -> Project | None:
return session.get(Project, project_id)

def list_projects(
self, session: Session, *, page: int, page_size: int, user_id: int | None = None
) -> tuple[list[Project], int]:
count_stmt = select(func.count()).select_from(Project)
stmt = select(Project)
if user_id is not None:
count_stmt = count_stmt.where(Project.user_id == user_id)
stmt = stmt.where(Project.user_id == user_id)
total = session.scalar(count_stmt) or 0
stmt = stmt.order_by(Project.id.desc()).offset((page - 1) * page_size).limit(page_size)
items = list(session.scalars(stmt))
return items, total

def delete_project(self, session: Session, project_id: int) -> bool:
project = session.get(Project, project_id)
if project is None:
return False
session.delete(project)
session.flush()
return True


service = SqlAlchemyProjectService()
Loading