Files
Django/backend/utils.py

658 lines
24 KiB
Python
Raw Permalink 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 _club_id_for_yonghuid(yonghuid, club_id=None):
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_user import get_user_club_id
from users.business_models import User
if club_id:
return club_id
u = User.query.filter(UserUID=yonghuid).first()
return get_user_club_id(u) if u else CLUB_ID_DEFAULT
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}")
class _AllPermissions(list):
"""拥有 000001 超级管理权限时使用,使任何权限码 in 检查都返回 True"""
def __contains__(self, item):
return True
def has_perm_code(permissions, *codes):
"""权限码 in 检查;系统超管 _AllPermissions 视为全部通过。"""
if isinstance(permissions, _AllPermissions):
return True
perms = permissions or []
return any(c in perms for c in codes)
def has_merchant_order_permission(permissions):
"""商家/链接订单处理权限002ac 商家订单、002ab 平台订单、003aa 跨平台。"""
return has_perm_code(permissions, '002ac', '002ab', '003aa')
def _legacy_backend_role_permissions(phone):
"""
读取旧权限表backend.models
user_role.account_id = 手机号 → role → role_permission → permission.perm_code
与 gvsdsdk 表并行,未改 SDK 结构。
"""
phone = (phone or '').strip()
if not phone:
return [], []
try:
from backend.models import (
UserRole as LegacyUserRole,
Role as BackendRole,
Permission as BackendPermission,
)
role_ids = list(
LegacyUserRole.objects.filter(account_id=phone).values_list('role_id', flat=True)
)
if not role_ids:
return [], []
perm_codes = list(
BackendPermission.objects.filter(
rolepermission__role_id__in=role_ids
).values_list('perm_code', flat=True).distinct()
)
role_names = list(
BackendRole.objects.filter(role_id__in=role_ids).values_list('role_name', flat=True)
)
return perm_codes, role_names
except Exception as e:
logger.warning('legacy_backend_role_permissions phone=%s err=%s', phone, e)
return [], []
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. 必须是客服或管理员 ----------
from jituan.constants import SUPER_ADMIN_PHONES
from jituan.services.admin_context import is_kefu_backend_account
is_super_phone = getattr(current_user, 'Phone', '') in SUPER_ADMIN_PHONES
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser or is_super_phone
if not is_kefu_backend_account(current_user):
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)
else:
# 管理员无需客服扩展表,用占位对象以通过 kefu_obj is None 检查
from types import SimpleNamespace
kefu = SimpleNamespace(user=current_user)
# ---------- 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. 获取用户的所有权限(与菜单 menu-access 同一套算法) ----------
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_SINGLE
from jituan.services.admin_context import resolve_admin_effective_club, get_allowed_club_ids, is_system_super_admin
from jituan.services.club_rbac import resolve_effective_permission_codes
eff_club = getattr(request, 'club_id', None) or CLUB_ID_DEFAULT
eff_scope = getattr(request, 'club_scope', None) or DATA_SCOPE_SINGLE
allowed_club_ids = None
if not is_system_super_admin(current_user):
eff_club, eff_scope, club_err = resolve_admin_effective_club(current_user, request)
if club_err:
return None, Response({'code': 403, 'msg': club_err}, status=403)
request.club_id = eff_club
request.club_scope = eff_scope
allowed_club_ids = list(get_allowed_club_ids(current_user))
try:
if is_system_super_admin(current_user):
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 = []
legacy_codes, _ = _legacy_backend_role_permissions(current_user.Phone)
if legacy_codes:
permissions = list(set(list(permissions) + legacy_codes))
else:
permissions = resolve_effective_permission_codes(
current_user, eff_club, eff_scope, allowed_club_ids,
)
except Exception as e:
logger.warning(
'verify_kefu_permission failed: UserUUID=%s Phone=%s err=%s',
getattr(current_user, 'UserUUID', None),
getattr(current_user, 'Phone', None),
e,
exc_info=True,
)
permissions = []
# 仅系统级超管自动拥有全部功能权限;俱乐部 000001 只解锁超管页,不展开全部菜单
if is_system_super_admin(current_user):
if '000001' not in permissions:
permissions.insert(0, '000001')
permissions = _AllPermissions(permissions)
if not permissions:
logger.warning(f"用户 {current_user.Phone} 没有任何权限,请检查角色分配")
return kefu, permissions
def update_dashou_daily_by_action(yonghuid, amount, action, club_id=None):
"""
根据行为类型更新打手每日统计(接单、成交、退款)
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = PlayerDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
PlayerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'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, order_id=None, club_id=None):
"""
根据操作行为更新商家每日统计(派发、结算、退款)
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = MerchantDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
MerchantID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'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)
if order_id:
try:
from merchant_ops.services.stats import sync_staff_stats_by_order_id
sync_staff_stats_by_order_id(order_id, amount, action)
except Exception:
logger.warning('同步客服日统计失败 order_id=%s', order_id, exc_info=True)
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00'), club_id=None):
"""
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = ManagerDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
ManagerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'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'), club_id=None):
"""管事续费统计(单独表)"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
ManagerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'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'), club_id=None):
"""根据操作行为更新组长每日统计"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = LeaderDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
LeaderID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'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, club_id=None):
"""
更新每日提现统计(平台维度,按俱乐部)
leixing: 1=打手, 2=管事, 3=组长
amount: 本次提现金额(申请金额,含手续费)
"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.withdraw_config import get_or_create_platform_daily_stat
club_id = (club_id or '').strip() or CLUB_ID_DEFAULT
today = date.today()
with transaction.atomic():
stat = get_or_create_platform_daily_stat(club_id, leixing, today)
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
def fmt_datetime(dt):
"""格式化时间为前端通用字符串。"""
if not dt:
return ''
return dt.strftime('%Y-%m-%d %H:%M:%S')
def datetime_aliases(dt):
"""同时返回 CreateTime / create_time避免前后端字段名不一致。"""
s = fmt_datetime(dt)
return {'CreateTime': s, 'create_time': s}
def pick_order_id(data):
"""兼容前端 dingdan_id 与 SDK 字段 OrderID。"""
return str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
def pick_leixing_id(data):
"""兼容前端 leixing_id 与 SDK 字段 ProductTypeID。"""
val = data.get('leixing_id')
if val is None or val == '':
val = data.get('ProductTypeID')
if val is None or val == '':
return None
try:
return int(val)
except (ValueError, TypeError):
return None
def pick_penalty_create_params(data):
"""兼容罚单创建SDK 字段名与旧版拼音字段名。"""
penalized_id = str(data.get('PenalizedUserID') or data.get('beichufa_id') or '').strip()
reason = str(data.get('Reason') or data.get('chufaliyou') or '').strip()
fine_amount = data.get('FineAmount')
if fine_amount is None:
fine_amount = data.get('fakuanjine')
affects = data.get('AffectsGrabbing')
if affects is None:
affects = data.get('yingxiang_qiangdan', 1)
return penalized_id, reason, fine_amount, affects
# ---------- 修改记录Xiugaijilu ----------
XIUGAI_LEIXING_DASHOU = 2
XIUGAI_LEIXING_GUANSHI = 3
XIUGAI_LEIXING_SHANGJIA = 4
XIUGAI_LEIXING_ZUZHANG = 5
XIUGAI_LEIXING_LABEL = {
XIUGAI_LEIXING_DASHOU: '打手',
XIUGAI_LEIXING_GUANSHI: '管事',
XIUGAI_LEIXING_SHANGJIA: '商家',
XIUGAI_LEIXING_ZUZHANG: '组长',
}
def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, club_id=None, request_ip='', **extra_fields):
"""
统一写入 xiugaijilu 修改记录。
qitashuoming 应详细描述本次操作(尤其是金额、身份、上下级变更等)。
extra_fields 可传入 xiugaitijiao、shangjiayue、guanshiyue 等模型字段。
"""
from users.models import Xiugaijilu
from jituan.services.admin_audit import log_admin_audit_from_xiugai
if not qitashuoming:
label = XIUGAI_LEIXING_LABEL.get(leixing, f'类型{leixing}')
qitashuoming = f'【后台】修改{label}信息,操作人{xiugaiid}'
Xiugaijilu.query.create(
yonghuid=str(yonghuid),
xiugaiid=str(xiugaiid),
leixing=leixing,
qitashuoming=qitashuoming,
**extra_fields,
)
log_admin_audit_from_xiugai(
yonghuid=yonghuid,
xiugaiid=xiugaiid,
leixing=leixing,
qitashuoming=qitashuoming,
club_id=club_id,
request_ip=request_ip,
)