Files
Django/backend/utils.py

491 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.
# houtai/utils.py
import logging
import json
from functools import lru_cache
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from rest_framework.response import Response
from .models import AbnormalUserLog, PlayerDailyStats, MerchantDailyStats, \
ManagerRenewalDailyStats, ManagerDailyStats, LeaderDailyStats, WithdrawalDailyStats
from gvsdsdk.models import UserRole as SdkUserRole, RolePermission as SdkRolePermission, Permission as SdkPermission
from django.db import transaction
from django.db.models import F
from datetime import date
from decimal import Decimal
from gvsdsdk.fluent import db, func, FQ
logger = logging.getLogger(__name__)
def _get_real_ip(request):
"""获取真实IP考虑代理"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
return ip, x_forwarded_for
else:
ip = request.META.get('REMOTE_ADDR')
return ip, ''
def _record_abnormal(account_id, request, attempt_type, detail=''):
"""记录异常操作(仅用于越权等攻击行为)"""
try:
real_ip, xff = _get_real_ip(request)
headers = {k: v for k, v in request.META.items() if k.startswith('HTTP_')}
headers_json = json.dumps(headers, ensure_ascii=False)[:2000]
request_params = json.dumps(request.query_params.dict(), ensure_ascii=False) if request.query_params else ''
request_body = request.body.decode('utf-8', errors='ignore')[:5000]
AbnormalUserLog.query.create(
account_id=account_id,
ip_address=real_ip,
real_ip=real_ip,
x_forwarded_for=xff,
user_agent=request.META.get('HTTP_USER_AGENT', ''),
request_method=request.method,
request_path=request.path,
request_params=request_params,
request_body=request_body,
request_headers=headers_json,
attempt_type=attempt_type,
detail=detail,
)
logger.warning(f"记录异常用户: {account_id} IP:{real_ip} 类型:{attempt_type}")
except Exception as e:
logger.error(f"记录异常日志失败: {e}")
def verify_kefu_permission(request, username_from_frontend=None):
"""
验证客服/管理员身份并返回该用户的所有权限列表基于新RBAC表
只有前端传递的username与当前登录用户不匹配时才记录异常并警告前端。
参数:
request: DRF request 对象
username_from_frontend: 前端传递的账号(用于防越权)
返回:
(kefu_obj, permissions_list) # 验证通过
(None, Response) # 验证失败直接返回Response
"""
current_user = request.user
# ---------- 1. 必须是客服或管理员 ----------
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if current_user.UserType != 'kefu' and not is_admin:
return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403)
# ---------- 2. 客服扩展表存在且状态正常(管理员跳过) ----------
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return None, Response({'code': 403, 'msg': '客服账号已被禁用'}, status=403)
except ObjectDoesNotExist:
return None, Response({'code': 403, 'msg': '客服账号不存在'}, status=403)
# ---------- 3. 越权检测只记录前端篡改username的情况 ----------
if username_from_frontend and current_user.Phone != username_from_frontend:
detail = f'前端username: {username_from_frontend},实际登录账号: {current_user.Phone}'
_record_abnormal(current_user.Phone, request, '越权尝试篡改username', detail=detail)
return None, Response({
'code': 403,
'msg': '身份错误,无权操作其他账号,相关操作已记录'
}, status=403)
# ---------- 4. 获取用户的所有权限 ----------
# 使用 gvsdsdk RBAC 表UserRole → RolePermission → Permission
try:
user_uuid = current_user.UserUUID
role_uuids = list(SdkUserRole.objects.filter(
UserUUID=user_uuid
).values_list('RoleUUID', flat=True))
if role_uuids:
perm_uuids = list(SdkRolePermission.objects.filter(
RoleUUID__in=role_uuids
).values_list('PermUUID', flat=True))
permissions = list(SdkPermission.objects.filter(
PermUUID__in=perm_uuids,
PermStatus=1
).values_list('PermCode', flat=True).distinct())
else:
permissions = []
except Exception:
permissions = []
# 超级用户自动拥有所有权限
if current_user.IsSuperuser and '000001' not in permissions:
permissions.insert(0, '000001')
# 拥有 000001超级管理权限的用户自动获得所有功能权限码
if '000001' in permissions:
_ALL_FEATURE_PERMS = {
'bankuai', 'caiwu', 'huiyuancaiwu', 'dingdancaiwu',
'qtczcaiwu', 'kaohepeizhi',
}
for p in _ALL_FEATURE_PERMS:
if p not in permissions:
permissions.append(p)
if not permissions:
logger.warning(f"用户 {current_user.Phone} 没有任何权限,请检查角色分配")
return kefu, permissions
def update_dashou_daily_by_action(yonghuid, amount, action):
"""
根据行为类型更新打手每日统计(接单、成交、退款)
参数:
yonghuid: 打手用户ID
amount: 本单涉及的金额Decimal
action: 1 = 接单, 2 = 成交(结算), 3 = 退款
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return # 金额非正不统计
today = date.today()
with transaction.atomic():
stat, created = PlayerDailyStats.objects.select_for_update().get_or_create(
PlayerID=yonghuid,
Date=today,
defaults={
'PlayerID': yonghuid,
'Date': today,
'AcceptedOrderTotal': 0,
'AcceptedAmount': Decimal('0.00'),
'CompletedOrderTotal': 0,
'CompletedAmount': Decimal('0.00'),
'RefundCount': 0,
'RefundAmount': Decimal('0.00'),
}
)
update_fields = {}
if action == 1: # 接单
update_fields['AcceptedOrderTotal'] = F('AcceptedOrderTotal') + 1
update_fields['AcceptedAmount'] = F('AcceptedAmount') + amount
elif action == 2: # 成交
update_fields['CompletedOrderTotal'] = F('CompletedOrderTotal') + 1
update_fields['CompletedAmount'] = F('CompletedAmount') + amount
elif action == 3: # 退款
update_fields['RefundCount'] = F('RefundCount') + 1
update_fields['RefundAmount'] = F('RefundAmount') + amount
else:
return # 无效行为
if update_fields:
PlayerDailyStats.query.filter(id=stat.id).update(**update_fields)
def update_shangjia_daily(yonghuid, amount, action):
"""
根据操作行为更新商家每日统计(派发、结算、退款)
参数:
yonghuid: 商家用户ID
amount: 当前订单涉及的金额Decimal
action: 操作类型1 = 派发2 = 结算成交3 = 退款
说明:
- 金额必须大于0否则不执行任何操作
- 使用 select_for_update + F() 表达式保证原子性和并发安全
- 每天每个商家只会有一条统计记录,不存在则自动创建
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return # 金额非正,不统计
today = date.today()
with transaction.atomic():
# 获取或创建当天统计记录,同时加锁防止并发
stat, created = MerchantDailyStats.objects.select_for_update().get_or_create(
MerchantID=yonghuid,
Date=today,
defaults={
'MerchantID': yonghuid,
'Date': today,
'AssignedOrderCount': 0,
'AssignedAmount': Decimal('0.00'),
'SettledOrderCount': 0,
'SettledAmount': Decimal('0.00'),
'RefundOrderCount': 0,
'RefundAmount': Decimal('0.00'),
}
)
# 构建本次更新的字段,使用 F 表达式进行原子累加
update_fields = {}
if action == 1: # 派发
update_fields['AssignedOrderCount'] = F('AssignedOrderCount') + 1
update_fields['AssignedAmount'] = F('AssignedAmount') + amount
elif action == 2: # 结算(成交)
update_fields['SettledOrderCount'] = F('SettledOrderCount') + 1
update_fields['SettledAmount'] = F('SettledAmount') + amount
elif action == 3: # 退款
update_fields['RefundOrderCount'] = F('RefundOrderCount') + 1
update_fields['RefundAmount'] = F('RefundAmount') + amount
else:
# 无效的行为类型,直接返回
return
# 执行原子更新
MerchantDailyStats.query.filter(id=stat.id).update(**update_fields)
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
"""
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红
"""
today = date.today()
with transaction.atomic():
stat, created = ManagerDailyStats.objects.select_for_update().get_or_create(
ManagerID=yonghuid,
Date=today,
defaults={
'ManagerID': yonghuid,
'Date': today,
'InvitedPlayerCount': 0,
'RechargedPlayerCount': 0,
'TotalIncome': Decimal('0.00'),
}
)
if action == 1:
# 邀请打手:无论新创建还是已存在,直接 +1
if created:
stat.InvitedPlayerCount = 1
stat.save()
else:
ManagerDailyStats.query.filter(id=stat.id).update(
InvitedPlayerCount=F('InvitedPlayerCount') + 1
)
elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
if created:
stat.RechargedPlayerCount = 1
stat.TotalIncome = amount
stat.save()
else:
ManagerDailyStats.query.filter(id=stat.id).update(
RechargedPlayerCount=F('RechargedPlayerCount') + 1,
TotalIncome=F('TotalIncome') + amount
)
elif action in (3, 4):
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
if created:
stat.TotalIncome = amount
stat.save()
else:
ManagerDailyStats.query.filter(id=stat.id).update(
TotalIncome=F('TotalIncome') + amount
)
def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')):
"""
管事续费统计(单独表)
用法: update_guanshi_xufei_daily('300001', Decimal('30'))
"""
today = date.today()
with transaction.atomic():
stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create(
ManagerID=yonghuid,
Date=today,
defaults={
'ManagerID': yonghuid,
'Date': today,
'RenewalTotal': 1,
'RenewalRevenue': xufei_jine,
}
)
if not created:
ManagerRenewalDailyStats.query.filter(id=stat.id).update(
RenewalTotal=F('RenewalTotal') + 1,
RenewalRevenue=F('RenewalRevenue') + xufei_jine
)
def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
"""
根据操作行为更新组长每日统计(邀请管事、分佣收入)
参数:
yonghuid: 组长用户ID
action: 操作类型
1 = 邀请管事(次数+1无金额
2 = 分佣收入(累加金额)
amount: 分佣金额Decimal仅在 action=2 时有效
"""
today = date.today()
with transaction.atomic():
stat, created = LeaderDailyStats.objects.select_for_update().get_or_create(
LeaderID=yonghuid,
Date=today,
defaults={
'LeaderID': yonghuid,
'Date': today,
'InvitedManagerCount': 0,
'TotalIncome': Decimal('0.00'),
'CommissionAmount': Decimal('0.00'),
}
)
if created:
# 新记录直接设置初始值
if action == 1:
stat.InvitedManagerCount = 1
elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00')
stat.TotalIncome = amount
stat.CommissionAmount = amount
stat.save()
else:
# 已存在记录,使用 F 表达式原子累加
if action == 1:
LeaderDailyStats.query.filter(id=stat.id).update(
InvitedManagerCount=F('InvitedManagerCount') + 1
)
elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount > 0:
LeaderDailyStats.query.filter(id=stat.id).update(
TotalIncome=F('TotalIncome') + amount,
CommissionAmount=F('CommissionAmount') + amount
)
def update_tixian_daily_stat(leixing, amount):
"""
更新每日提现统计(平台维度)
leixing: 1=打手, 2=管事, 3=组长
amount: 本次提现金额(申请金额,含手续费)
"""
today = date.today()
with transaction.atomic():
stat, created = WithdrawalDailyStats.objects.select_for_update().get_or_create(
Date=today,
WithdrawalType=leixing,
defaults={'total_amount': amount}
)
if not created:
WithdrawalDailyStats.query.filter(id=stat.id).update(
total_amount=F('total_amount') + amount
)
# 罚款权限(与 permission 表一致)
# 66693a/b/c + 66694c按身份管理罚款99933abs罚款申请通用可查看列表并操作各身份
_DEFAULT_FADAN_PERM_TO_SHENFEN = {
'66693a': 1, # 罚款管理-打手
'66693c': 2, # 罚款管理-管事
'66693b': 3, # 罚款管理-商家
'66694c': 4, # 罚款管理-组长
}
_FADAN_GENERIC_PERMS = (
'99933abs', # 罚款申请权限
)
_SHENFEN_KEYWORDS = {
'打手': 1,
'管事': 2,
'商家': 3,
'组长': 4,
}
SHENFEN_PROFILE_MAP = {
1: 'DashouProfile',
2: 'GuanshiProfile',
3: 'ShopProfile',
4: 'ZuzhangProfile',
}
@lru_cache(maxsize=1)
def _get_fadan_permission_config():
"""
加载罚款相关权限码(硬编码 + permission 表按 perm_name 补充)。
仅匹配 perm_name 含「罚款/罚单」,避免 qtczcaiwu 等描述里带「罚款」的财务权限误入。
"""
perm_map = dict(_DEFAULT_FADAN_PERM_TO_SHENFEN)
generic_codes = list(_FADAN_GENERIC_PERMS)
try:
rows = Permission.query.filter(
Q(perm_name__icontains='罚款') | Q(perm_name__icontains='罚单')
).values('perm_code', 'perm_name')
for row in rows:
code = row['perm_code']
name = row['perm_name'] or ''
matched_sf = None
for kw, sf in _SHENFEN_KEYWORDS.items():
if kw in name:
matched_sf = sf
break
if matched_sf is not None:
perm_map[code] = matched_sf
elif code not in generic_codes:
generic_codes.append(code)
except Exception as e:
logger.warning(f"从 permission 表加载罚款权限失败,使用硬编码映射: {e}")
return perm_map, tuple(generic_codes)
# 兼容旧引用
PERMISSION_TO_SHENFEN = _DEFAULT_FADAN_PERM_TO_SHENFEN
def has_fadan_view_permission(permissions, account_id=''):
"""列表/统计:拥有任一罚款权限或超级管理员权限即可查看。"""
if '000001' in permissions:
return True
perm_map, generic_codes = _get_fadan_permission_config()
all_codes = set(perm_map.keys()) | set(generic_codes)
if any(p in permissions for p in all_codes):
return True
logger.warning(
f"罚款查看权限不足 account={account_id} "
f"user_perms={permissions} need_any={sorted(all_codes)}"
)
return False
def check_fadan_permission(permissions, shenfen):
"""创建/处理:需拥有对应身份的罚款权限,或通用罚款权限,或超级管理员权限。"""
if '000001' in permissions:
return True
if any(p in permissions for p in _get_fadan_permission_config()[1]):
return True
shenfen = int(shenfen)
perm_map, _ = _get_fadan_permission_config()
for perm, sf in perm_map.items():
if sf == shenfen and perm in permissions:
return True
return False