feat(#1718): PATCH /auth/me 资料更新接口 + profile_completed 字段 (#1728)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
CI/CD Pipeline / Build Staging API Image (push) Successful in 17s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Validate - Style (push) Successful in 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
AI Code Review / AI Code Review (pull_request) Successful in 3m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m17s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m10s
CI/CD Pipeline / Unit Tests (push) Successful in 8m18s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 11m48s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1728.
This commit is contained in:
2026-09-06 12:06:58 +08:00
committed by auto-approve-bot
parent ff60fdf956
commit 528f56254d
8 changed files with 322 additions and 19 deletions
+65 -19
View File
@@ -1,4 +1,5 @@
"""
from __future__ import annotations
Canonical authentication API routes.
The route layer is intentionally thin: repository construction lives in
@@ -15,7 +16,7 @@ from app.config import settings
from app.dependencies import get_auth_email_service, get_auth_session_store, get_user_repository
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, EmailStr, field_validator
from packages.adapters.redis import NoopSessionStore
from packages.adapters.smtp import NoopEmailService
@@ -85,6 +86,22 @@ class CurrentUserResponse(BaseModel):
phone_verified: bool = False
binding_complete: bool = False
wechat_bound: bool = False
profile_completed: bool = True
class UserProfileResponse(BaseModel):
"""用户资料负载(PATCH /me、绑定/解绑接口复用;字段与 GET /auth/me 一致,前端 normalizeUser 直接消费)"""
user_id: str
email: str
username: str
display_name: str
email_verified: bool
phone: str = ""
phone_verified: bool = False
binding_complete: bool = False
wechat_bound: bool = False
profile_completed: bool = True
class PasswordResetRequestModel(BaseModel):
@@ -274,9 +291,51 @@ async def get_current_user_info(
phone_verified=user.phone_verified,
binding_complete=binding_complete,
wechat_bound=bool(user.wechat_openid),
profile_completed=user.profile_completed,
)
class UpdateProfileRequest(BaseModel):
"""更新个人资料请求(当前仅支持昵称)"""
display_name: str
@field_validator("display_name")
@classmethod
def _validate_display_name(cls, v: str) -> str:
name = (v or "").strip()
if not name:
raise ValueError("昵称不能为空白")
if len(name) > 20:
raise ValueError("昵称长度需在 1-20 个字符之间")
return name
class UpdateProfileResponse(BaseModel):
"""更新资料响应:前端 normalizeUser(response.user) 直接消费"""
user: UserProfileResponse
@router.patch("/me", response_model=UpdateProfileResponse)
async def update_current_user_profile(
request: UpdateProfileRequest,
current_user: AuthenticatedUser = Depends(get_current_user),
user_repository: UserRepository = Depends(get_user_repository),
) -> UpdateProfileResponse:
"""更新当前登录用户昵称(微信新用户首次设置昵称后置 profile_completed=True)。"""
user = current_user.user
user.display_name = request.display_name # 已 stripvalidator
if not user.profile_completed:
user.profile_completed = True
user_repository.save(user)
logger.info("[资料更新] 用户 %s 更新昵称,profile_completed=%s", user.id, user.profile_completed)
# 重新读取,确保返回的是持久化后的最新状态
fresh = user_repository.find_by_id(user.id) or user
return UpdateProfileResponse(user=_user_profile(fresh))
class _NoopSessionStore(NoopSessionStore):
pass
@@ -507,34 +566,20 @@ class WechatBindCompleteRequest(BaseModel):
state: str = ""
class WechatBindUserProfile(BaseModel):
"""绑定/解绑后返回的用户信息(字段对齐 /auth/me,前端 normalizeUser 直接消费)"""
user_id: str
email: str
username: str
display_name: str
email_verified: bool
phone: str = ""
phone_verified: bool = False
binding_complete: bool = False
wechat_bound: bool = False
class WechatBindCompleteResponse(BaseModel):
success: bool
user: WechatBindUserProfile
user: UserProfileResponse
class WechatUnbindResponse(BaseModel):
success: bool
def _wechat_user_profile(user) -> WechatBindUserProfile:
def _user_profile(user) -> UserProfileResponse:
binding_complete = bool(
user.phone_verified and user.email_verified and user.email and "@wechat.local" not in user.email
)
return WechatBindUserProfile(
return UserProfileResponse(
user_id=user.id,
email=user.email,
username=user.username,
@@ -544,6 +589,7 @@ def _wechat_user_profile(user) -> WechatBindUserProfile:
phone_verified=user.phone_verified,
binding_complete=binding_complete,
wechat_bound=bool(user.wechat_openid),
profile_completed=user.profile_completed,
)
@@ -589,7 +635,7 @@ async def wechat_bind(
raise HTTPException(status_code=http_status, detail=error)
logger.info("[微信绑定] 用户 %s 绑定成功 openid=%s", current_user.user.id, wechat_user.openid[:8])
return WechatBindCompleteResponse(success=True, user=_wechat_user_profile(result.user))
return WechatBindCompleteResponse(success=True, user=_user_profile(result.user))
@router.delete("/wechat/bind", response_model=WechatUnbindResponse)