# 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_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 = DashouRiTongji.objects.select_for_update().get_or_create( yonghuid=yonghuid, riqi=today, defaults={ 'yonghuid': yonghuid, 'riqi': today, 'jiedan_zongliang': 0, 'jiedan_zonge': Decimal('0.00'), 'chengjiao_zongliang': 0, 'chengjiao_zonge': Decimal('0.00'), 'tuikuan_liang': 0, 'tuikuan_jine': Decimal('0.00'), } ) update_fields = {} if action == 1: # 接单 update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1 update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount elif action == 2: # 成交 update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount elif action == 3: # 退款 update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1 update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount else: return # 无效行为 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 = 押金分红 """ 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_daily_by_action(yonghuid, action, amount=Decimal('0.00')): """ 根据操作行为更新管事每日统计(邀请打手、充值会员) 参数: yonghuid: 管事用户ID action: 操作类型 1 = 邀请打手(次数自动+1,无需传其他参数) 2 = 充值会员(需要传 amount 参数) amount: 充值金额(Decimal),仅在 action=2 时有效 说明: - 每次调用只代表一次操作,不会批量处理。 - 使用 select_for_update + F() 保证原子性和并发安全。 """ today = date.today() # 邀请打手:次数+1,金额不变 if action == 1: count_field = 'yaoqing_dashou_shu' amount = Decimal('0.00') # 充值会员:次数+1,金额累加 elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') if amount <= 0: return count_field = 'chongzhi_dashou_shu' else: return # 无效行为,直接返回 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 created: # 新记录直接赋初值 if action == 1: stat.yaoqing_dashou_shu = 1 else: # 已存在记录,使用 F 表达式原子累加次数 update_fields = {count_field: F(count_field) + 1} if action == 2: update_fields['shouru_zonge'] = F('shouru_zonge') + amount GuanshiRiTongji.objects.filter(id=stat.id).update(**update_fields)''' 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_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 = ZuzhangRiTongji.objects.select_for_update().get_or_create( yonghuid=yonghuid, riqi=today, defaults={ 'yonghuid': yonghuid, 'riqi': today, 'yaoqing_guanshi_shu': 0, 'shouru_zonge': Decimal('0.00'), 'fenyong_jine': Decimal('0.00'), } ) if created: # 新记录直接设置初始值 if action == 1: stat.yaoqing_guanshi_shu = 1 elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') stat.shouru_zonge = amount stat.fenyong_jine = amount stat.save() else: # 已存在记录,使用 F 表达式原子累加 if action == 1: ZuzhangRiTongji.objects.filter(id=stat.id).update( yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 ) elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') if amount > 0: ZuzhangRiTongji.objects.filter(id=stat.id).update( shouru_zonge=F('shouru_zonge') + amount, fenyong_jine=F('fenyong_jine') + amount ) 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 )