2025-12-16 18:06:50 +08:00
|
|
|
|
"""
|
|
|
|
|
|
用户服务
|
|
|
|
|
|
"""
|
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
from sqlalchemy import and_, or_, desc
|
|
|
|
|
|
from typing import List, Optional, Tuple
|
|
|
|
|
|
from ..models.user import User, Transaction, UserRole, UserStatus
|
|
|
|
|
|
from ..schemas.user import UserCreate, UserUpdate, ChangePasswordRequest
|
|
|
|
|
|
from ..core.security import get_password_hash, verify_password
|
|
|
|
|
|
from ..core.config import settings
|
|
|
|
|
|
|
|
|
|
|
|
# 获取游戏配置
|
|
|
|
|
|
game_settings = settings.game
|
|
|
|
|
|
from ..utils.redis import redis_client
|
|
|
|
|
|
from datetime import datetime
|
2025-12-16 19:03:48 +08:00
|
|
|
|
from ..services.system_service import SystemService
|
|
|
|
|
|
from ..models.system import ConfigType
|
2025-12-16 18:06:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserService:
|
|
|
|
|
|
"""用户服务"""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def create_user(db: Session, user_data: UserCreate) -> User:
|
|
|
|
|
|
"""
|
|
|
|
|
|
创建用户
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 检查用户名和邮箱是否已存在
|
|
|
|
|
|
existing_user = db.query(User).filter(
|
|
|
|
|
|
(User.username == user_data.username) | (User.email == user_data.email)
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if existing_user:
|
|
|
|
|
|
raise ValueError("用户名或邮箱已存在")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查密码长度(虽然前端已检查,但后端也要验证)
|
|
|
|
|
|
if len(user_data.password) > 72:
|
|
|
|
|
|
raise ValueError("密码长度不能超过72个字符")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建用户
|
|
|
|
|
|
hashed_password = get_password_hash(user_data.password)
|
|
|
|
|
|
db_user = User(
|
|
|
|
|
|
username=user_data.username,
|
|
|
|
|
|
email=user_data.email,
|
|
|
|
|
|
hashed_password=hashed_password,
|
|
|
|
|
|
)
|
|
|
|
|
|
db.add(db_user)
|
|
|
|
|
|
db.commit() # 先提交用户,获取ID
|
|
|
|
|
|
db.refresh(db_user) # 刷新对象,获取自增ID
|
|
|
|
|
|
|
|
|
|
|
|
# 记录注册奖励交易
|
|
|
|
|
|
db.add(Transaction(
|
|
|
|
|
|
user_id=db_user.id,
|
|
|
|
|
|
type="注册奖励",
|
|
|
|
|
|
amount=game_settings.NEW_USER_REWARD,
|
|
|
|
|
|
balance_after=db_user.balance,
|
|
|
|
|
|
description="新用户注册奖励"
|
|
|
|
|
|
))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return db_user
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def authenticate_user(db: Session, username: str, password: str) -> User:
|
|
|
|
|
|
"""
|
|
|
|
|
|
验证用户
|
|
|
|
|
|
"""
|
|
|
|
|
|
user = db.query(User).filter(User.username == username).first()
|
|
|
|
|
|
if not user or not verify_password(password, user.hashed_password):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_user_by_id(db: Session, user_id: int) -> User:
|
|
|
|
|
|
"""
|
|
|
|
|
|
根据ID获取用户
|
|
|
|
|
|
"""
|
|
|
|
|
|
return db.query(User).filter(User.id == user_id).first()
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_user_by_username(db: Session, username: str) -> User:
|
|
|
|
|
|
"""
|
|
|
|
|
|
根据用户名获取用户
|
|
|
|
|
|
"""
|
|
|
|
|
|
return db.query(User).filter(User.username == username).first()
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def update_user(db: Session, user: User, user_data: UserUpdate) -> User:
|
|
|
|
|
|
"""
|
|
|
|
|
|
更新用户信息
|
|
|
|
|
|
"""
|
|
|
|
|
|
update_data = user_data.dict(exclude_unset=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果更新邮箱,检查邮箱是否已存在
|
|
|
|
|
|
if "email" in update_data and update_data["email"] != user.email:
|
|
|
|
|
|
existing = db.query(User).filter(
|
|
|
|
|
|
User.email == update_data["email"],
|
|
|
|
|
|
User.id != user.id
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
raise ValueError("邮箱已存在")
|
|
|
|
|
|
|
|
|
|
|
|
for field, value in update_data.items():
|
|
|
|
|
|
setattr(user, field, value)
|
|
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(user)
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_user_list(
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
skip: int = 0,
|
|
|
|
|
|
limit: int = 20,
|
|
|
|
|
|
search: Optional[str] = None,
|
|
|
|
|
|
role: Optional[str] = None,
|
|
|
|
|
|
status: Optional[str] = None,
|
|
|
|
|
|
sort_by: str = "created_at",
|
|
|
|
|
|
order: str = "desc"
|
|
|
|
|
|
) -> List[User]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取用户列表(管理员)
|
|
|
|
|
|
"""
|
|
|
|
|
|
query = db.query(User)
|
|
|
|
|
|
|
|
|
|
|
|
# 搜索过滤
|
|
|
|
|
|
if search:
|
|
|
|
|
|
query = query.filter(
|
|
|
|
|
|
or_(
|
|
|
|
|
|
User.username.like(f"%{search}%"),
|
|
|
|
|
|
User.email.like(f"%{search}%"),
|
|
|
|
|
|
User.nickname.like(f"%{search}%")
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 角色过滤
|
|
|
|
|
|
if role:
|
|
|
|
|
|
query = query.filter(User.role == role)
|
|
|
|
|
|
|
|
|
|
|
|
# 状态过滤
|
|
|
|
|
|
if status:
|
|
|
|
|
|
query = query.filter(User.status == status)
|
|
|
|
|
|
|
|
|
|
|
|
# 排序
|
|
|
|
|
|
sort_field = getattr(User, sort_by, User.created_at)
|
|
|
|
|
|
if order.lower() == "desc":
|
|
|
|
|
|
query = query.order_by(desc(sort_field))
|
|
|
|
|
|
else:
|
|
|
|
|
|
query = query.order_by(sort_field)
|
|
|
|
|
|
|
|
|
|
|
|
return query.offset(skip).limit(limit).all()
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def adjust_balance_with_version(
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
user: User,
|
|
|
|
|
|
amount: int,
|
|
|
|
|
|
description: str,
|
|
|
|
|
|
admin_user: User,
|
|
|
|
|
|
expected_version: Optional[int] = None
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
调整用户余额(使用乐观锁)
|
|
|
|
|
|
"""
|
2025-12-16 19:03:48 +08:00
|
|
|
|
# 记录变更前的余额
|
|
|
|
|
|
old_balance = user.balance
|
|
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
# 如果指定了版本号,进行版本检查
|
|
|
|
|
|
if expected_version is not None and user.version != expected_version:
|
|
|
|
|
|
raise ValueError("用户数据已更新,请刷新后重试")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新余额和版本
|
|
|
|
|
|
user.balance += amount
|
|
|
|
|
|
user.version += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 通知用户余额更新
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试获取当前事件循环
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
# 局部导入避免循环导入
|
|
|
|
|
|
from ..routers.websocket import notify_user_balance_update
|
|
|
|
|
|
loop.create_task(notify_user_balance_update(user.id, user.balance))
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
# 如果没有运行中的事件循环,则在新线程中处理
|
|
|
|
|
|
import threading
|
|
|
|
|
|
def run_async():
|
|
|
|
|
|
async def _run():
|
|
|
|
|
|
# 局部导入避免循环导入
|
|
|
|
|
|
from ..routers.websocket import notify_user_balance_update
|
|
|
|
|
|
await notify_user_balance_update(user.id, user.balance)
|
|
|
|
|
|
asyncio.run(_run())
|
|
|
|
|
|
thread = threading.Thread(target=run_async, daemon=True)
|
|
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
# 记录交易
|
|
|
|
|
|
transaction = Transaction(
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
type="管理员调整",
|
|
|
|
|
|
amount=amount,
|
|
|
|
|
|
balance_after=user.balance,
|
|
|
|
|
|
description=f"{description} (操作人: {admin_user.username})"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
db.add(transaction)
|
|
|
|
|
|
db.add(user)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
2025-12-16 19:03:48 +08:00
|
|
|
|
# 余额变更后,触发余额监控服务
|
|
|
|
|
|
from ..services.balance_monitor_service import BalanceMonitorService
|
|
|
|
|
|
BalanceMonitorService.on_balance_changed(db, user, old_balance, user.balance)
|
|
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def create_transaction(
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
user_id: int,
|
|
|
|
|
|
transaction_type: str,
|
|
|
|
|
|
amount: int,
|
|
|
|
|
|
balance_after: int,
|
|
|
|
|
|
description: str,
|
|
|
|
|
|
related_id: Optional[int] = None
|
|
|
|
|
|
) -> Transaction:
|
|
|
|
|
|
"""
|
|
|
|
|
|
创建交易记录
|
|
|
|
|
|
"""
|
|
|
|
|
|
transaction = Transaction(
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
type=transaction_type,
|
|
|
|
|
|
amount=amount,
|
|
|
|
|
|
balance_after=balance_after,
|
|
|
|
|
|
description=description,
|
|
|
|
|
|
related_id=related_id
|
|
|
|
|
|
)
|
|
|
|
|
|
db.add(transaction)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(transaction)
|
|
|
|
|
|
return transaction
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_user_transactions(
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
user_id: int,
|
|
|
|
|
|
skip: int = 0,
|
|
|
|
|
|
limit: int = 50
|
|
|
|
|
|
) -> List[Transaction]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取用户交易记录
|
|
|
|
|
|
"""
|
|
|
|
|
|
return db.query(Transaction).filter(
|
|
|
|
|
|
Transaction.user_id == user_id
|
|
|
|
|
|
).order_by(desc(Transaction.created_at)).offset(skip).limit(limit).all()
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_user_transactions_paginated(
|
|
|
|
|
|
db: Session,
|
|
|
|
|
|
user_id: int,
|
|
|
|
|
|
skip: int = 0,
|
|
|
|
|
|
limit: int = 20
|
|
|
|
|
|
) -> List[Transaction]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
分页获取用户交易记录(用于管理员界面)
|
|
|
|
|
|
"""
|
|
|
|
|
|
return db.query(Transaction).filter(
|
|
|
|
|
|
Transaction.user_id == user_id
|
|
|
|
|
|
).order_by(desc(Transaction.created_at)).offset(skip).limit(limit).all()
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def claim_daily_allowance(db: Session, user: User) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
领取每日低保
|
|
|
|
|
|
"""
|
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
|
allowance_key = f"daily_allowance:{user.id}:{today.isoformat()}"
|
2025-12-16 19:03:48 +08:00
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
# 检查今日是否已领取
|
|
|
|
|
|
if redis_client.exists(allowance_key):
|
|
|
|
|
|
return False
|
2025-12-16 19:03:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 从数据库获取低保金额
|
|
|
|
|
|
daily_allowance = UserService.get_daily_allowance(db)
|
|
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
# 发放低保
|
2025-12-16 19:03:48 +08:00
|
|
|
|
user.balance += daily_allowance
|
2025-12-16 18:06:50 +08:00
|
|
|
|
user.version += 1
|
|
|
|
|
|
db.commit()
|
2025-12-16 19:03:48 +08:00
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
# 通知用户余额更新
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试获取当前事件循环
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
# 局部导入避免循环导入
|
|
|
|
|
|
from ..routers.websocket import notify_user_balance_update
|
|
|
|
|
|
loop.create_task(notify_user_balance_update(user.id, user.balance))
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
# 如果没有运行中的事件循环,则在新线程中处理
|
|
|
|
|
|
import threading
|
|
|
|
|
|
def run_async():
|
|
|
|
|
|
async def _run():
|
|
|
|
|
|
# 局部导入避免循环导入
|
|
|
|
|
|
from ..routers.websocket import notify_user_balance_update
|
|
|
|
|
|
await notify_user_balance_update(user.id, user.balance)
|
|
|
|
|
|
asyncio.run(_run())
|
|
|
|
|
|
thread = threading.Thread(target=run_async, daemon=True)
|
|
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
# 记录交易
|
|
|
|
|
|
UserService.create_transaction(
|
|
|
|
|
|
db=db,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
transaction_type="低保",
|
2025-12-16 19:03:48 +08:00
|
|
|
|
amount=daily_allowance,
|
2025-12-16 18:06:50 +08:00
|
|
|
|
balance_after=user.balance,
|
|
|
|
|
|
description="每日低保"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置领取标记(24小时过期)
|
|
|
|
|
|
redis_client.setex(allowance_key, 86400, "claimed")
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_rich_ranking(db: Session, limit: int = 10) -> List[User]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取富豪榜
|
|
|
|
|
|
"""
|
|
|
|
|
|
return db.query(User).filter(
|
|
|
|
|
|
User.is_active == True
|
|
|
|
|
|
).order_by(desc(User.balance)).limit(limit).all()
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_next_allowance_time(db: Session, user: User) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取下次低保领取时间
|
|
|
|
|
|
"""
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
|
allowance_key = f"daily_allowance:{user.id}:{today.isoformat()}"
|
2025-12-16 19:03:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 从数据库获取低保金额
|
|
|
|
|
|
daily_allowance = UserService.get_daily_allowance(db)
|
|
|
|
|
|
|
2025-12-16 18:06:50 +08:00
|
|
|
|
# 检查今日是否已领取
|
|
|
|
|
|
if redis_client.exists(allowance_key):
|
|
|
|
|
|
# 如果今天已领取,下次领取时间为明天
|
|
|
|
|
|
tomorrow = today + timedelta(days=1)
|
|
|
|
|
|
next_claim_time = datetime.combine(tomorrow, datetime.min.time())
|
|
|
|
|
|
return {
|
|
|
|
|
|
"can_claim": False,
|
|
|
|
|
|
"next_claim_time": next_claim_time.isoformat(),
|
2025-12-16 19:03:48 +08:00
|
|
|
|
"daily_allowance": daily_allowance
|
2025-12-16 18:06:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果今天未领取,可以立即领取
|
|
|
|
|
|
return {
|
|
|
|
|
|
"can_claim": True,
|
|
|
|
|
|
"next_claim_time": datetime.now().isoformat(),
|
2025-12-16 19:03:48 +08:00
|
|
|
|
"daily_allowance": daily_allowance
|
2025-12-16 18:06:50 +08:00
|
|
|
|
}
|
2025-12-16 19:03:48 +08:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_daily_allowance(db: Session) -> int:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从数据库获取每日低保金额
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = SystemService.get_config(db, "GAME_DAILY_ALLOWANCE")
|
|
|
|
|
|
if config:
|
|
|
|
|
|
typed_value = SystemService.get_typed_value(config)
|
|
|
|
|
|
return int(typed_value)
|
|
|
|
|
|
# 如果数据库中没有配置,返回默认值
|
|
|
|
|
|
return game_settings.DAILY_ALLOWANCE
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_balance_zero_reward(db: Session) -> int:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从数据库获取余额清零自动发放金额
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = SystemService.get_config(db, "BALANCE_ZERO_REWARD_AMOUNT")
|
|
|
|
|
|
if config:
|
|
|
|
|
|
typed_value = SystemService.get_typed_value(config)
|
|
|
|
|
|
return int(typed_value)
|
|
|
|
|
|
# 如果数据库中没有配置,返回默认值10000分
|
|
|
|
|
|
return 10000
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def get_allowance_reset_time(db: Session) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从数据库获取低保每日刷新时间
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = SystemService.get_config(db, "ALLOWANCE_RESET_TIME")
|
|
|
|
|
|
if config:
|
|
|
|
|
|
return config.config_value
|
|
|
|
|
|
# 如果数据库中没有配置,返回默认值
|
|
|
|
|
|
return "00:00"
|