# 认证路由 - 使用字段编码 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": "密码重置成功"}