Files
app_03_pregnancy/backend/app/routers/auth.py
Karlcarlo 96a938e11b feat: openid-login + avatar_url 修复
- 后端: 新增 /auth/openid-login 接口(调 CRMEB 补齐用户信息)
- 后端: config.py 新增 crmeb_base_url / crmeb_api_key 配置
- 后端: api.ts 新增 openid-login 接口声明
- 前端: LoginPage 改为走 openid-login(openid 单参登录)
- 前端: UserContext 修复 avatar_url 未映射导致头像不显示的问题
2026-05-27 22:24:14 +08:00

178 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import os
from fastapi import APIRouter, HTTPException
from jose import jwt
import httpx
from pydantic import BaseModel
from app.config import settings
from app.models.user import User
from app.services.wechat import code_to_session
router = APIRouter()
DEV_MODE = os.environ.get("DEV_MODE", "").lower() in ("1", "true", "yes")
# 固定测试账号 openid(dev 模式下使用)
DEV_OPENID = "dev_test_account_001"
class ExchangeCodeRequest(BaseModel):
code: str
mobile: str | None = None
class TokenResponse(BaseModel):
token: str
user_id: str
openid: str
mobile: str
def _build_token(user: User) -> str:
"""生成自包含 JWT:uid + openid + phone(兼容外部 token 格式)"""
expire = datetime.now(timezone.utc) + timedelta(days=30)
payload = {
"uid": str(user.id),
"openid": user.openid,
"phone": user.phone or "",
"exp": expire,
"iss": "localhost",
}
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
@router.post("/exchange-code")
async def exchange_code(req: ExchangeCodeRequest) -> TokenResponse:
"""小程序登录:code → openid → 创建/查找 User → 返回自包含 JWT"""
wechat_data = await code_to_session(req.code)
openid = wechat_data["openid"]
user = await User.find_one(User.openid == openid)
if not user:
user = User(
openid=openid,
session_key=wechat_data.get("session_key"),
union_id=wechat_data.get("unionid"),
phone=req.mobile or None,
)
await user.insert()
else:
# 更新 session_key + 手机号
user.session_key = wechat_data.get("session_key")
if req.mobile:
user.phone = req.mobile
await user.save()
token = _build_token(user)
return TokenResponse(
token=token,
user_id=str(user.id),
openid=user.openid,
mobile=user.phone or "",
)
class ParamsLoginRequest(BaseModel):
uid: str = ""
openid: str
phone: str = ""
@router.post("/params-login")
async def params_login(req: ParamsLoginRequest) -> TokenResponse:
"""小程序传 uid/openid/phone → 查找或创建用户 → 返回自签 token"""
user = await User.find_one(User.openid == req.openid)
if not user:
user = User(openid=req.openid, phone=req.phone or None)
await user.insert()
else:
if req.phone and req.phone != (user.phone or ""):
user.phone = req.phone
await user.save()
token = _build_token(user)
return TokenResponse(
token=token,
user_id=str(user.id),
openid=user.openid,
mobile=user.phone or "",
)
class OpenidRequest(BaseModel):
openid: str
@router.post("/openid-login")
async def openid_login(req: OpenidRequest) -> TokenResponse:
"""仅传 openid → 调 CRMEB 获取用户信息 → 创建/更新 User → 返回 token"""
if not settings.crmeb_base_url or not settings.crmeb_api_key:
raise HTTPException(status_code=500, detail="CRMEB 未配置")
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{settings.crmeb_base_url}/api/internal/user-info",
params={"openid": req.openid},
headers={"X-Internal-Key": settings.crmeb_api_key},
timeout=10,
)
data = resp.json()
if data.get("code") != 200:
raise HTTPException(status_code=400, detail="获取用户信息失败")
info = data["data"]
# 查找或创建用户
user = await User.find_one(User.openid == req.openid)
if not user:
user = User(
openid=req.openid,
name=info.get("nickname", "") or "",
phone=info.get("phone") or None,
avatar_url=info.get("avatar") or None,
)
await user.insert()
else:
user.name = info.get("nickname", "") or user.name
if info.get("phone"):
user.phone = info["phone"]
if info.get("avatar"):
user.avatar_url = info["avatar"]
await user.save()
token = _build_token(user)
return TokenResponse(
token=token,
user_id=str(user.id),
openid=user.openid,
mobile=user.phone or "",
)
@router.post("/dev-login")
async def dev_login() -> TokenResponse:
"""开发环境:使用固定测试账号,不依赖微信"""
if not DEV_MODE:
raise HTTPException(status_code=403, detail="仅开发环境可用")
user = await User.find_one(User.openid == DEV_OPENID)
if not user:
user = User(
openid=DEV_OPENID,
name="开发测试用户",
phone="13800000000",
)
await user.insert()
token = _build_token(user)
return TokenResponse(
token=token,
user_id=str(user.id),
openid=user.openid,
mobile=user.phone or "",
)