You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

11 KiB

认证 API 响应格式规范化修复

日期: 2026-02-11
类型: Bug 修复
影响范围: 认证接口
状态: 已完成


概述

修复手机号登录(/api/v1/auth/login/phone)和微信登录(/api/v1/wechat/result)接口的返回格式,使其符合 API 设计规范。

问题描述

原有问题

  1. 字段命名不规范:使用 snake_case 而非 camelCase

    • access_token accessToken
    • refresh_token refreshToken
    • token_type: "bearer" tokenType: "Bearer"
  2. 缺少必需字段:缺少 expiresIn 字段(Token 过期时间,单位:秒)

  3. 数据结构不一致:返回原始字典而非规范的 Schema

API 规范要求

根据 api-design.md,登录接口应返回:

{
  "success": true,
  "code": 200,
  "message": "Success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 1800,
    "user": {
      "userId": "550e8400-e29b-41d4-a716-446655440000",
      "phone": "13800138000",
      "nickname": "用户昵称"
    }
  },
  "timestamp": "2026-02-11T10:30:00+00:00"
}

解决方案

1. 更新 Schema 定义

文件: server/app/schemas/user.py

class LoginResponse(BaseModel):
    """登录响应"""
    model_config = ConfigDict(populate_by_name=True)
    
    access_token: str = Field(..., alias="accessToken", description="访问令牌")
    refresh_token: str = Field(..., alias="refreshToken", description="刷新令牌")
    token_type: str = Field(default="Bearer", alias="tokenType", description="令牌类型")
    expires_in: int = Field(..., alias="expiresIn", description="过期时间(秒)")
    user: UserResponse = Field(..., description="用户信息")


class RefreshTokenResponse(BaseModel):
    """刷新 Token 响应"""
    model_config = ConfigDict(populate_by_name=True)
    
    access_token: str = Field(..., alias="accessToken", description="新的访问令牌")
    token_type: str = Field(default="Bearer", alias="tokenType", description="令牌类型")
    expires_in: int = Field(..., alias="expiresIn", description="过期时间(秒)")

变更

  • 添加 expires_in 字段
  • 修改 token_type 默认值为 "Bearer"(首字母大写)
  • 调整字段顺序,user 放在最后(符合规范)
  • 新增 RefreshTokenResponse Schema

文件: server/app/schemas/wechat.py

class WechatLoginResultResponse(BaseModel):
    """微信登录结果响应"""
    status: Literal["pending", "success"] = Field(..., description="登录状态")
    user: Optional[UserInfoResponse] = Field(None, description="用户信息")
    access_token: Optional[str] = Field(None, alias="accessToken", description="访问令牌")
    refresh_token: Optional[str] = Field(None, alias="refreshToken", description="刷新令牌")
    token_type: Optional[str] = Field(None, alias="tokenType", description="令牌类型")
    expires_in: Optional[int] = Field(None, alias="expiresIn", description="过期时间(秒)")

变更

  • 添加 expires_in 字段

2. 修改 Service 层返回格式

文件: server/app/services/user_service.py

手机号登录

async def login_with_phone(...) -> Dict[str, Any]:
    # 生成 Token
    access_token = create_access_token(data={'user_id': str(user.user_id)})
    refresh_token = create_refresh_token(data={'user_id': str(user.user_id)})
    
    # 获取过期时间(秒)
    from app.core.config import settings
    expires_in = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
    
    return {
        'user': user,
        'access_token': access_token,
        'refresh_token': refresh_token,
        'token_type': 'Bearer',
        'expires_in': expires_in
    }

变更

  • 添加 expires_in 计算(从配置读取 ACCESS_TOKEN_EXPIRE_MINUTES,转换为秒)
  • 修改 token_type'Bearer'(首字母大写)

刷新 Token

async def refresh_token(self, refresh_token: str) -> Dict[str, Any]:
    # 生成新的 Access Token
    new_access_token = create_access_token(data={'user_id': user_id})
    
    # 更新会话
    await self.repository.update_session(...)
    
    # 获取过期时间(秒)
    from app.core.config import settings
    expires_in = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
    
    return {
        'access_token': new_access_token,
        'token_type': 'Bearer',
        'expires_in': expires_in
    }

变更

  • 添加 expires_in 计算
  • 修改 token_type'Bearer'

文件: server/app/services/wechat_service.py

async def get_login_result(self, scene_id: str) -> Optional[Dict[str, Any]]:
    # ...
    
    # 获取过期时间(秒)
    from app.core.config import settings
    expires_in = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
    
    return {
        'status': 'success',
        'user': user,
        'access_token': access_token,
        'refresh_token': refresh_token,
        'token_type': 'Bearer',
        'expires_in': expires_in
    }

变更

  • 添加 expires_in 计算
  • 修改 token_type'Bearer'

3. 更新 API 路由类型注解

文件: server/app/api/v1/auth.py

手机号登录

@router.post("/login/phone", response_model=SuccessResponse[LoginResponse], summary="手机号登录")
async def login_with_phone(...):
    # ...
    return SuccessResponse(data=result)

变更

  • response_modelSuccessResponse[dict] 改为 SuccessResponse[LoginResponse]
  • 导入 LoginResponse Schema

刷新 Token

@router.post("/refresh", response_model=SuccessResponse[RefreshTokenResponse], summary="刷新 Token")
async def refresh_token(...):
    # ...
    return SuccessResponse(data=result)

变更

  • response_modelSuccessResponse[dict] 改为 SuccessResponse[RefreshTokenResponse]
  • 导入 RefreshTokenResponse Schema

文件: server/app/api/v1/wechat.py

# 登录成功
return SuccessResponse(
    data=WechatLoginResultResponse(
        status="success",
        user=UserInfoResponse.model_validate(user),
        access_token=result['access_token'],
        refresh_token=result['refresh_token'],
        token_type=result['token_type'],
        expires_in=result['expires_in']  # ← 新增
    )
)

变更

  • 添加 expires_in 字段传递

技术细节

Token 过期时间计算

from app.core.config import settings

# 配置: ACCESS_TOKEN_EXPIRE_MINUTES = 30(默认 30 分钟)
expires_in = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60  # 1800 秒

说明

  • Access Token 过期时间由 app/core/config.pyACCESS_TOKEN_EXPIRE_MINUTES 配置控制
  • 默认值:30 分钟(1800 秒)
  • API 返回秒数,方便前端计算过期时间

字段序列化

Pydantic 自动处理 snake_casecamelCase 转换:

class LoginResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    
    # Python 字段名        → JSON 字段名
    access_token: str      "accessToken"
    refresh_token: str     "refreshToken"
    token_type: str        "tokenType"
    expires_in: int        "expiresIn"

影响的接口

接口 方法 路径 变更
手机号登录 POST /api/v1/auth/login/phone 修复
刷新 Token POST /api/v1/auth/refresh 修复
微信登录轮询 GET /api/v1/wechat/result 修复

向后兼容性

⚠️ Breaking Change

此修改会破坏现有前端代码,因为字段名从 snake_case 变为 camelCase

前端需要同步修改

// ❌ 旧代码
const { access_token, refresh_token, token_type } = response.data;

// ✅ 新代码
const { accessToken, refreshToken, tokenType, expiresIn } = response.data;

测试建议

1. 手机号登录测试

# 发送验证码
curl -X POST http://localhost:6170/api/v1/auth/sms/send \
  -H "Content-Type: application/json" \
  -d '{"phone": "13800138000", "countryCode": "+86", "purpose": "login"}'

# 登录(测试模式验证码:888888)
curl -X POST http://localhost:6170/api/v1/auth/login/phone \
  -H "Content-Type: application/json" \
  -d '{"phone": "13800138000", "countryCode": "+86", "code": "888888"}'

预期响应

{
  "success": true,
  "code": 200,
  "message": "Success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 1800,
    "user": {
      "userId": "...",
      "phone": "13800138000",
      "username": "...",
      "nickname": null,
      "avatarUrl": null,
      "aiCreditsBalance": 100,
      ...
    }
  },
  "timestamp": "2026-02-11T10:30:00+00:00"
}

2. 刷新 Token 测试

curl -X POST http://localhost:6170/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "eyJhbGciOiJIUzI1NiIs..."}'

预期响应

{
  "success": true,
  "code": 200,
  "message": "Success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 1800
  },
  "timestamp": "2026-02-11T10:30:00+00:00"
}

3. 字段检查

验证响应中的关键字段:

  • accessToken(非 access_token
  • refreshToken(非 refresh_token
  • tokenType: "Bearer"(首字母大写)
  • expiresIn: 1800(秒数)
  • user.userId(非 user.user_id

相关文件

  • server/app/schemas/user.py - 用户 Schema 定义
  • server/app/schemas/wechat.py - 微信 Schema 定义
  • server/app/services/user_service.py - 用户 Service
  • server/app/services/wechat_service.py - 微信 Service
  • server/app/api/v1/auth.py - 认证 API 路由
  • server/app/api/v1/wechat.py - 微信 API 路由
  • .claude/skills/jointo-tech-stack/references/api-design.md - API 设计规范

参考规范

总结

本次修复确保了认证接口的响应格式符合项目 API 设计规范:

  1. 字段命名统一使用 camelCase
  2. 添加 expiresIn 字段,方便前端计算过期时间
  3. 修正 tokenType"Bearer"(首字母大写)
  4. 使用 Pydantic Schema 提供类型安全和自动文档生成

此修改需要前端同步更新代码以匹配新的字段名。