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
11 KiB
认证 API 响应格式规范化修复
日期: 2026-02-11
类型: Bug 修复
影响范围: 认证接口
状态: ✅ 已完成
概述
修复手机号登录(/api/v1/auth/login/phone)和微信登录(/api/v1/wechat/result)接口的返回格式,使其符合 API 设计规范。
问题描述
原有问题
-
字段命名不规范:使用
snake_case而非camelCase- ❌
access_token→ ✅accessToken - ❌
refresh_token→ ✅refreshToken - ❌
token_type: "bearer"→ ✅tokenType: "Bearer"
- ❌
-
缺少必需字段:缺少
expiresIn字段(Token 过期时间,单位:秒) -
数据结构不一致:返回原始字典而非规范的 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放在最后(符合规范) - ✅ 新增
RefreshTokenResponseSchema
文件: 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_model从SuccessResponse[dict]改为SuccessResponse[LoginResponse] - ✅ 导入
LoginResponseSchema
刷新 Token
@router.post("/refresh", response_model=SuccessResponse[RefreshTokenResponse], summary="刷新 Token")
async def refresh_token(...):
# ...
return SuccessResponse(data=result)
变更:
- ✅ 将
response_model从SuccessResponse[dict]改为SuccessResponse[RefreshTokenResponse] - ✅ 导入
RefreshTokenResponseSchema
文件: 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.py的ACCESS_TOKEN_EXPIRE_MINUTES配置控制 - 默认值:30 分钟(1800 秒)
- API 返回秒数,方便前端计算过期时间
字段序列化
Pydantic 自动处理 snake_case → camelCase 转换:
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- 用户 Serviceserver/app/services/wechat_service.py- 微信 Serviceserver/app/api/v1/auth.py- 认证 API 路由server/app/api/v1/wechat.py- 微信 API 路由.claude/skills/jointo-tech-stack/references/api-design.md- API 设计规范
参考规范
总结
本次修复确保了认证接口的响应格式符合项目 API 设计规范:
- ✅ 字段命名统一使用 camelCase
- ✅ 添加
expiresIn字段,方便前端计算过期时间 - ✅ 修正
tokenType为"Bearer"(首字母大写) - ✅ 使用 Pydantic Schema 提供类型安全和自动文档生成
此修改需要前端同步更新代码以匹配新的字段名。