Files
Django/jituan/services/finance_detail_stats.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00

560 lines
21 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.
"""财务子模块统计(订单/会员/其他充值),供 /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