Files
along_django/houtai/utils.py

368 lines
14 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 django.core.exceptions import ObjectDoesNotExist
from rest_framework.response import Response
from .models import AbnormalUserLog, UserRole, RolePermission, Permission, DashouRiTongji, ShangjiaRiTongji, \
GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji
from django.db import transaction
from django.db.models import F
from datetime import date
from decimal import Decimal
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.objects.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. 必须是客服 ----------
if current_user.user_type != 'kefu':
return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403)
# ---------- 2. 客服扩展表存在且状态正常 ----------
try:
kefu = current_user.kefu_profile
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. 获取用户的所有权限(通过角色) ----------
# 步骤1获取用户的所有角色ID
role_ids = UserRole.objects.filter(account_id=current_user.phone).values_list('role_id', flat=True)
# 步骤2通过角色ID获取权限编码
permissions = list(Permission.objects.filter(
rolepermission__role_id__in=role_ids
).values_list('perm_code', flat=True).distinct())
if not permissions:
logger.warning(f"用户 {current_user.phone} 没有任何权限,请检查角色分配")
return kefu, permissions
def update_dashou_daily(yonghuid, jiedan_jine=Decimal('0.00'), chengjiao_jine=Decimal('0.00'),
tuikuan_jine=Decimal('0.00')):
"""
打手每日统计(接单、成交、退款)
调用时只传入发生的金额,对应的次数自动+1。
用法示例:
# 打手接单一单100元
update_dashou_daily('200001', jiedan_jine=Decimal('100'))
# 打手完成订单100元
update_dashou_daily('200001', chengjiao_jine=Decimal('100'))
# 打手退款30元
update_dashou_daily('200001', tuikuan_jine=Decimal('30'))
"""
today = date.today()
with transaction.atomic():
stat, created = DashouRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'jiedan_zongliang': 1 if jiedan_jine else 0,
'jiedan_zonge': jiedan_jine,
'chengjiao_zongliang': 1 if chengjiao_jine else 0,
'chengjiao_zonge': chengjiao_jine,
'tuikuan_liang': 1 if tuikuan_jine else 0,
'tuikuan_jine': tuikuan_jine,
}
)
if not created:
update_fields = {}
if jiedan_jine:
update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1
update_fields['jiedan_zonge'] = F('jiedan_zonge') + jiedan_jine
if chengjiao_jine:
update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1
update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + chengjiao_jine
if tuikuan_jine:
update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1
update_fields['tuikuan_jine'] = F('tuikuan_jine') + tuikuan_jine
if update_fields:
DashouRiTongji.objects.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 = ShangjiaRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'paifa_dingdan_shu': 0,
'paifa_jine': Decimal('0.00'),
'jiesuan_dingdan_shu': 0,
'jiesuan_jine': Decimal('0.00'),
'tuikuan_dingdan_shu': 0,
'tuikuan_jine': Decimal('0.00'),
}
)
# 构建本次更新的字段,使用 F 表达式进行原子累加
update_fields = {}
if action == 1: # 派发
update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1
update_fields['paifa_jine'] = F('paifa_jine') + amount
elif action == 2: # 结算(成交)
update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1
update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount
elif action == 3: # 退款
update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1
update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount
else:
# 无效的行为类型,直接返回
return
# 执行原子更新
ShangjiaRiTongji.objects.filter(id=stat.id).update(**update_fields)
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
"""
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红
"""
from django.db import transaction
from django.db.models import F
from datetime import date
today = date.today()
with transaction.atomic():
stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'yaoqing_dashou_shu': 0,
'chongzhi_dashou_shu': 0,
'shouru_zonge': Decimal('0.00'),
}
)
if action == 1:
# 邀请打手:无论新创建还是已存在,直接 +1
if created:
stat.yaoqing_dashou_shu = 1
stat.save()
else:
GuanshiRiTongji.objects.filter(id=stat.id).update(
yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1
)
elif action == 2:
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
if created:
stat.chongzhi_dashou_shu = 1
stat.shouru_zonge = amount
stat.save()
else:
GuanshiRiTongji.objects.filter(id=stat.id).update(
chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1,
shouru_zonge=F('shouru_zonge') + amount
)
elif action in (3, 4):
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return
if created:
stat.shouru_zonge = amount
stat.save()
else:
GuanshiRiTongji.objects.filter(id=stat.id).update(
shouru_zonge=F('shouru_zonge') + 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 = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'xufei_zongshu': 1,
'xufei_shouyi': xufei_jine,
}
)
if not created:
GuanshiXufeiRiTongji.objects.filter(id=stat.id).update(
xufei_zongshu=F('xufei_zongshu') + 1,
xufei_shouyi=F('xufei_shouyi') + xufei_jine
)
def update_zuzhang_daily(yonghuid, yaoqing_count=0, shouru_jine=Decimal('0.00')):
"""
组长每日统计(邀请管事、分佣收入)
邀请管事无金额,传入 yaoqing_count=1。
分佣收入传入金额,次数不加(收入不需要次数)。
用法示例:
# 组长邀请一个管事
update_zuzhang_daily('400001', yaoqing_count=1)
# 组长获得分佣20元
update_zuzhang_daily('400001', shouru_jine=Decimal('20'))
"""
today = date.today()
with transaction.atomic():
stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create(
yonghuid=yonghuid,
riqi=today,
defaults={
'yonghuid': yonghuid,
'riqi': today,
'yaoqing_guanshi_shu': yaoqing_count,
'shouru_zonge': shouru_jine,
}
)
if not created:
update_fields = {}
if yaoqing_count:
update_fields['yaoqing_guanshi_shu'] = F('yaoqing_guanshi_shu') + yaoqing_count
if shouru_jine:
update_fields['shouru_zonge'] = F('shouru_zonge') + shouru_jine
if update_fields:
ZuzhangRiTongji.objects.filter(id=stat.id).update(**update_fields)
def update_tixian_daily_stat(leixing, amount):
"""
更新每日提现统计(平台维度)
leixing: 1=打手, 2=管事, 3=组长
amount: 本次提现金额(申请金额,含手续费)
"""
today = date.today()
with transaction.atomic():
stat, created = TixianRiTongji.objects.select_for_update().get_or_create(
riqi=today,
leixing=leixing,
defaults={'total_amount': amount}
)
if not created:
TixianRiTongji.objects.filter(id=stat.id).update(
total_amount=F('total_amount') + amount
)
def decrease_tixian_daily_stat(leixing, amount):
"""
减少每日提现统计(平台维度),用于提现失败或取消时回滚
:param leixing: 1=打手,2=管事,3=组长
:param amount: 要减少的金额(实际到账金额)
"""
today = date.today()
with transaction.atomic():
# 使用 select_for_update 防止并发回滚时数据错乱
stat = TixianRiTongji.objects.select_for_update().get(riqi=today, leixing=leixing)
stat.total_amount -= amount
stat.save(update_fields=['total_amount'])