from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import and_ from pydantic import BaseModel, Field from typing import Optional import uuid import random import re from datetime import datetime, timedelta from ..database import get_db from ..models import ( User, LoginLog, RegisterSetting, SmsConfig, SmsVerificationCode, gen_uuid ) from ..auth import get_current_user, hash_password, verify_password, create_access_token from ..middleware.log_service import LogService from ..sms_client import send_sms_code router = APIRouter() class LoginRequest(BaseModel): username: str password: str class RegisterRequest(BaseModel): username: str = Field(min_length=3, max_length=50) password: str = Field(min_length=6, max_length=100) email: Optional[str] = None phone: Optional[str] = None sms_code: Optional[str] = None class SendCodeRequest(BaseModel): phone: str purpose: str = "register" # register / reset_password class ResetPasswordRequest(BaseModel): phone: str code: str new_password: str = Field(min_length=6, max_length=100) @router.post("/login") def login(req: LoginRequest, db: Session = Depends(get_db)): import logging logger = logging.getLogger(__name__) user = db.query(User).filter(User.username == req.username).first() if not user: logger.warning(f"登录失败: 用户 '{req.username}' 不存在") raise HTTPException(status_code=401, detail="用户名不存在") stored_hash = user.password_hash logger.info(f"登录调试: username={req.username} hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}") if not verify_password(req.password, user.password_hash): logger.warning(f"登录失败: 用户 '{req.username}' 密码验证失败 hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}") raise HTTPException(status_code=401, detail="密码错误") if user.status == "banned": raise HTTPException(status_code=403, detail="账号已被封禁") token_data = {"sub": user.id, "username": user.username, "role": user.role} access_token = create_access_token(token_data) # 记录登录日志 login_log = LoginLog( id=gen_uuid(), user_id=user.id, ip="", login_at=datetime.now(), ) db.add(login_log) # 记录操作日志 LogService.log_operation( user_id=user.id, username=user.username, action="login", target="user", detail=f"用户 {user.username} 登录成功", ) db.commit() return { "code": 0, "data": { "access_token": access_token, "token_type": "bearer", "user": { "id": user.id, "username": user.username, "email": user.email, "phone": user.phone, "avatar": user.avatar, "role": user.role, "status": user.status, "membership_level": user.membership_level, "membership_expire_at": str(user.membership_expire_at) if user.membership_expire_at else None, "created_at": str(user.created_at) if user.created_at else None, }, }, "message": "登录成功", } @router.post("/register") def register(req: RegisterRequest, db: Session = Depends(get_db)): # 检查注册设置 reg_setting = db.query(RegisterSetting).first() if reg_setting and reg_setting.phone_required and not req.phone: raise HTTPException(status_code=400, detail="手机号为必填项") if reg_setting and reg_setting.email_required and not req.email: raise HTTPException(status_code=400, detail="邮箱为必填项") # 检查用户名是否已存在 existing = db.query(User).filter(User.username == req.username).first() if existing: raise HTTPException(status_code=400, detail="用户名已存在") if req.email: existing_email = db.query(User).filter(User.email == req.email).first() if existing_email: raise HTTPException(status_code=400, detail="邮箱已被注册") if req.phone: existing_phone = db.query(User).filter(User.phone == req.phone).first() if existing_phone: raise HTTPException(status_code=400, detail="手机号已被注册") # 短信验证码校验(手机号注册且开启短信验证) if req.phone and reg_setting and reg_setting.phone_sms_required: if not req.sms_code: raise HTTPException(status_code=400, detail="请先获取并填写短信验证码") vrec = db.query(SmsVerificationCode).filter( SmsVerificationCode.phone == req.phone, SmsVerificationCode.purpose == "register", SmsVerificationCode.used == False, SmsVerificationCode.expires_at > datetime.now(), ).order_by(SmsVerificationCode.created_at.desc()).first() if not vrec or vrec.code != req.sms_code: raise HTTPException(status_code=400, detail="短信验证码错误或已过期") vrec.used = True user = User( id=gen_uuid(), username=req.username, password_hash=hash_password(req.password), email=req.email, phone=req.phone, avatar=None, role="user", status="active", real_name_status="unverified", membership_level=0, created_at=datetime.now(), updated_at=datetime.now(), ) db.add(user) db.flush() # 创建Token余额记录 from ..models import TokenBalance balance = TokenBalance( id=gen_uuid(), user_id=user.id, total_consumed=0, balance=0, daily_free_used=0, ) db.add(balance) token_data = {"sub": user.id, "username": user.username, "role": user.role} access_token = create_access_token(token_data) LogService.log_operation( user_id=user.id, username=user.username, action="register", target="user", detail=f"新用户 {user.username} 注册成功", ) db.commit() return { "code": 0, "data": { "access_token": access_token, "token_type": "bearer", "user": { "id": user.id, "username": user.username, "email": user.email or "", "phone": user.phone or "", "avatar": user.avatar or "", "role": user.role, "status": user.status, "membership_level": user.membership_level, "created_at": str(user.created_at) if user.created_at else None, }, }, "message": "注册成功", } @router.post("/send-code") def send_code(req: SendCodeRequest, db: Session = Depends(get_db)): """发送短信验证码(注册 / 重置密码)。公开接口。""" if not re.match(r'^1[3-9]\d{9}$', req.phone or ''): raise HTTPException(status_code=400, detail="手机号格式不正确") purpose = req.purpose or "register" if purpose not in ("register", "reset_password"): raise HTTPException(status_code=400, detail="不支持的验证码类型") config = db.query(SmsConfig).first() if not config or not config.provider: raise HTTPException(status_code=400, detail="短信服务未配置,请联系管理员") # 注册场景:手机号已注册则直接提示,避免浪费短信 if purpose == "register": existing = db.query(User).filter(User.phone == req.phone).first() if existing: raise HTTPException(status_code=400, detail="该手机号已注册,请直接登录") # 频率限制:同一手机号+用途 60 秒内不重复下发 recent = db.query(SmsVerificationCode).filter( SmsVerificationCode.phone == req.phone, SmsVerificationCode.purpose == purpose, SmsVerificationCode.used == False, SmsVerificationCode.expires_at > datetime.now(), ).order_by(SmsVerificationCode.created_at.desc()).first() if recent: age = (datetime.now() - recent.created_at).total_seconds() if age < 60: raise HTTPException(status_code=429, detail=f"验证码发送过于频繁,请{int(60 - age)}秒后再试") code = f"{random.randint(100000, 999999)}" rec = SmsVerificationCode( id=gen_uuid(), phone=req.phone, code=code, purpose=purpose, expires_at=datetime.now() + timedelta(minutes=5), used=False, ) db.add(rec) db.commit() ok, msg, _ = send_sms_code(config, req.phone, code, purpose) if not ok: raise HTTPException(status_code=400, detail=f"短信发送失败:{msg}") return {"code": 0, "data": {"sent": True}, "message": "验证码已发送"} @router.post("/reset-password") def reset_password(req: ResetPasswordRequest, db: Session = Depends(get_db)): """通过短信验证码重置密码(公开接口)。""" user = db.query(User).filter(User.phone == req.phone).first() if not user: raise HTTPException(status_code=400, detail="该手机号未注册") vrec = db.query(SmsVerificationCode).filter( SmsVerificationCode.phone == req.phone, SmsVerificationCode.purpose == "reset_password", SmsVerificationCode.used == False, SmsVerificationCode.expires_at > datetime.now(), ).order_by(SmsVerificationCode.created_at.desc()).first() if not vrec or vrec.code != req.code: raise HTTPException(status_code=400, detail="验证码错误或已过期") user.password_hash = hash_password(req.new_password) user.updated_at = datetime.now() vrec.used = True LogService.log_operation( user_id=user.id, username=user.username, action="reset_password", target="user", detail=f"用户 {user.username} 通过短信验证码重置了密码", ) db.commit() return {"code": 0, "message": "密码重置成功"} @router.get("/me") def get_me(current_user: User = Depends(get_current_user)): return { "code": 0, "data": { "id": current_user.id, "username": current_user.username, "email": current_user.email or "", "phone": current_user.phone or "", "avatar": current_user.avatar or "", "role": current_user.role, "status": current_user.status, "real_name_status": current_user.real_name_status, "membership_level": current_user.membership_level, "membership_expire_at": str(current_user.membership_expire_at) if current_user.membership_expire_at else None, "auto_renew": current_user.auto_renew, "created_at": str(current_user.created_at) if current_user.created_at else None, }, "message": "ok", } @router.post("/logout") def logout(current_user: User = Depends(get_current_user)): LogService.log_operation( user_id=current_user.id, username=current_user.username, action="logout", target="user", detail=f"用户 {current_user.username} 登出", ) return {"code": 0, "message": "登出成功"} @router.get("/settings") def get_settings(db: Session = Depends(get_db)): settings = db.query(RegisterSetting).first() if not settings: return {"code": 0, "data": {}, "message": "ok"} return { "code": 0, "data": { "phone_required": settings.phone_required, "phone_sms_required": settings.phone_sms_required, "email_required": settings.email_required, "email_verify_required": settings.email_verify_required, "real_name_required": settings.real_name_required, }, "message": "ok", }