简化H5应用用户授权

This commit is contained in:
2026-05-23 01:59:52 +08:00
parent 53128a6f02
commit 08df1825a7
8 changed files with 168 additions and 423 deletions

View File

@@ -1,87 +1,97 @@
import uuid
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import os
from fastapi import APIRouter, HTTPException
from pymongo.errors import DuplicateKeyError
from jose import jwt
from pydantic import BaseModel
from app.config import settings
from app.models.user import User
from app.schemas.auth import WechatLoginRequest, PhoneLoginRequest, TokenResponse
from app.middleware.auth import create_access_token
from app.services.wechat import code_to_session
router = APIRouter()
@router.post("/demo-login", response_model=TokenResponse)
async def demo_login():
"""Web 体验用的演示登录,每次创建独立的临时用户"""
demo_openid = f"demo_{uuid.uuid4().hex[:12]}"
user = User(
openid=demo_openid,
name="体验用户",
)
await user.insert()
token = create_access_token(str(user.id))
return TokenResponse(
access_token=token,
user_id=str(user.id),
is_new_user=True,
)
DEV_MODE = os.environ.get("DEV_MODE", "").lower() in ("1", "true", "yes")
# 固定测试账号 openid(dev 模式下使用)
DEV_OPENID = "dev_test_account_001"
@router.post("/wechat-login", response_model=TokenResponse)
async def wechat_login(req: WechatLoginRequest):
"""微信小程序登录"""
try:
wechat_data = await code_to_session(req.code)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
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:sub + openid + mobile"""
expire = datetime.now(timezone.utc) + timedelta(days=30)
payload = {
"sub": str(user.id),
"openid": user.openid,
"mobile": user.phone or "",
"exp": expire,
}
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)
is_new = user is None
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 = create_access_token(str(user.id))
token = _build_token(user)
return TokenResponse(
access_token=token,
token=token,
user_id=str(user.id),
is_new_user=is_new,
openid=user.openid,
mobile=user.phone or "",
)
@router.post("/phone-login", response_model=TokenResponse)
async def phone_login(req: PhoneLoginRequest):
"""手机号登录(简化版,生产环境需接入短信验证)"""
# TODO: 接入真实短信验证服务
if req.sms_code != "123456":
raise HTTPException(status_code=400, detail="验证码错误")
user = await User.find_one(User.phone == req.phone)
is_new = user is None
@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:
try:
user = User(openid=f"phone_{req.phone}", phone=req.phone)
await user.insert()
except DuplicateKeyError:
# 并发竞态:另一个请求已创建该用户,回退查找
user = await User.find_one(User.phone == req.phone)
is_new = False
user = User(
openid=DEV_OPENID,
name="开发测试用户",
phone="13800000000",
)
await user.insert()
token = create_access_token(str(user.id))
token = _build_token(user)
return TokenResponse(
access_token=token,
token=token,
user_id=str(user.id),
is_new_user=is_new,
openid=user.openid,
mobile=user.phone or "",
)

View File

@@ -1,20 +1,13 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class WechatLoginRequest(BaseModel):
code: str # wx.login() 获取的 code
class PhoneLoginRequest(BaseModel):
phone: str
sms_code: str
class ExchangeCodeRequest(BaseModel):
code: str
mobile: str | None = None
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
token: str
user_id: str
is_new_user: bool = False
openid: str
mobile: str

View File

@@ -64,6 +64,7 @@ init_config() {
APP_PORT=${APP_PORT:-${PORT:-8000}}
APP_WORKERS=${APP_WORKERS:-1}
APP_LOG_LEVEL=${APP_LOG_LEVEL:-"info"}
DEV_MODE=${DEV_MODE:-""}
# PID 文件路径
PID_FILE="${APP_DIR}/.${APP_NAME}.pid"
@@ -82,6 +83,7 @@ init_config() {
log_info "服务器端口: $APP_PORT"
log_info "工作进程数: $APP_WORKERS"
log_info "日志级别: $APP_LOG_LEVEL"
log_info "开发模式: $([ -n \"$DEV_MODE\" ] && echo \"已启用\" || echo \"已禁用\")"
log_info "日志文件: $LOG_FILE"
}