中文汉化
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled

This commit is contained in:
z66
2025-12-18 09:40:41 +08:00
parent 6a91475bf6
commit db867dcbe5
41 changed files with 671 additions and 666 deletions
+15 -19
View File
@@ -4,16 +4,15 @@ from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
# 这是 Alembic 配置对象,提供对正在使用的 .ini 文件中值的访问。
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
# 解析配置文件以进行 Python 日志记录。
# 这行代码用于设置日志记录器。
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# 在此处添加模型的 MetaData 对象
# 以支持 'autogenerate'
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None
@@ -23,10 +22,9 @@ from app.core.config import settings # noqa
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# 根据 env.py 的需要,可以从配置中获取其他值:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
# ... 等等。
def get_url():
@@ -34,15 +32,13 @@ def get_url():
def run_migrations_offline():
"""Run migrations in 'offline' mode.
""""离线"模式下运行迁移。
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
这使用 URL 而不是 Engine 来配置上下文,
尽管 Engine 在这里也是可以接受的。
通过跳过 Engine 的创建,我们甚至不需要 DBAPI 可用。
Calls to context.execute() here emit the given string to the
script output.
在这里调用 context.execute() 会将给定的字符串输出到脚本输出。
"""
url = get_url()
@@ -55,10 +51,10 @@ def run_migrations_offline():
def run_migrations_online():
"""Run migrations in 'online' mode.
""""在线"模式下运行迁移。
In this scenario we need to create an Engine
and associate a connection with the context.
在这种情况下,我们需要创建一个 Engine
并将连接与上下文关联。
"""
configuration = config.get_section(config.config_ini_section)
+4 -4
View File
@@ -36,13 +36,13 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
detail="无法验证凭据",
)
user = session.get(User, token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
raise HTTPException(status_code=404, detail="用户未找到")
if not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
raise HTTPException(status_code=400, detail="用户未激活")
return user
@@ -52,6 +52,6 @@ CurrentUser = Annotated[User, Depends(get_current_user)]
def get_current_active_superuser(current_user: CurrentUser) -> User:
if not current_user.is_superuser:
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
status_code=403, detail="用户权限不足"
)
return current_user
+12 -12
View File
@@ -15,7 +15,7 @@ def read_items(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
"""
Retrieve items.
获取 Item 列表。
"""
if current_user.is_superuser:
@@ -44,13 +44,13 @@ def read_items(
@router.get("/{id}", response_model=ItemPublic)
def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
"""
Get item by ID.
通过 ID 获取 Item。
"""
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
raise HTTPException(status_code=404, detail="项目未找到")
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
raise HTTPException(status_code=400, detail="权限不足")
return item
@@ -59,7 +59,7 @@ def create_item(
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
) -> Any:
"""
Create new item.
创建新的 Item
"""
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
@@ -77,13 +77,13 @@ def update_item(
item_in: ItemUpdate,
) -> Any:
"""
Update an item.
更新 Item
"""
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
raise HTTPException(status_code=404, detail="项目未找到")
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
raise HTTPException(status_code=400, detail="权限不足")
update_dict = item_in.model_dump(exclude_unset=True)
item.sqlmodel_update(update_dict)
session.add(item)
@@ -97,13 +97,13 @@ def delete_item(
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
) -> Message:
"""
Delete an item.
删除 Item
"""
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
raise HTTPException(status_code=404, detail="项目未找到")
if not current_user.is_superuser and (item.owner_id != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
raise HTTPException(status_code=400, detail="权限不足")
session.delete(item)
session.commit()
return Message(message="Item deleted successfully")
return Message(message="项目删除成功")
+14 -14
View File
@@ -26,15 +26,15 @@ def login_access_token(
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
"""
OAuth2 compatible token login, get an access token for future requests
OAuth2 兼容的令牌登录,获取用于后续请求的访问令牌
"""
user = crud.authenticate(
session=session, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
raise HTTPException(status_code=400, detail="邮箱或密码错误")
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
raise HTTPException(status_code=400, detail="用户未激活")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return Token(
access_token=security.create_access_token(
@@ -46,7 +46,7 @@ def login_access_token(
@router.post("/login/test-token", response_model=UserPublic)
def test_token(current_user: CurrentUser) -> Any:
"""
Test access token
测试访问令牌
"""
return current_user
@@ -54,14 +54,14 @@ def test_token(current_user: CurrentUser) -> Any:
@router.post("/password-recovery/{email}")
def recover_password(email: str, session: SessionDep) -> Message:
"""
Password Recovery
密码找回
"""
user = crud.get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this email does not exist in the system.",
detail="系统中不存在该邮箱的用户。",
)
password_reset_token = generate_password_reset_token(email=email)
email_data = generate_reset_password_email(
@@ -72,30 +72,30 @@ def recover_password(email: str, session: SessionDep) -> Message:
subject=email_data.subject,
html_content=email_data.html_content,
)
return Message(message="Password recovery email sent")
return Message(message="密码找回邮件已发送")
@router.post("/reset-password/")
def reset_password(session: SessionDep, body: NewPassword) -> Message:
"""
Reset password
重置密码
"""
email = verify_password_reset_token(token=body.token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
raise HTTPException(status_code=400, detail="无效的令牌")
user = crud.get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this email does not exist in the system.",
detail="系统中不存在该邮箱的用户。",
)
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
raise HTTPException(status_code=400, detail="用户未激活")
hashed_password = get_password_hash(password=body.new_password)
user.hashed_password = hashed_password
session.add(user)
session.commit()
return Message(message="Password updated successfully")
return Message(message="密码更新成功")
@router.post(
@@ -105,14 +105,14 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
)
def recover_password_html_content(email: str, session: SessionDep) -> Any:
"""
HTML Content for Password Recovery
获取密码找回邮件的 HTML 内容
"""
user = crud.get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
detail="系统中不存在该用户名的用户。",
)
password_reset_token = generate_password_reset_token(email=email)
email_data = generate_reset_password_email(
+1 -1
View File
@@ -23,7 +23,7 @@ class PrivateUserCreate(BaseModel):
@router.post("/users/", response_model=UserPublic)
def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any:
"""
Create a new user.
创建新用户。
"""
user = User(
+24 -24
View File
@@ -36,7 +36,7 @@ router = APIRouter(prefix="/users", tags=["users"])
)
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
"""
Retrieve users.
获取用户列表。
"""
count_statement = select(func.count()).select_from(User)
@@ -53,13 +53,13 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
)
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
"""
Create new user.
创建新用户。
"""
user = crud.get_user_by_email(session=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
detail="系统中已存在该邮箱的用户。",
)
user = crud.create_user(session=session, user_create=user_in)
@@ -80,14 +80,14 @@ def update_user_me(
*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser
) -> Any:
"""
Update own user.
更新当前用户信息。
"""
if user_in.email:
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
if existing_user and existing_user.id != current_user.id:
raise HTTPException(
status_code=409, detail="User with this email already exists"
status_code=409, detail="该邮箱已被其他用户使用"
)
user_data = user_in.model_dump(exclude_unset=True)
current_user.sqlmodel_update(user_data)
@@ -102,25 +102,25 @@ def update_password_me(
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
) -> Any:
"""
Update own password.
更新当前用户密码。
"""
if not verify_password(body.current_password, current_user.hashed_password):
raise HTTPException(status_code=400, detail="Incorrect password")
raise HTTPException(status_code=400, detail="密码错误")
if body.current_password == body.new_password:
raise HTTPException(
status_code=400, detail="New password cannot be the same as the current one"
status_code=400, detail="新密码不能与当前密码相同"
)
hashed_password = get_password_hash(body.new_password)
current_user.hashed_password = hashed_password
session.add(current_user)
session.commit()
return Message(message="Password updated successfully")
return Message(message="密码更新成功")
@router.get("/me", response_model=UserPublic)
def read_user_me(current_user: CurrentUser) -> Any:
"""
Get current user.
获取当前用户信息。
"""
return current_user
@@ -128,27 +128,27 @@ def read_user_me(current_user: CurrentUser) -> Any:
@router.delete("/me", response_model=Message)
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
"""
Delete own user.
删除当前用户。
"""
if current_user.is_superuser:
raise HTTPException(
status_code=403, detail="Super users are not allowed to delete themselves"
status_code=403, detail="超级用户不允许删除自己"
)
session.delete(current_user)
session.commit()
return Message(message="User deleted successfully")
return Message(message="用户删除成功")
@router.post("/signup", response_model=UserPublic)
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
"""
Create new user without the need to be logged in.
无需登录即可创建新用户(用户注册)。
"""
user = crud.get_user_by_email(session=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system",
detail="系统中已存在该邮箱的用户",
)
user_create = UserCreate.model_validate(user_in)
user = crud.create_user(session=session, user_create=user_create)
@@ -160,7 +160,7 @@ def read_user_by_id(
user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser
) -> Any:
"""
Get a specific user by id.
通过 ID 获取指定用户。
"""
user = session.get(User, user_id)
if user == current_user:
@@ -168,7 +168,7 @@ def read_user_by_id(
if not current_user.is_superuser:
raise HTTPException(
status_code=403,
detail="The user doesn't have enough privileges",
detail="用户权限不足",
)
return user
@@ -185,20 +185,20 @@ def update_user(
user_in: UserUpdate,
) -> Any:
"""
Update a user.
更新用户信息。
"""
db_user = session.get(User, user_id)
if not db_user:
raise HTTPException(
status_code=404,
detail="The user with this id does not exist in the system",
detail="系统中不存在该ID的用户",
)
if user_in.email:
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
if existing_user and existing_user.id != user_id:
raise HTTPException(
status_code=409, detail="User with this email already exists"
status_code=409, detail="该邮箱已被其他用户使用"
)
db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in)
@@ -210,17 +210,17 @@ def delete_user(
session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID
) -> Message:
"""
Delete a user.
删除用户。
"""
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
raise HTTPException(status_code=404, detail="用户未找到")
if user == current_user:
raise HTTPException(
status_code=403, detail="Super users are not allowed to delete themselves"
status_code=403, detail="超级用户不允许删除自己"
)
statement = delete(Item).where(col(Item.owner_id) == user_id)
session.exec(statement) # type: ignore
session.delete(user)
session.commit()
return Message(message="User deleted successfully")
return Message(message="用户删除成功")
+2 -2
View File
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/utils", tags=["utils"])
)
def test_email(email_to: EmailStr) -> Message:
"""
Test emails.
测试邮件发送。
"""
email_data = generate_test_email(email_to=email_to)
send_email(
@@ -23,7 +23,7 @@ def test_email(email_to: EmailStr) -> Message:
subject=email_data.subject,
html_content=email_data.html_content,
)
return Message(message="Test email sent")
return Message(message="测试邮件已发送")
@router.get("/health-check/")
+3 -3
View File
@@ -22,7 +22,7 @@ wait_seconds = 1
def init(db_engine: Engine) -> None:
try:
with Session(db_engine) as session:
# Try to create session to check if DB is awake
# 尝试创建会话以检查数据库是否已就绪
session.exec(select(1))
except Exception as e:
logger.error(e)
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
def main() -> None:
logger.info("Initializing service")
logger.info("正在初始化服务")
init(engine)
logger.info("Service finished initializing")
logger.info("服务初始化完成")
if __name__ == "__main__":
+4 -4
View File
@@ -25,14 +25,14 @@ def parse_cors(v: Any) -> list[str] | str:
class Settings(BaseSettings):
model_config = SettingsConfigDict(
# Use top level .env file (one level above ./backend/)
# 使用顶层的 .env 文件(在 ./backend/ 上一级目录)
env_file="../.env",
env_ignore_empty=True,
extra="ignore",
)
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days
# 60 分钟 * 24 小时 * 8 = 8
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
FRONTEND_HOST: str = "http://localhost:5173"
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
@@ -97,8 +97,8 @@ class Settings(BaseSettings):
def _check_default_secret(self, var_name: str, value: str | None) -> None:
if value == "changethis":
message = (
f'The value of {var_name} is "changethis", '
"for security, please change it, at least for deployments."
f'{var_name} 的值为 "changethis"'
"出于安全考虑,请修改它,至少在部署时需要修改。"
)
if self.ENVIRONMENT == "local":
warnings.warn(message, stacklevel=1)
+6 -7
View File
@@ -7,18 +7,17 @@ from app.models import User, UserCreate
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
# make sure all SQLModel models are imported (app.models) before initializing DB
# otherwise, SQLModel might fail to initialize relationships properly
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
# 确保在初始化数据库之前已导入所有 SQLModel 模型(app.models
# 否则,SQLModel 可能无法正确初始化关系
# 更多详情请参考:https://github.com/fastapi/full-stack-fastapi-template/issues/28
def init_db(session: Session) -> None:
# Tables should be created with Alembic migrations
# But if you don't want to use migrations, create
# the tables un-commenting the next lines
# 数据库表应该通过 Alembic 迁移创建
# 但如果你不想使用迁移,可以取消注释下面的代码来创建表
# from sqlmodel import SQLModel
# This works because the models are already imported and registered from app.models
# 这样可以正常工作,因为模型已经从 app.models 导入并注册
# SQLModel.metadata.create_all(engine)
user = session.exec(
+2 -2
View File
@@ -14,9 +14,9 @@ def init() -> None:
def main() -> None:
logger.info("Creating initial data")
logger.info("正在创建初始数据")
init()
logger.info("Initial data created")
logger.info("初始数据创建完成")
if __name__ == "__main__":
+1 -1
View File
@@ -20,7 +20,7 @@ app = FastAPI(
generate_unique_id_function=custom_generate_unique_id,
)
# Set all CORS enabled origins
# 设置所有允许 CORS 的源
if settings.all_cors_origins:
app.add_middleware(
CORSMiddleware,
+13 -13
View File
@@ -4,7 +4,7 @@ from pydantic import EmailStr
from sqlmodel import Field, Relationship, SQLModel
# Shared properties
# 共享属性
class UserBase(SQLModel):
email: EmailStr = Field(unique=True, index=True, max_length=255)
is_active: bool = True
@@ -12,7 +12,7 @@ class UserBase(SQLModel):
full_name: str | None = Field(default=None, max_length=255)
# Properties to receive via API on creation
# 创建用户时通过 API 接收的属性
class UserCreate(UserBase):
password: str = Field(min_length=8, max_length=128)
@@ -23,7 +23,7 @@ class UserRegister(SQLModel):
full_name: str | None = Field(default=None, max_length=255)
# Properties to receive via API on update, all are optional
# 更新用户时通过 API 接收的属性,所有字段均为可选
class UserUpdate(UserBase):
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
password: str | None = Field(default=None, min_length=8, max_length=128)
@@ -39,14 +39,14 @@ class UpdatePassword(SQLModel):
new_password: str = Field(min_length=8, max_length=128)
# Database model, database table inferred from class name
# 数据库模型,数据库表名从类名推断
class User(UserBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
hashed_password: str
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
# Properties to return via API, id is always required
# 通过 API 返回的属性,id 始终必填
class UserPublic(UserBase):
id: uuid.UUID
@@ -56,23 +56,23 @@ class UsersPublic(SQLModel):
count: int
# Shared properties
# 共享属性
class ItemBase(SQLModel):
title: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=255)
# Properties to receive on item creation
# 创建 Item 时接收的属性
class ItemCreate(ItemBase):
pass
# Properties to receive on item update
# 更新 Item 时接收的属性
class ItemUpdate(ItemBase):
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
# Database model, database table inferred from class name
# 数据库模型,数据库表名从类名推断
class Item(ItemBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
owner_id: uuid.UUID = Field(
@@ -81,7 +81,7 @@ class Item(ItemBase, table=True):
owner: User | None = Relationship(back_populates="items")
# Properties to return via API, id is always required
# 通过 API 返回的属性,id 始终必填
class ItemPublic(ItemBase):
id: uuid.UUID
owner_id: uuid.UUID
@@ -92,18 +92,18 @@ class ItemsPublic(SQLModel):
count: int
# Generic message
# 通用消息
class Message(SQLModel):
message: str
# JSON payload containing access token
# 包含访问令牌的 JSON 载荷
class Token(SQLModel):
access_token: str
token_type: str = "bearer"
# Contents of JWT token
# JWT 令牌内容
class TokenPayload(SQLModel):
sub: str | None = None
+3 -3
View File
@@ -21,7 +21,7 @@ wait_seconds = 1
)
def init(db_engine: Engine) -> None:
try:
# Try to create session to check if DB is awake
# 尝试创建会话以检查数据库是否已就绪
with Session(db_engine) as session:
session.exec(select(1))
except Exception as e:
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
def main() -> None:
logger.info("Initializing service")
logger.info("正在初始化服务")
init(engine)
logger.info("Service finished initializing")
logger.info("服务初始化完成")
if __name__ == "__main__":
+1 -1
View File
@@ -36,7 +36,7 @@ def send_email(
subject: str = "",
html_content: str = "",
) -> None:
assert settings.emails_enabled, "no provided configuration for email variables"
assert settings.emails_enabled, "未提供邮件相关配置变量"
message = emails.Message(
subject=subject,
html=html_content,