CI / backend (push) Canceled after 0s
跑通迁移与测试后修掉的真问题: - alembic 0001:asyncpg 走预备语句,一次 execute 只能一条 SQL。 CREATE FUNCTION / CREATE POLICY / GRANT 全部拆成单语句逐条执行, 否则报 "cannot insert multiple commands into a prepared statement"。 - 创建工作区:ORM flush 只按 relationship 排序,裸外键不算数, member 可能先于 workspace 落地撞 FK。改为先 flush workspace 再插 member。 - datetime.UTC 是 3.11+ 才有的,本机 3.10 直接 ImportError。改用 timezone.utc。 - 幂等中间件:原来会缓存 4xx。校验失败被缓存后,用户改对参数仍拿到同一个 422。 改为只缓存 <400。 - 发码节流窗口改为可配置 SMS_THROTTLE_SECONDS(测试置 0), 节流用例自己显式打开,避免"整库关掉节流"导致节流从未被验证。 - 租户隔离用例:pytest.raises(Exception) 改为精确的 ProgrammingError, 否则任何异常都会让"应用角色不能建表"这条断言假通过。 验收:pytest 33 passed;ruff check / format 通过;真机跑 uvicorn, /health、/version、登录、建工作区、列表、/auth/me 全通。
68 lines
1.3 KiB
Python
68 lines
1.3 KiB
Python
"""对外输出模型。字段命名一次定死,客户端按契约生成。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class UserOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
phone: str = Field(description="已打码的手机号,如 138****8000")
|
|
nickname: str
|
|
created_at: datetime
|
|
|
|
|
|
class TokenOut(BaseModel):
|
|
access_token: str
|
|
refresh_token: str
|
|
token_type: str = "bearer" # noqa: S105 - 不是密码,是 OAuth2 的固定字段值
|
|
expires_in: int
|
|
|
|
|
|
class LoginOut(BaseModel):
|
|
user: UserOut
|
|
tokens: TokenOut
|
|
|
|
|
|
class WorkspaceOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
name: str
|
|
slug: str
|
|
plan: str
|
|
role: str = ""
|
|
created_at: datetime
|
|
|
|
|
|
class MemberOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
user_id: uuid.UUID
|
|
role: str
|
|
status: str
|
|
nickname: str = ""
|
|
phone: str = ""
|
|
joined_at: datetime
|
|
|
|
|
|
class InvitationOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
code: str
|
|
role: str
|
|
expires_at: datetime
|
|
accepted_at: datetime | None = None
|
|
|
|
|
|
class MeOut(BaseModel):
|
|
user: UserOut
|
|
workspaces: list[WorkspaceOut]
|