# 认证 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`,登录接口应返回: ```json { "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` ```python 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` ```python 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` #### 手机号登录 ```python 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 ```python 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` ```python 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` #### 手机号登录 ```python @router.post("/login/phone", response_model=SuccessResponse[LoginResponse], summary="手机号登录") async def login_with_phone(...): # ... return SuccessResponse(data=result) ``` **变更**: - ✅ 将 `response_model` 从 `SuccessResponse[dict]` 改为 `SuccessResponse[LoginResponse]` - ✅ 导入 `LoginResponse` Schema #### 刷新 Token ```python @router.post("/refresh", response_model=SuccessResponse[RefreshTokenResponse], summary="刷新 Token") async def refresh_token(...): # ... return SuccessResponse(data=result) ``` **变更**: - ✅ 将 `response_model` 从 `SuccessResponse[dict]` 改为 `SuccessResponse[RefreshTokenResponse]` - ✅ 导入 `RefreshTokenResponse` Schema **文件**: `server/app/api/v1/wechat.py` ```python # 登录成功 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 过期时间计算 ```python 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.py` 的 `ACCESS_TOKEN_EXPIRE_MINUTES` 配置控制 - 默认值:30 分钟(1800 秒) - API 返回秒数,方便前端计算过期时间 ### 字段序列化 Pydantic 自动处理 `snake_case` → `camelCase` 转换: ```python 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`。 **前端需要同步修改**: ```typescript // ❌ 旧代码 const { access_token, refresh_token, token_type } = response.data; // ✅ 新代码 const { accessToken, refreshToken, tokenType, expiresIn } = response.data; ``` ## 测试建议 ### 1. 手机号登录测试 ```bash # 发送验证码 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"}' ``` **预期响应**: ```json { "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 测试 ```bash curl -X POST http://localhost:6170/api/v1/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken": "eyJhbGciOiJIUzI1NiIs..."}' ``` **预期响应**: ```json { "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 设计规范](../../.claude/skills/jointo-tech-stack/references/api-design.md) - 统一响应格式和字段命名规范 - [后端开发规范](../../.claude/skills/jointo-tech-stack/references/backend.md) - Schema 定义和类型注解 ## 总结 本次修复确保了认证接口的响应格式符合项目 API 设计规范: 1. ✅ 字段命名统一使用 camelCase 2. ✅ 添加 `expiresIn` 字段,方便前端计算过期时间 3. ✅ 修正 `tokenType` 为 `"Bearer"`(首字母大写) 4. ✅ 使用 Pydantic Schema 提供类型安全和自动文档生成 此修改需要前端同步更新代码以匹配新的字段名。