""" 提现抽成利润统计(真实抽成 = 申请扣款额 − 实际到账额 = shouxufei) 口径说明(务必与分账一致): - 仅统计「提现成功」记录:Tixianjilu.zhuangtai = 2 - 利润 = COALESCE(shouxufei, max(shenqing_jine - jine, 0)) - 手动/自动一并计入(dakuan_mode 可筛) - 日期维度默认按 update_time(成功落账日);可按 create_time(申请日) - 不含会员/押金充值/商家充值等,那些不是抽成利润 """ import logging from datetime import datetime, time from decimal import Decimal from django.conf import settings from django.db.models import ( Count, DecimalField, F, Q, Sum, Value, ) from django.db.models.functions import Coalesce, Greatest, TruncDate, TruncMonth from django.utils import timezone from rest_framework.parsers import JSONParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from yonghu.models import Tixianjilu from .view import verify_kefu_permission logger = logging.getLogger(__name__) _LEIXING_LABEL = { 1: '打手佣金', 2: '管事分红', 3: '组长分红', 4: '审核官分佣', 5: '打手押金', 6: '商家余额', } _FEE_EXPR = Coalesce( F('shouxufei'), Greatest( Coalesce(F('shenqing_jine'), Value(Decimal('0.00'))) - Coalesce(F('jine'), Value(Decimal('0.00'))), Value(Decimal('0.00')), ), Value(Decimal('0.00')), output_field=DecimalField(max_digits=12, decimal_places=2), ) def _parse_date(s): if not s: return None try: return datetime.strptime(str(s).strip()[:10], '%Y-%m-%d').date() except (TypeError, ValueError): return None def _day_bounds(d): """日历日 → datetime 区间 [start, end];兼容 USE_TZ=False(MySQL 禁止 aware datetime)。""" start = datetime.combine(d, time.min) end = datetime.combine(d, time(23, 59, 59, 999999)) if getattr(settings, 'USE_TZ', False): tz = timezone.get_current_timezone() start = timezone.make_aware(start, tz) end = timezone.make_aware(end, tz) return start, end class TixianChouchengProfitView(APIView): """ POST /houtai/txchouchengtj 提现抽成利润统计(分账专用口径) 参数: phone: 客服手机号 date_from / date_to: YYYY-MM-DD(闭区间,按成功日或申请日) date_field: update_time(默认,成功日) | create_time(申请日) leixing: 1~6 可选;不传=全部身份 dakuan_mode: 1手动 2自动 可选;不传=全部 group_by: day | month | leixing | none(默认 day) page / page_size: 明细分页(可选,仅 group_by=none 时返回 list) """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): phone = (request.data.get('phone') or request.data.get('username') or '').strip() kefu, permissions = verify_kefu_permission(request, phone) if kefu is None: return permissions if 'caiwu' not in permissions and not any( p in permissions for p in ('5500a', '5500b', '5500c') ): return Response({'code': 403, 'msg': '无提现抽成利润查看权限'}) date_from = _parse_date(request.data.get('date_from') or request.data.get('start_date')) date_to = _parse_date(request.data.get('date_to') or request.data.get('end_date')) if not date_from or not date_to: today = timezone.localdate() date_from = date_from or today date_to = date_to or today if date_from > date_to: return Response({'code': 400, 'msg': 'date_from 不能晚于 date_to'}) date_field = (request.data.get('date_field') or 'update_time').strip() if date_field not in ('update_time', 'create_time'): return Response({'code': 400, 'msg': 'date_field 仅支持 update_time / create_time'}) group_by = (request.data.get('group_by') or 'day').strip() if group_by not in ('day', 'month', 'leixing', 'none'): return Response({'code': 400, 'msg': 'group_by 仅支持 day/month/leixing/none'}) leixing_val = None raw_leixing = request.data.get('leixing') if raw_leixing is not None and str(raw_leixing).strip() != '': try: leixing_val = int(raw_leixing) except (TypeError, ValueError): return Response({'code': 400, 'msg': 'leixing 无效'}) if leixing_val not in (1, 2, 3, 4, 5, 6): return Response({'code': 400, 'msg': 'leixing 仅支持 1~6'}) dakuan_mode_val = None raw_mode = request.data.get('dakuan_mode') if raw_mode is not None and str(raw_mode).strip() != '': try: dakuan_mode_val = int(raw_mode) except (TypeError, ValueError): return Response({'code': 400, 'msg': 'dakuan_mode 无效'}) if dakuan_mode_val not in (1, 2): return Response({'code': 400, 'msg': 'dakuan_mode 仅支持 1/2'}) start_dt, _ = _day_bounds(date_from) _, end_dt = _day_bounds(date_to) # 仅提现成功;手动+自动 q = Q(zhuangtai=2) if date_field == 'create_time': q &= Q(create_time__gte=start_dt, create_time__lte=end_dt) else: q &= Q(update_time__gte=start_dt, update_time__lte=end_dt) if leixing_val is not None: q &= Q(leixing=leixing_val) if dakuan_mode_val is not None: q &= Q(dakuan_mode=dakuan_mode_val) qs = Tixianjilu.objects.filter(q) annotated = qs.annotate(fee=_FEE_EXPR) total_agg = annotated.aggregate( fee_sum=Sum('fee'), apply_sum=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), payout_sum=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), cnt=Count('id'), ) fee_total = total_agg.get('fee_sum') or Decimal('0.00') apply_total = total_agg.get('apply_sum') or Decimal('0.00') payout_total = total_agg.get('payout_sum') or Decimal('0.00') # 交叉校验:申请 − 到账 应约等于抽成(允许历史脏数据偏差) cross_check = (apply_total - payout_total).quantize(Decimal('0.01')) summary = { 'profit_total': str(fee_total.quantize(Decimal('0.01'))), 'apply_total': str(apply_total.quantize(Decimal('0.01'))), 'payout_total': str(payout_total.quantize(Decimal('0.01'))), 'apply_minus_payout': str(cross_check), 'success_count': int(total_agg.get('cnt') or 0), 'date_from': date_from.isoformat(), 'date_to': date_to.isoformat(), 'date_field': date_field, 'leixing': leixing_val, 'leixing_label': _LEIXING_LABEL.get(leixing_val, '全部身份'), 'dakuan_mode': dakuan_mode_val, 'formula': '利润=申请扣款额(shenqing_jine)−实际到账(jine)=手续费(shouxufei);仅统计提现成功(zhuangtai=2)', } rows = [] if group_by == 'day': trunc = TruncDate(date_field) grouped = ( annotated.annotate(period=trunc) .values('period') .annotate( profit=Sum('fee'), apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), count=Count('id'), ) .order_by('period') ) for g in grouped: period = g['period'] rows.append({ 'period': period.isoformat() if period else '', 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'count': g['count'] or 0, }) elif group_by == 'month': trunc = TruncMonth(date_field) grouped = ( annotated.annotate(period=trunc) .values('period') .annotate( profit=Sum('fee'), apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), count=Count('id'), ) .order_by('period') ) for g in grouped: period = g['period'] rows.append({ 'period': period.strftime('%Y-%m') if period else '', 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'count': g['count'] or 0, }) elif group_by == 'leixing': grouped = ( annotated.values('leixing') .annotate( profit=Sum('fee'), apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))), payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))), count=Count('id'), ) .order_by('leixing') ) for g in grouped: lx = g['leixing'] rows.append({ 'leixing': lx, 'leixing_label': _LEIXING_LABEL.get(lx, f'类型{lx}'), 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), 'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))), 'count': g['count'] or 0, }) else: # 明细 try: page = max(1, int(request.data.get('page') or 1)) page_size = min(200, max(1, int(request.data.get('page_size') or 50))) except (TypeError, ValueError): page, page_size = 1, 50 total = annotated.count() items = list( annotated.order_by(f'-{date_field}', '-id') [(page - 1) * page_size: page * page_size] .values( 'id', 'yonghuid', 'leixing', 'dakuan_mode', 'shenqing_jine', 'jine', 'shouxufei', 'fee', 'create_time', 'update_time', 'shenhe_danhao', ) ) for it in items: fee = it.get('fee') or Decimal('0') rows.append({ 'id': it['id'], 'yonghuid': it['yonghuid'], 'leixing': it['leixing'], 'leixing_label': _LEIXING_LABEL.get(it['leixing'], ''), 'dakuan_mode': it['dakuan_mode'], 'dakuan_mode_label': '自动' if it['dakuan_mode'] == 2 else '手动', 'shenqing_jine': str(it['shenqing_jine'] or '0.00'), 'shijidaozhang': str(it['jine'] or '0.00'), 'shouxufei': str(it['shouxufei'] if it['shouxufei'] is not None else fee), 'profit': str(Decimal(fee).quantize(Decimal('0.01'))), 'shenhe_danhao': it.get('shenhe_danhao') or '', 'create_time': it['create_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('create_time') else '', 'update_time': it['update_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('update_time') else '', }) return Response({ 'code': 0, 'data': { 'summary': summary, 'group_by': group_by, 'list': rows, 'total': total, 'page': page, 'page_size': page_size, }, }) # 按身份拆分汇总(便于分账核对,始终附带) by_leixing = [] for g in ( annotated.values('leixing') .annotate(profit=Sum('fee'), count=Count('id')) .order_by('leixing') ): by_leixing.append({ 'leixing': g['leixing'], 'leixing_label': _LEIXING_LABEL.get(g['leixing'], ''), 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), 'count': g['count'] or 0, }) by_mode = [] for g in ( annotated.values('dakuan_mode') .annotate(profit=Sum('fee'), count=Count('id')) .order_by('dakuan_mode') ): by_mode.append({ 'dakuan_mode': g['dakuan_mode'], 'dakuan_mode_label': '自动' if g['dakuan_mode'] == 2 else '手动', 'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))), 'count': g['count'] or 0, }) return Response({ 'code': 0, 'data': { 'summary': summary, 'group_by': group_by, 'rows': rows, 'by_leixing': by_leixing, 'by_dakuan_mode': by_mode, }, })