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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ output/
.pytest_cache/
.import_linter_cache/

# 测试覆盖率
.coverage
htmlcov/

# 本地数据库初始化脚本
init.sql
2 changes: 1 addition & 1 deletion backend/packages/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies = [
"windup-ai-engine",
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"pydantic>=2.7",
"pydantic[email]>=2.7",
"sqlalchemy>=2.0",
"python-multipart>=0.0.9",
]
Expand Down
24 changes: 16 additions & 8 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@
import os
from contextlib import asynccontextmanager

import windup_framework.db # noqa: F401 组装时显式触发 DB engine/session 初始化
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from windup_framework.db import Base, engine

# 模型导入:触发 Base.metadata 注册,确保 create_all 能发现所有表
from windup_app.server.character.model import Character # noqa: F401
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.generation import router as generation_router
from windup_app.web.api.media import router as media_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


def _env_flag(name: str) -> bool:
Expand Down Expand Up @@ -54,7 +61,8 @@ def print_banner() -> None:

@asynccontextmanager
async def _lifespan(app: FastAPI):
"""应用启动时打印 banner,关闭时无特殊处理。"""
"""应用启动时建表 + 打印 banner,关闭时无特殊处理。"""
Base.metadata.create_all(engine)
print_banner()
yield

Expand All @@ -69,17 +77,18 @@ def create_app() -> FastAPI:
allow_methods=["*"],
allow_headers=["*"],
)
# 中间件(add_middleware 后加的先执行:请求先进 RateLimit → 再进 Auth → 最后到路由)
app.add_middleware(AuthMiddleware)
app.add_middleware(RateLimitMiddleware)
app.include_router(auth_router)
app.include_router(media_router)
app.include_router(generation_router)
register_exception_handlers(app)
return app


def main() -> None:
"""开发启动入口:用 uvicorn 跑 ``create_app``。

host/port/reload 可用 ``WINDUP_HOST`` / ``WINDUP_PORT`` / ``WINDUP_RELOAD`` 覆盖。
"""
"""开发启动入口:用 uvicorn 跑 ``create_app``。"""
import uvicorn

uvicorn.run(
Expand All @@ -91,6 +100,5 @@ def main() -> None:
)



if __name__ == "__main__":
main()
main()
30 changes: 13 additions & 17 deletions backend/packages/app/src/windup_app/server/user/interface.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""用户领域服务抽象接口。

API 层只依赖本模块定义的抽象,不感知具体实现(ORM / Redis / OAuth SDK)。
API 层只依赖本模块定义的抽象,不感知具体实现(ORM / Redis / Resend)。
"""

from abc import ABC, abstractmethod
Expand All @@ -11,7 +11,7 @@
LoginByPasswordInput,
LoginResult,
RegisterInput,
User,
UserView,
)


Expand All @@ -37,9 +37,10 @@ def login_by_password(self, input: LoginByPasswordInput) -> LoginResult:
"""

@abstractmethod
def send_verification_code(self, email: str) -> None:
def send_verification_code(self, email: str, purpose: str) -> None:
"""发送邮箱验证码。

:param purpose: 用途,如 "login" / "register" / "reset_password"。
:raises windup_common.exceptions.BizException: 发送频率超限。
"""

Expand All @@ -53,23 +54,20 @@ def login_by_code(self, input: LoginByCodeInput) -> LoginResult:
# -- 登出 ------------------------------------------------------------

@abstractmethod
def logout(self, session_token: str) -> None:
"""销毁会话。"""
def logout(self, refresh_token: str) -> None:
"""销毁 refresh_token。"""

# -- OAuth -----------------------------------------------------------
# 第三方认证暂不设计、不实现。保留该区域作为后续扩展占位。
# 相关 authorize / callback / bind 接口和 UserOAuth 模型暂时停用。
# -- 会话管理 ---------------------------------------------------------

@abstractmethod
def validate_session(self, session_token: str) -> User | None:
"""校验会话并返回用户,过期 / 无效返回 ``None``。"""
def validate_access_token(self, token: str) -> UserView | None:
"""校验 access_token 并返回用户,过期 / 无效返回 ``None``。"""

@abstractmethod
def refresh_session(self, session_token: str) -> str:
"""刷新会话,返回新 token;旧 token 立即失效
def refresh_tokens(self, refresh_token: str) -> LoginResult:
"""刷新 token,返回新的 access+refresh

:raises windup_common.exceptions.BizException: 会话无效
:raises windup_common.exceptions.BizException: refresh token 无效 / 已撤销
"""

# -- 密码 ------------------------------------------------------------
Expand All @@ -84,11 +82,9 @@ def change_password(self, user_id: int, input: ChangePasswordInput) -> None:
# -- 查询 ------------------------------------------------------------

@abstractmethod
def get_by_id(self, user_id: int) -> User | None:
def get_by_id(self, user_id: int) -> UserView | None:
"""按 ID 查询用户。"""

@abstractmethod
def get_by_email(self, email: str) -> User | None:
def get_by_email(self, email: str) -> UserView | None:
"""按邮箱查询用户。"""

# 第三方认证暂不设计、不实现,因此没有 OAuth 绑定查询接口。
123 changes: 83 additions & 40 deletions backend/packages/app/src/windup_app/server/user/model.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,73 @@
"""用户领域模型。

与 ``windup_user`` / ``windup_user_oauth`` 表一一对应,
字段名与数据库列名保持一致,方便后续 ORM 映射。
与 ``windup_user`` 表一一对应,字段名与数据库列名保持一致。

ORM 模型
--------

::

windup_user
├── id BigInteger PK: 自增主键
├── email String(255) UNIQUE: 邮箱
├── password_hash String(255): bcrypt 哈希
├── nickname String(50) NULL: 昵称
├── email_verified_at DateTime(tz) NULL: 邮箱验证时间
├── status SmallInteger: 0=正常, 1=封禁
├── last_login_at DateTime(tz) NULL: 最后登录
├── create_at DateTime(tz): 创建时间
└── update_at DateTime(tz): 更新时间
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import IntEnum

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

from windup_framework.db import Base


# -- ORM ----------------------------------------------------------------


class User(Base):
"""用户表。"""

__tablename__ = "windup_user"

id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
primary_key=True,
autoincrement=True,
)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False, default="")
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True)
email_verified_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
status: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
last_login_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
create_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
update_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)


# -- 枚举 ----------------------------------------------------------------


class UserStatus(IntEnum):
"""用户状态。

Expand All @@ -22,22 +79,15 @@ class UserStatus(IntEnum):
BANNED = 1


class OAuthProvider(str):
"""第三方登录平台(值约束)。"""
# -- 领域模型 ------------------------------------------------------------

GITHUB = "github"
GOOGLE = "google"


# -- 数据模型 ------------------------------------------------------------

@dataclass
class User:
"""用户(对应 ``windup_user`` 表)。"""
class UserView:
"""用户视图(脱敏,不含 password_hash)。"""

id: int | None = None
email: str | None = None
password_hash: str = ""
nickname: str | None = None
email_verified_at: datetime | None = None
status: UserStatus = UserStatus.NORMAL
Expand All @@ -54,27 +104,16 @@ def is_email_verified(self) -> bool:
return self.email_verified_at is not None


@dataclass
class UserOAuth:
"""第三方登录绑定(对应 ``windup_user_oauth`` 表)。"""

id: int | None = None
user_id: int = 0
provider: str = "" # "github" / "google"
provider_user_id: str = ""
provider_email: str | None = None
create_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
update_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))


# -- 输入/输出模型 --------------------------------------------------------


@dataclass
class RegisterInput:
"""邮箱注册入参。"""

email: str
password: str
code: str
nickname: str | None = None


Expand All @@ -95,29 +134,33 @@ class LoginByCodeInput:


@dataclass
class OAuthCallbackInput:
"""OAuth 回调入参。"""
class ChangePasswordInput:
"""修改密码入参。"""

provider: str # "github" / "google"
code: str
state: str # CSRF 防护
old_password: str
new_password: str


@dataclass
class ChangePasswordInput:
"""修改密码入参。"""
class UpdateNicknameInput:
"""修改昵称入参。"""

old_password: str
nickname: str


@dataclass
class ResetPasswordInput:
"""重置密码入参。"""

email: str
code: str
new_password: str


@dataclass
class LoginResult:
"""登录结果。

``session_token`` 由调用方通过 Set-Cookie 写入客户端;
``user`` 返回脱敏后的用户信息(不含 password_hash)。
"""
"""登录结果。"""

user: User
session_token: str
user: UserView
access_token: str
refresh_token: str
Loading