"""财务子模块统计(订单/会员/其他充值),供 /jituan/houtai/*;旧 /houtai/* 不变。""" from datetime import date, datetime from decimal import Decimal from django.db.models import Case, Count, DecimalField, Q, Sum, Value, When from django.db.models.functions import TruncDate, TruncMonth, TruncYear from jituan.constants import DATA_SCOPE_ALL from jituan.models import ClubHuiyuanPrice from jituan.services.club_context import ( filter_queryset_by_club, resolve_club_id_from_request, resolve_club_scope, ) from jituan.services.club_penalty import filter_gsfenhong_qs, filter_penalty_qs from orders.models import Order, Penalty from products.czjilu_types import CZJILU_LEIXING_SHANGJIA, exclude_penalty_from_cz_qs from products.models import Czjilu, Gsfenhong, Huiyuan, ShangpinLeixing from rank.models import Bankuai def _meta(request): return { 'club_id': resolve_club_id_from_request(request), 'scope': resolve_club_scope(request), } def _parse_time_range(granularity, year, month, summary_date): now = date.today() if granularity == 'day': if not year or not month: return None, '按日统计需要年份和月份' start_date = date(int(year), int(month), 1) end_date = date(int(year), int(month) + 1, 1) if int(month) < 12 else date(int(year) + 1, 1, 1) trunc_func = TruncDate('CreateTime') if not summary_date: summary_date = now.strftime('%Y-%m-%d') summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date() elif granularity == 'month': if not year: return None, '按月统计需要年份' y = int(year) start_date = date(y, 1, 1) end_date = date(y + 1, 1, 1) trunc_func = TruncMonth('CreateTime') if not summary_date: summary_date = now.strftime('%Y-%m') summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date() elif granularity == 'year': y = int(year or now.year) start_date = date(y, 1, 1) end_date = date(y + 1, 1, 1) trunc_func = TruncYear('CreateTime') if not summary_date: summary_date = str(y) summary_q = date(y, 1, 1) else: return None, '无效的颗粒度' start_dt = datetime.combine(start_date, datetime.min.time()) end_dt = datetime.combine(end_date, datetime.min.time()) return { 'start_dt': start_dt, 'end_dt': end_dt, 'trunc_func': trunc_func, 'summary_q': summary_q, 'granularity': granularity, 'year': int(year) if year is not None else None, 'month': int(month) if month is not None else None, }, None def _summary_date_filter(granularity, summary_q, field_prefix='CreateTime'): if granularity == 'day': return {f'{field_prefix}__date': summary_q} if granularity == 'month': return {f'{field_prefix}__year': summary_q.year, f'{field_prefix}__month': summary_q.month} return {f'{field_prefix}__year': summary_q.year} def _penalty_trunc_func(granularity): if granularity == 'day': return TruncDate('UpdateTime') if granularity == 'month': return TruncMonth('UpdateTime') return TruncYear('UpdateTime') def _penalty_fenhong_aggregates(): """罚款分红只统计已入账(BonusCredited),与 process_fadan_fenhong 一致。""" zero = Value(Decimal('0.00'), output_field=DecimalField(max_digits=12, decimal_places=2)) return { 'fenhong_amount': Sum( Case( When(BonusCredited=True, then='ApplicantBonusAmount'), default=zero, output_field=DecimalField(max_digits=12, decimal_places=2), ) ), 'fenhong_count': Count( 'id', filter=Q(BonusCredited=True, ApplicantBonusAmount__gt=0), ), } def _czjilu_finance_qs(request, start_dt, end_dt, cz_types): """充值财务 czjilu:已支付;type4 排除罚款误标。""" qs = filter_queryset_by_club( Czjilu.query.filter( CreateTime__gte=start_dt, CreateTime__lt=end_dt, leixing__in=cz_types, zhuangtai=3, ), request, ) if CZJILU_LEIXING_SHANGJIA in cz_types: qs = exclude_penalty_from_cz_qs(qs) return qs def _czjilu_summary_qs(request, lx, granularity, summary_q): filter_kwargs = {'leixing': lx, 'zhuangtai': 3} filter_kwargs.update(_summary_date_filter(granularity, summary_q)) qs = filter_queryset_by_club(Czjilu.query.filter(**filter_kwargs), request) if lx == CZJILU_LEIXING_SHANGJIA: qs = exclude_penalty_from_cz_qs(qs) return qs def build_order_types_payload(): types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao') return list(types) def build_order_finance_payload( request, granularity, year, month, type_id=0, order_source=0, summary_date=None, ): parsed, err = _parse_time_range(granularity, year, month, summary_date) if err: return None, err start_dt = parsed['start_dt'] end_dt = parsed['end_dt'] trunc_func = parsed['trunc_func'] summary_q = parsed['summary_q'] granularity = parsed['granularity'] filters = { 'CreateTime__gte': start_dt, 'CreateTime__lt': end_dt, 'Status__in': [1, 2, 3, 4, 5, 6, 7, 8], } if int(type_id or 0) != 0: filters['ProductTypeID'] = type_id if int(order_source or 0) in [1, 2]: filters['Platform'] = int(order_source) order_base = filter_queryset_by_club( Order.query.filter(**filters), request, club_field='ClubID', ) qs = order_base.annotate(period=trunc_func).values('period').annotate( order_count=Count('id'), order_amount=Sum('Amount'), success_count=Count('id', filter=Q(Status=3)), success_amount=Sum('Amount', filter=Q(Status=3)), refund_count=Count('id', filter=Q(Status=5)), PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)), dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)), platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)), merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)), platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)), merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)), ).order_by('period') time_series = [] for item in qs: period_str = ( item['period'].strftime('%Y-%m-%d') if granularity == 'day' else item['period'].strftime('%Y-%m') if granularity == 'month' else item['period'].strftime('%Y') ) platform_profit = (item['platform_success_amount'] or 0) - (item['platform_dashou'] or 0) - (item['dianpu_fenhong_sum'] or 0) merchant_profit = (item['merchant_success_amount'] or 0) - (item['merchant_dashou'] or 0) time_series.append({ 'date': period_str, 'order_count': item['order_count'], 'order_amount': float(item['order_amount'] or 0), 'success_count': item['success_count'], 'success_amount': float(item['success_amount'] or 0), 'refund_count': item['refund_count'], 'dashou_fencheng': float(item['PlayerCommission_sum'] or 0), 'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0), 'profit': round(float(platform_profit + merchant_profit), 2), }) summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID') # 合并 13 次独立 count/aggregate → 1 次 aggregate _summary_agg = summary_qs.aggregate( total_order_count=Count('id'), total_order_amount=Sum('Amount'), total_success_count=Count('id', filter=Q(Status=3)), total_success_amount=Sum('Amount', filter=Q(Status=3)), total_refund_count=Count('id', filter=Q(Status=5)), total_dashou=Sum('PlayerCommission', filter=Q(Status=3)), total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)), platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)), merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)), platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)), merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)), ) total_order_count = _summary_agg['total_order_count'] or 0 total_order_amount = _summary_agg['total_order_amount'] or 0 total_success_count = _summary_agg['total_success_count'] or 0 total_success_amount = _summary_agg['total_success_amount'] or 0 total_refund_count = _summary_agg['total_refund_count'] or 0 total_dashou = _summary_agg['total_dashou'] or 0 total_dianpu_fenhong = _summary_agg['total_dianpu_fenhong'] or 0 platform_amount = _summary_agg['platform_amount'] or 0 merchant_amount = _summary_agg['merchant_amount'] or 0 platform_dashou_sum = _summary_agg['platform_dashou_sum'] or 0 merchant_dashou_sum = _summary_agg['merchant_dashou_sum'] or 0 profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong profit_merchant = merchant_amount - merchant_dashou_sum total_profit = profit_platform + profit_merchant data = { **_meta(request), 'time_series': time_series, 'summary': { 'total_order_count': total_order_count, 'total_order_amount': round(float(total_order_amount), 2), 'total_success_count': total_success_count, 'total_success_amount': round(float(total_success_amount), 2), 'total_refund_count': total_refund_count, 'total_PlayerCommission': round(float(total_dashou), 2), 'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2), 'total_profit': round(float(total_profit), 2), }, } return data, None def build_chongzhi_finance_payload(request, granularity, year, month, types=None, summary_date=None): if types is None: types = [2, 3, 4, 5, 6] if isinstance(types, list) and len(types) == 0: types = [2, 3, 4, 5, 6] parsed, err = _parse_time_range(granularity, year, month, summary_date) if err: return None, err start_dt = parsed['start_dt'] end_dt = parsed['end_dt'] trunc_func = parsed['trunc_func'] summary_q = parsed['summary_q'] granularity = parsed['granularity'] time_series_map = {} def ensure_date(ds): if ds not in time_series_map: time_series_map[ds] = {} return time_series_map[ds] cz_types = [t for t in types if t in [2, 3, 4, 5]] if cz_types: cz_qs = _czjilu_finance_qs(request, start_dt, end_dt, cz_types).annotate( period=trunc_func, ).values('period', 'leixing').annotate( total_count=Count('id'), total_amount=Sum('jine'), ).order_by('period', 'leixing') for item in cz_qs: period_str = ( item['period'].strftime('%Y-%m-%d') if granularity == 'day' else item['period'].strftime('%Y-%m') if granularity == 'month' else item['period'].strftime('%Y') ) lx = item['leixing'] amount = float(item['total_amount'] or 0) count = item['total_count'] profit = amount if lx == 3 else 0.0 day_data = ensure_date(period_str) day_data[str(lx)] = { 'count': count, 'amount': amount, 'profit': profit, 'fenhong_count': 0, 'fenhong_amount': 0.0, } if 6 in types: penalty_trunc = _penalty_trunc_func(granularity) penalty_qs = filter_penalty_qs( Penalty.query.filter( UpdateTime__gte=start_dt, UpdateTime__lt=end_dt, Status=2, ), request, ).annotate(period=penalty_trunc).values('period').annotate( penalty_count=Count('id'), penalty_amount=Sum('FineAmount'), **_penalty_fenhong_aggregates(), ).order_by('period') for item in penalty_qs: period_str = ( item['period'].strftime('%Y-%m-%d') if granularity == 'day' else item['period'].strftime('%Y-%m') if granularity == 'month' else item['period'].strftime('%Y') ) day_data = ensure_date(period_str) penalty_amt = float(item['penalty_amount'] or 0) fenhong_amt = float(item['fenhong_amount'] or 0) day_data['6'] = { 'count': item['penalty_count'], 'amount': penalty_amt, 'profit': round(penalty_amt - fenhong_amt, 2), 'fenhong_count': item['fenhong_count'], 'fenhong_amount': fenhong_amt, } time_series = [] for period_str in sorted(time_series_map.keys()): entry = {'date': period_str, 'types': {}} for type_code in sorted(time_series_map[period_str].keys()): entry['types'][type_code] = time_series_map[period_str][type_code] time_series.append(entry) summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0} for lx in cz_types: agg = _czjilu_summary_qs(request, lx, granularity, summary_q).aggregate( count=Count('id'), amount=Sum('jine'), ) count = agg['count'] or 0 amount = float(agg['amount'] or 0) profit = amount if lx == 3 else 0.0 summary_result['by_type'][str(lx)] = { 'count': count, 'amount': round(amount, 2), 'profit': round(profit, 2), 'fenhong_count': 0, 'fenhong_amount': 0.0, } summary_result['total_count'] += count summary_result['total_amount'] += amount summary_result['total_profit'] += profit if 6 in types: penalty_filter = {'Status': 2} penalty_filter.update(_summary_date_filter(granularity, summary_q, field_prefix='UpdateTime')) agg = filter_penalty_qs(Penalty.query.filter(**penalty_filter), request).aggregate( count=Count('id'), amount=Sum('FineAmount'), **_penalty_fenhong_aggregates(), ) penalty_count = agg['count'] or 0 penalty_amount = float(agg['amount'] or 0) fh_amount = float(agg['fenhong_amount'] or 0) fh_count = agg['fenhong_count'] or 0 profit = round(penalty_amount - fh_amount, 2) summary_result['by_type']['6'] = { 'count': penalty_count, 'amount': round(penalty_amount, 2), 'profit': profit, 'fenhong_count': fh_count, 'fenhong_amount': round(fh_amount, 2), } summary_result['total_count'] += penalty_count summary_result['total_amount'] += penalty_amount summary_result['total_profit'] += profit summary_result['total_amount'] = round(summary_result['total_amount'], 2) summary_result['total_profit'] = round(summary_result['total_profit'], 2) return { **_meta(request), 'time_series': time_series, 'summary': summary_result, }, None def build_huiyuan_bankuai_payload(request): club_id = resolve_club_id_from_request(request) price_map = {} if resolve_club_scope(request) != DATA_SCOPE_ALL: for row in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True): price_map[row.huiyuan_id] = { 'jiage': float(row.jiage or 0), 'guanshifc': float(row.guanshifc or 0), 'zuzhangfc': float(row.zuzhangfc or 0), } # 批量预取所有 bankuai 和 huiyuan,避免循环内 N+1 _bankuai_qs = Bankuai.query.all().only('bankuai_id', 'mingcheng') _members_by_bankuai = {} for m in Huiyuan.query.values('huiyuan_id', 'jieshao', 'bankuai_id'): _members_by_bankuai.setdefault(m['bankuai_id'], []).append(m) bankuai_list = [] for bk in _bankuai_qs: members = [] for m in _members_by_bankuai.get(bk.bankuai_id, []): item = dict(m) if m['huiyuan_id'] in price_map: item['club_price'] = price_map[m['huiyuan_id']] members.append(item) bankuai_list.append({ 'bankuai_id': bk.bankuai_id, 'mingcheng': bk.mingcheng, 'members': members, }) return {**_meta(request), 'bankuai_list': bankuai_list} def build_huiyuan_stats_payload( request, huiyuan_id, granularity, year, month, include_guanshi=False, include_zuzhang=False, summary_date=None, ): if not huiyuan_id: return None, '缺少会员ID' parsed, err = _parse_time_range(granularity, year, month, summary_date) if err: return None, err start_dt = parsed['start_dt'] end_dt = parsed['end_dt'] trunc_func = parsed['trunc_func'] summary_q = parsed['summary_q'] granularity = parsed['granularity'] chongzhi_filter = { 'huiyuan_id': huiyuan_id, 'leixing': 1, 'zhuangtai': 3, 'CreateTime__gte': start_dt, 'CreateTime__lt': end_dt, } fenhong_filter = { 'huiyuan_id': huiyuan_id, 'CreateTime__gte': start_dt, 'CreateTime__lt': end_dt, } chongzhi_qs = filter_queryset_by_club( Czjilu.query.filter(**chongzhi_filter), request, ).annotate(period=trunc_func).values('period').annotate( chongzhi_count=Count('id'), chongzhi_amount=Sum('jine'), ).order_by('period') fenhong_qs = None if include_guanshi or include_zuzhang: fenhong_agg = {} if include_guanshi: fenhong_agg['guanshi_amount'] = Sum('fenhong') if include_zuzhang: fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') fenhong_qs = filter_gsfenhong_qs( Gsfenhong.query.filter(**fenhong_filter), request, ).annotate(period=trunc_func).values('period').annotate(**fenhong_agg).order_by('period') data_map = {} for item in chongzhi_qs: period_str = ( item['period'].strftime('%Y-%m-%d') if granularity == 'day' else item['period'].strftime('%Y-%m') if granularity == 'month' else item['period'].strftime('%Y') ) data_map[period_str] = { 'chongzhi_count': item['chongzhi_count'], 'chongzhi_amount': float(item['chongzhi_amount'] or 0), } if fenhong_qs: for item in fenhong_qs: period_str = ( item['period'].strftime('%Y-%m-%d') if granularity == 'day' else item['period'].strftime('%Y-%m') if granularity == 'month' else item['period'].strftime('%Y') ) if period_str not in data_map: data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0} if include_guanshi: data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0) if include_zuzhang: data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0) time_series = [] for period_str, vals in data_map.items(): guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0 zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0 vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2) vals['date'] = period_str time_series.append(vals) time_series.sort(key=lambda x: x['date']) summary_chongzhi_filter = dict(chongzhi_filter) summary_chongzhi_filter.pop('CreateTime__gte', None) summary_chongzhi_filter.pop('CreateTime__lt', None) summary_chongzhi_filter.update(_summary_date_filter(granularity, summary_q)) summary_chongzhi = filter_queryset_by_club( Czjilu.query.filter(**summary_chongzhi_filter), request, ).aggregate(total_count=Count('id'), total_amount=Sum('jine')) total_chongzhi_count = summary_chongzhi['total_count'] or 0 total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0) total_guanshi = 0.0 total_zuzhang = 0.0 if include_guanshi or include_zuzhang: summary_fenhong_filter = dict(fenhong_filter) summary_fenhong_filter.pop('CreateTime__gte', None) summary_fenhong_filter.pop('CreateTime__lt', None) summary_fenhong_filter.update(_summary_date_filter(granularity, summary_q)) fenhong_agg = {} if include_guanshi: fenhong_agg['guanshi_sum'] = Sum('fenhong') if include_zuzhang: fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') summary_fenhong = filter_gsfenhong_qs( Gsfenhong.query.filter(**summary_fenhong_filter), request, ).aggregate(**fenhong_agg) total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2) return { **_meta(request), 'time_series': time_series, 'summary': { 'total_chongzhi_count': total_chongzhi_count, 'total_chongzhi_amount': total_chongzhi_amount, 'total_guanshi_fenhong': total_guanshi, 'total_zuzhang_fenhong': total_zuzhang, 'total_shouyi': total_shouyi, }, }, None