jiachenlong/backend/app/routers/auth.py

557 lines
18 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 fastapi import APIRouter, Depends, HTTPException, status, Body, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user
from app.core.logging_config import logger
from app.models.models import User
from app.schemas.schemas import Token, UserCreate, UserResponse
router = APIRouter(prefix="/api/auth", tags=["认证"])
def generate_user_code(db):
"""生成用户编码从201开始按自然数顺序递增跳过已存在的"""
# 查找最大的user_code
max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first()
if max_code and max_code[0]:
try:
num = int(max_code[0]) + 1
if num < 201:
num = 201
# 检查是否已存在,如果存在则继续递增
while db.query(User).filter(User.user_code == str(num)).first():
num += 1
return str(num)
except Exception:
pass
return "201"
@router.post("/register", response_model=UserResponse)
def register(user_data: UserCreate, request: Request, db: Session = Depends(get_db)):
"""用户注册"""
# 检查用户名是否已存在
existing_user = db.query(User).filter(User.f01_01_name == user_data.f01_01_name).first()
if existing_user:
logger.warning(
"用户注册失败:用户名已存在",
extra={
'ip_address': request.client.host if request.client else None,
'data': {'username': user_data.f01_01_name, 'reason': 'username_exists'}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="f01_01_name: 用户名已存在"
)
# 检查邮箱是否已存在
# 检查手机号是否已存在
if user_data.phone:
existing_phone = db.query(User).filter(User.phone == user_data.phone).first()
if existing_phone:
logger.warning(
"用户注册失败:手机号已被注册",
extra={
'ip_address': request.client.host if request.client else None,
'data': {'phone': user_data.phone[:3] + '****' + user_data.phone[7:], 'reason': 'phone_exists'}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="E00040:该手机号已被注册,请更换手机号"
)
if user_data.email:
existing_email = db.query(User).filter(User.email == user_data.email).first()
if existing_email:
logger.warning(
"用户注册失败:邮箱已被注册",
extra={
'ip_address': request.client.host if request.client else None,
'data': {'email': user_data.email, 'reason': 'email_exists'}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="E00041:该邮箱已被注册,请更换邮箱"
)
# 处理邀请码
invited_by_user = None
if user_data.invite_code:
# 查找邀请人
invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first()
if not invited_by_user:
logger.warning(
"用户注册失败:邀请码无效",
extra={
'ip_address': request.client.host if request.client else None,
'data': {'invite_code': user_data.invite_code, 'reason': 'invalid_invite_code'}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="E00042:邀请码无效"
)
# 创建用户
import uuid
hashed_password = get_password_hash(user_data.password)
generated_code = generate_user_code(db)
user = User(
f99_90_id=str(uuid.uuid4()),
f99_91_user_id=str(uuid.uuid4()),
user_code=generated_code,
f01_01_name=user_data.f01_01_name,
email=user_data.email,
phone=user_data.phone,
avatar=user_data.avatar,
address=user_data.address,
bio=user_data.bio,
password=hashed_password,
role="user"
)
db.add(user)
db.flush() # 确保获取user ID
# 更新邀请人、被邀请人的关联关系
if invited_by_user:
# 记录是被谁邀请的
user.f01_13_invite_code = invited_by_user.user_code
# 增加邀请人的邀请计数
invited_by_user.f99_101_invited_count = (invited_by_user.f99_101_invited_count or 0) + 1
# 生成自己的邀请码用自己的user_code
user.f01_13_invite_code = generated_code
db.commit()
db.refresh(user)
# 记录注册成功日志
logger.info(
"用户注册成功",
extra={
'user_id': user.f99_90_id,
'ip_address': request.client.host if request.client else None,
'data': {
'user_code': user.user_code,
'username': user.f01_01_name,
'invited_by': invited_by_user.user_code if invited_by_user else None
}
}
)
# 返回用户信息避免Pydantic序列化问题
return {
"id": user.f99_90_id,
"username": user.f01_01_name,
"user_code": user.user_code,
"email": user.email,
"phone": user.phone,
"avatar": user.avatar,
"role": user.role,
"level": user.f99_94_level,
"aiCount": user.f99_95_ai_count or 0,
"searchCount": user.f99_96_search_count or 0,
"collectionCount": user.f99_97_collection_count or 0
}
@router.post("/login", response_model=Token)
def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db),
request: Request = None
):
"""用户登录 - 支持用户名或用户编码登录"""
ip_address = request.client.host if request.client else None
# 先尝试用户名登录
user = db.query(User).filter(User.f01_01_name == form_data.username).first()
# 如果用户名不存在,尝试用户编码登录
if not user:
user = db.query(User).filter(User.user_code == form_data.username).first()
if not user:
logger.warning(
"用户登录失败:用户不存在",
extra={
'ip_address': ip_address,
'data': {'username': form_data.username, 'reason': 'user_not_found'}
}
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="E00011: 用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 验证密码
if not verify_password(form_data.password, user.password):
logger.warning(
"用户登录失败:密码错误",
extra={
'user_id': user.f99_90_id,
'ip_address': ip_address,
'data': {'username': form_data.username, 'reason': 'wrong_password'}
}
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="E00011: 用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 更新登录次数和最后登录时间
from datetime import datetime
user.f99_98_login_count = (user.f99_98_login_count or 0) + 1
user.f99_99_last_login = datetime.now()
db.commit()
# 记录登录成功日志
logger.info(
"用户登录成功",
extra={
'user_id': user.f99_90_id,
'ip_address': ip_address,
'data': {
'username': form_data.username,
'user_code': user.user_code,
'login_count': user.f99_98_login_count
}
}
)
# 生成 token
access_token = create_access_token(data={"sub": user.f99_90_id})
return {
"access_token": access_token,
"token_type": "bearer"
}
@router.get("/me", response_model=UserResponse)
def get_current_user_info(
current_user: User = Depends(get_current_user)
):
"""获取当前用户信息"""
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="未登录"
)
return UserResponse(
f99_90_id=current_user.f99_90_id,
f01_01_name=current_user.f01_01_name,
user_code=current_user.user_code,
email=current_user.email,
phone=current_user.phone,
avatar=current_user.avatar,
address=current_user.address,
bio=current_user.bio,
role=current_user.role,
level=current_user.f99_94_level,
aiCount=current_user.f99_95_ai_count or 0,
searchCount=current_user.f99_96_search_count or 0,
collectionCount=current_user.f99_97_collection_count or 0,
phoneVerified=current_user.f01_06_phone_verified or False,
loginCount=current_user.f99_98_login_count or 0,
lastLogin=current_user.f99_99_last_login,
gender=current_user.f01_07_gender,
birthday=current_user.f01_08_birthday,
region=current_user.f01_09_region,
realnameVerified=current_user.f01_10_realname_verified or False,
points=current_user.f99_100_points or 0,
balance=current_user.f01_11_balance or 0,
totalAmount=current_user.f01_12_total_amount or 0,
inviteCode=current_user.f01_13_invite_code,
invitedCount=current_user.f99_101_invited_count or 0,
created_at=current_user.f99_92_created_at,
updated_at=current_user.f99_93_updated_at,
)
@router.post("/change-password")
def change_password(
old_password: str = Body(...),
new_password: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""修改当前用户密码"""
from app.core.auth import verify_password, get_password_hash
# 在当前session中重新查询用户
user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first()
if not user:
logger.error(
"修改密码失败:用户不存在",
extra={
'user_id': current_user.f99_90_id,
'data': {'reason': 'user_not_found'}
}
)
raise HTTPException(status_code=404, detail="用户不存在")
# 验证旧密码
if not verify_password(old_password, user.password):
logger.warning(
"修改密码失败:旧密码错误",
extra={
'user_id': user.f99_90_id,
'data': {'reason': 'wrong_old_password'}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前密码错误"
)
# 更新密码
user.password = get_password_hash(new_password)
db.commit()
# 记录密码修改成功日志
logger.info(
"用户密码修改成功",
extra={
'user_id': user.f99_90_id,
'data': {'action': 'change_password'}
}
)
return {"message": "密码修改成功"}
# ============ 短信验证码接口 ============
@router.post("/send-verification-code")
def send_verification_code(
phone: str = Body(..., min_length=11, max_length=11),
purpose: str = Body("register"), # register | login | reset_password
request: Request = None
):
"""发送短信验证码"""
from app.services.sms import send_verification_code as send_sms
from app.core.logging_config import logger
ip_address = request.client.host if request and request.client else None
# 验证手机号格式
if not phone.startswith("1") or len(phone) != 11:
logger.warning(
"发送验证码失败:手机号格式不正确",
extra={
'ip_address': ip_address,
'data': {'phone': phone, 'purpose': purpose, 'reason': 'invalid_format'}
}
)
return {"success": False, "message": "手机号格式不正确"}
result = send_sms(phone)
if result["success"]:
# 记录验证码发送成功日志(不记录验证码本身)
logger.info(
"验证码发送成功",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose
}
}
)
return {
"success": True,
"message": f"验证码已发送到 {phone[:3]}****{phone[7:]}",
"expire": result.get("expire", 300)
}
else:
# 记录验证码发送失败日志
logger.error(
"验证码发送失败",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose,
'reason': result.get('message', 'unknown_error')
}
}
)
return result
@router.post("/verify-code")
def verify_code(
phone: str = Body(...),
code: str = Body(..., min_length=6, max_length=6),
new_password: str = Body(None, min_length=6), # 可选:新密码(用于重置密码)
purpose: str = Body("verify"), # verify | reset_password
request: Request = None,
db: Session = Depends(get_db)
):
"""
验证短信验证码
- purpose=verify: 仅验证验证码有效性
- purpose=reset_password: 验证并重置密码需提供new_password
"""
from app.services.sms import verify_code as check_code
from app.core.auth import get_password_hash
from app.core.logging_config import logger
ip_address = request.client.host if request and request.client else None
is_valid = check_code(phone, code)
if not is_valid:
logger.warning(
"验证码验证失败",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose,
'reason': 'invalid_or_expired_code'
}
}
)
return {"success": False, "message": "验证码错误或已过期"}
# 验证成功后处理
if purpose == "reset_password":
if not new_password:
return {"success": False, "message": "重置密码需要提供新密码"}
# 查找该手机号的用户
user = db.query(User).filter(User.phone == phone).first()
if not user:
logger.warning(
"密码重置失败:手机号未注册",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'reason': 'user_not_found'
}
}
)
return {"success": False, "message": "该手机号未注册"}
# 更新密码
user.password = get_password_hash(new_password)
db.commit()
# 记录密码重置成功日志
logger.info(
"密码重置成功",
extra={
'user_id': user.f99_90_id,
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'action': 'reset_password_by_verify_code'
}
}
)
return {"success": True, "message": "密码重置成功"}
# 记录验证成功日志
logger.info(
"验证码验证成功",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose
}
}
)
return {"success": True, "message": "验证成功"}
@router.post("/reset-password")
def reset_password(
phone: str = Body(...),
code: str = Body(..., min_length=6, max_length=6),
new_password: str = Body(..., min_length=6),
request: Request = None,
db: Session = Depends(get_db)
):
"""
重置密码(通过短信验证码)
流程:
1. 调用 /send-verification-code 发送验证码purpose=reset_password
2. 调用 /reset-password 重置密码
"""
from app.services.sms import verify_code as check_code
from app.core.auth import get_password_hash
from app.core.logging_config import logger
ip_address = request.client.host if request and request.client else None
# 验证验证码
is_valid = check_code(phone, code)
if not is_valid:
logger.warning(
"密码重置失败:验证码错误",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'reason': 'invalid_verification_code'
}
}
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="验证码错误或已过期"
)
# 查找该手机号的用户
user = db.query(User).filter(User.phone == phone).first()
if not user:
logger.warning(
"密码重置失败:用户不存在",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'reason': 'user_not_found'
}
}
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="该手机号未注册"
)
# 更新密码
user.password = get_password_hash(new_password)
db.commit()
# 记录密码重置成功日志
logger.info(
"密码重置成功",
extra={
'user_id': user.f99_90_id,
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'action': 'reset_password'
}
}
)
return {"message": "密码重置成功"}