fastapi: 输出时动态增加新字段

一,代码:

从 Pydantic v2 开始,官方引入了 @computed_field 装饰器,
它会自动将一个 Python property 包含在序列化输出(model_dump() / JSON 响应)中,并能被 FastAPI 的 Swagger UI 完美捕获生成文档。

# 输出给前端的用户信息(安全公开,不含密码,包含只读字段)
class UserResponse(BaseModel):
    id: int = Field(..., description="用户的唯一ID")
    username: str = Field(..., description="用户名")
    nickname: str = Field(..., description="用户昵称")
    avatar_id: int = 0
    head_url: str = ''

    # 强制将这个属性包含在 API 输出中
    @computed_field
    @property
    def avatar_url(self) -> str:
        # 在生产环境中,这里通常是根据配置的 CDN 域名 + 用户标识动态生成
        CDN_DOMAIN = "https://cdn.example.com"
        return f"{CDN_DOMAIN}/avatars/{self.id}.png"



    class Config:
        from_attributes = True
        json_schema_extra = {
            "example": {"id": 1, "username": "alex_green", "nickname": "alex@example.com"}
        }


# 输出给前端的用户信息(安全公开,不含密码,包含只读字段)
class AssetResponse(BaseModel):
    id: int = Field(..., description="资源的唯一ID")
    file_name: str = Field(..., description="文件名")
    ext: str = Field(..., description="扩展名")

    class Config:
        from_attributes = True
        json_schema_extra = {
            "example": {"id": 1, "file_name": "alex_green", "ext": "alex@example.com"}
        }

# 这是你需要补上的分页响应模型
class ListPageResponse(BaseModel, Generic[T]):
    total: int
    # items: List[Union[UserResponse, AssetResponse]] # 核心:这里告诉 Pydantic,items 里面才是用户列表
    items: List[T]  # 核心:这里告诉 Pydantic,items 里面才是用户列表


@router.get("/all2", response_model=ApiResponse[ListPageResponse[UserResponse]], summary="获取商品和用户数据")
async def get_all_users2(request: Request,
                        prod_data: Annotated[UserListForm, Query()],
                           db: AsyncSession = Depends(get_db)):
    # 参数
    page = prod_data.page
    page_size = prod_data.page_size
    ctype = prod_data.ctype
    # 使用 func.count().over() 作为一个隐藏列
    stmt = (
        select(User, func.count().over().label("total_count"))
        .where(User.status == 1)
        .offset((page - 1) * page_size)
        .limit(page_size)
    )

    result = await db.execute(stmt)

    # 因为多查了一个 count 列,result 出来的每一行是一个元组 (User对象, total_count值)
    rows = result.all()

    # 解析结果
    if not rows:
        return {"total": 0, "page": page, "size": page_size, "items": []}

    # 无论哪一行,对应的 total_count 都是一样的,取第一行的即可
    total = rows[0].total_count
    print("total:", total)
    items = [row.User for row in rows]

    print("items:", items)

    response_data = []
    for user_one in items:
        # 直接实例化 UserResponse,并明确赋值!
        avatar_id = user_one.avatar_id if user_one.avatar_id is not None else 0
        print("avatar_id:", avatar_id)
        user_vo = UserResponse(
            id=user_one.id,
            username=user_one.username,
            nickname = user_one.nickname,
            avatar_id = avatar_id,
            head_url = await get_head_url(db, avatar_id),
        )
        response_data.append(user_vo)

    data = {
        "items": response_data,
        "total": total,
    }

    # return ApiResponse.fail(code=403,message="测试报错")
    return ApiResponse.success(data=data)


async def get_head_url(db,avatar_id: int | None = 0)->str:
    DEFAULT_AVATAR = "http://127.0.0.1:8008/logo_300.png"
    FILE_HOST = "http://127.0.0.1:8008"
    if not avatar_id:
        return DEFAULT_AVATAR

    # 2. 异步查询 assets 表
    try:
        stmt = select(Asset.full_path).where(Asset.id == avatar_id)
        result = await db.execute(stmt)
        path = result.scalar_one_or_none()

        return FILE_HOST + path if path else DEFAULT_AVATAR
    except Exception:
        # 生产环境建议加上日志记录(如 logger.error)
        return DEFAULT_AVATAR

说明:

head_url是需要查库后添加的字符串 

avatar_url则是通过computed_field自动获取到的属性

 

二,测试效果:

image

posted @ 2026-07-25 21:25  刘宏缔的架构森林  阅读(12)  评论(0)    收藏  举报