Files
app_03_pregnancy/backend/app/routers/auth.py

184 lines
5.3 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 未配置")
# 尝试从 CRMEB 拉取用户资料;失败(新用户/网络)时降级为匿名建档,openid 即身份
nickname, phone, avatar = "", "", ""
try:
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:
info = data.get("data") or {}
nickname = (info.get("nickname") or "").strip()
phone = (info.get("phone") or "").strip()
avatar = (info.get("avatar") or "").strip()
except Exception:
pass # CRMEB 不可用时仍允许 openid 建档
# 查找或创建用户
user = await User.find_one(User.openid == req.openid)
if not user:
user = User(
openid=req.openid,
name=nickname,
phone=phone or None,
avatar_url=avatar or None,
)
await user.insert()
else:
if nickname:
user.name = nickname
if phone:
user.phone = phone
if avatar:
user.avatar_url = 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 "",
)