jiachenlong/backend/app/routers/auth.py

557 lines
18 KiB
Python
Raw Normal View History

2026-03-23 11:08:52 +08:00
# 认证路由 - 使用字段编码
from fastapi import APIRouter, Depends, HTTPException, status, Body, Request
2026-03-23 11:08:52 +08:00
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
2026-03-23 11:08:52 +08:00
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开始按自然数顺序递增跳过已存在的"""
2026-03-23 11:08:52 +08:00
# 查找最大的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
2026-04-09 00:41:21 +08:00
if num < 201:
num = 201
# 检查是否已存在,如果存在则继续递增
while db.query(User).filter(User.user_code == str(num)).first():
num += 1
2026-04-09 00:41:21 +08:00
return str(num)
except Exception:
2026-03-23 11:08:52 +08:00
pass
2026-04-09 00:41:21 +08:00
return "201"
2026-03-23 11:08:52 +08:00
@router.post("/register", response_model=UserResponse)
def register(user_data: UserCreate, request: Request, db: Session = Depends(get_db)):
2026-03-23 11:08:52 +08:00
"""用户注册"""
# 检查用户名是否已存在
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'}
}
)
2026-03-23 11:08:52 +08:00
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:该手机号已被注册,请更换手机号"
)
2026-03-23 11:08:52 +08:00
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'}
}
)
2026-03-23 11:08:52 +08:00
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="E00041:该邮箱已被注册,请更换邮箱"
2026-03-23 11:08:52 +08:00
)
# 处理邀请码
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:邀请码无效"
)
2026-03-23 11:08:52 +08:00
# 创建用户
import uuid
hashed_password = get_password_hash(user_data.password)
generated_code = generate_user_code(db)
2026-03-23 11:08:52 +08:00
user = User(
f99_90_id=str(uuid.uuid4()),
f99_91_user_id=str(uuid.uuid4()),
user_code=generated_code,
2026-03-23 11:08:52 +08:00
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
2026-03-23 11:08:52 +08:00
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
}
2026-03-23 11:08:52 +08:00
@router.post("/login", response_model=Token)
def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db),
request: Request = None
2026-03-23 11:08:52 +08:00
):
2026-04-09 00:45:19 +08:00
"""用户登录 - 支持用户名或用户编码登录"""
ip_address = request.client.host if request.client else None
2026-04-09 00:45:19 +08:00
# 先尝试用户名登录
2026-03-23 11:08:52 +08:00
user = db.query(User).filter(User.f01_01_name == form_data.username).first()
2026-04-09 00:45:19 +08:00
# 如果用户名不存在,尝试用户编码登录
if not user:
user = db.query(User).filter(User.user_code == form_data.username).first()
2026-03-23 11:08:52 +08:00
if not user:
logger.warning(
"用户登录失败:用户不存在",
extra={
'ip_address': ip_address,
'data': {'username': form_data.username, 'reason': 'user_not_found'}
}
)
2026-03-23 11:08:52 +08:00
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'}
}
)
2026-03-23 11:08:52 +08:00
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
}
}
)
2026-03-23 11:08:52 +08:00
# 生成 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(
2026-04-12 09:49:18 +08:00
current_user: User = Depends(get_current_user)
2026-03-23 11:08:52 +08:00
):
"""获取当前用户信息"""
2026-04-12 09:49:18 +08:00
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,
2026-03-23 11:08:52 +08:00
)
@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'}
}
)
2026-03-23 11:08:52 +08:00
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'}
}
)
2026-03-23 11:08:52 +08:00
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'}
}
)
2026-03-23 11:08:52 +08:00
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
2026-03-23 11:08:52 +08:00
):
"""发送短信验证码"""
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
2026-03-23 11:08:52 +08:00
# 验证手机号格式
if not phone.startswith("1") or len(phone) != 11:
logger.warning(
"发送验证码失败:手机号格式不正确",
extra={
'ip_address': ip_address,
'data': {'phone': phone, 'purpose': purpose, 'reason': 'invalid_format'}
}
)
2026-03-23 11:08:52 +08:00
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
}
}
)
2026-03-23 11:08:52 +08:00
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')
}
}
)
2026-03-23 11:08:52 +08:00
return result
@router.post("/verify-code")
def verify_code(
phone: str = Body(...),
2026-04-12 09:49:18 +08:00
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,
2026-04-12 09:49:18 +08:00
db: Session = Depends(get_db)
2026-03-23 11:08:52 +08:00
):
2026-04-12 09:49:18 +08:00
"""
验证短信验证码
- purpose=verify: 仅验证验证码有效性
- purpose=reset_password: 验证并重置密码需提供new_password
"""
2026-03-23 11:08:52 +08:00
from app.services.sms import verify_code as check_code
2026-04-12 09:49:18 +08:00
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
2026-03-23 11:08:52 +08:00
is_valid = check_code(phone, code)
2026-04-12 09:49:18 +08:00
if not is_valid:
logger.warning(
"验证码验证失败",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose,
'reason': 'invalid_or_expired_code'
}
}
)
2026-03-23 11:08:52 +08:00
return {"success": False, "message": "验证码错误或已过期"}
2026-04-12 09:49:18 +08:00
# 验证成功后处理
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'
}
}
)
2026-04-12 09:49:18 +08:00
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'
}
}
)
2026-04-12 09:49:18 +08:00
return {"success": True, "message": "密码重置成功"}
# 记录验证成功日志
logger.info(
"验证码验证成功",
extra={
'ip_address': ip_address,
'data': {
'phone': phone[:3] + '****' + phone[7:],
'purpose': purpose
}
}
)
2026-04-12 09:49:18 +08:00
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,
2026-04-12 09:49:18 +08:00
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
2026-04-12 09:49:18 +08:00
# 验证验证码
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'
}
}
)
2026-04-12 09:49:18 +08:00
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'
}
}
)
2026-04-12 09:49:18 +08:00
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'
}
}
)
2026-04-12 09:49:18 +08:00
return {"message": "密码重置成功"}