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
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu,旧 /houtai/caiwu 不变)。"""
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from django.db.models import Sum
|
||||
from django.db.models import Count, Q, Sum
|
||||
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
from jituan.services.club_context import (
|
||||
@@ -61,31 +61,35 @@ def build_caiwu_payload(request):
|
||||
today_income_amount, today_income_count = today_income_for_dashboard(today, request)
|
||||
today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
|
||||
|
||||
today_orders = order_qs.filter(
|
||||
# 合并订单统计:6 次独立 count/aggregate → 1 次 aggregate
|
||||
_today_order_agg = order_qs.filter(
|
||||
CreateTime__gte=today_start,
|
||||
CreateTime__lt=today_end,
|
||||
Status__in=[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
).aggregate(
|
||||
today_order_count=Count('id'),
|
||||
today_order_amount=Sum('Amount'),
|
||||
today_completed_count=Count('id', filter=Q(Status=3)),
|
||||
today_completed_amount=Sum('Amount', filter=Q(Status=3)),
|
||||
today_tuikuan_count=Count('id', filter=Q(Status=5)),
|
||||
today_tuikuan_amount=Sum('Amount', filter=Q(Status=5)),
|
||||
)
|
||||
today_order_count = today_orders.count()
|
||||
today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||||
today_order_count = _today_order_agg['today_order_count'] or 0
|
||||
today_order_amount = float(_today_order_agg['today_order_amount'] or 0.00)
|
||||
today_completed_count = _today_order_agg['today_completed_count'] or 0
|
||||
today_completed_amount = float(_today_order_agg['today_completed_amount'] or 0.00)
|
||||
today_tuikuan_count = _today_order_agg['today_tuikuan_count'] or 0
|
||||
today_tuikuan_amount = float(_today_order_agg['today_tuikuan_amount'] or 0.00)
|
||||
|
||||
today_completed = order_qs.filter(
|
||||
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=3,
|
||||
)
|
||||
today_completed_count = today_completed.count()
|
||||
today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||||
|
||||
today_tuikuan = order_qs.filter(
|
||||
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=5,
|
||||
)
|
||||
today_tuikuan_count = today_tuikuan.count()
|
||||
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
||||
|
||||
today_chongzhi = cz_qs.filter(
|
||||
# 合并充值统计:2 次独立 count/aggregate → 1 次 aggregate
|
||||
_today_chongzhi_agg = cz_qs.filter(
|
||||
CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3,
|
||||
).aggregate(
|
||||
count=Count('id'),
|
||||
total=Sum('jine'),
|
||||
)
|
||||
today_chongzhi_count = today_chongzhi.count()
|
||||
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
|
||||
today_chongzhi_count = _today_chongzhi_agg['count'] or 0
|
||||
today_chongzhi_amount = float(_today_chongzhi_agg['total'] or 0.00)
|
||||
|
||||
new_huiyuan_user_ids = hy_qs.filter(
|
||||
CreateTime__gte=today_start, CreateTime__lt=today_end,
|
||||
|
||||
@@ -141,10 +141,13 @@ def count_order_status_buckets(q_conditions, request=None):
|
||||
('5', Q(Status=5)),
|
||||
('6', Q(Status=6)),
|
||||
)
|
||||
counts = {'all': order_scope_qs(request).filter(q_conditions).count()}
|
||||
# 合并 8 次独立 count → 1 次 aggregate with 条件 Count
|
||||
from django.db.models import Count
|
||||
_base = order_scope_qs(request).filter(q_conditions)
|
||||
_agg_kwargs = {'all': Count('id')}
|
||||
for key, status_q in buckets:
|
||||
counts[key] = order_scope_qs(request).filter(q_conditions & status_q).count()
|
||||
return counts
|
||||
_agg_kwargs[key] = Count('id', filter=status_q)
|
||||
return _base.aggregate(**_agg_kwargs)
|
||||
|
||||
|
||||
def paginate_fluent_query(fluent_qs, page, page_size):
|
||||
|
||||
@@ -95,7 +95,12 @@ def count_club_member_sales(club_id, huiyuan_id, formal_only=True):
|
||||
def build_member_list_payload(request):
|
||||
scope = resolve_club_scope(request)
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
members = Huiyuan.query.all().order_by('-CreateTime')
|
||||
# P1 优化:限制查询字段,避免加载不必要的大字段
|
||||
members = list(Huiyuan.query.all().only(
|
||||
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
||||
'guanshifc', 'zuzhangfc', 'goumai_cishu', 'bankuai_id',
|
||||
'CreateTime', 'is_bundle',
|
||||
).order_by('-CreateTime'))
|
||||
member_list = []
|
||||
|
||||
price_map = {}
|
||||
@@ -105,6 +110,20 @@ def build_member_list_payload(request):
|
||||
for r in ClubHuiyuanPrice.query.filter(club_id=club_id)
|
||||
}
|
||||
|
||||
# P1 优化:批量预取本俱乐部各会员的已支付单数,避免循环内 N+1
|
||||
_sales_map = {}
|
||||
if scope != DATA_SCOPE_ALL and members:
|
||||
from products.models import Czjilu
|
||||
from django.db.models import Count
|
||||
_sales_rows = Czjilu.query.filter(
|
||||
club_id=club_id,
|
||||
huiyuan_id__in=[m.huiyuan_id for m in members],
|
||||
leixing=1,
|
||||
zhuangtai=3,
|
||||
is_trial=False,
|
||||
).values('huiyuan_id').annotate(cnt=Count('id'))
|
||||
_sales_map = {r['huiyuan_id']: r['cnt'] for r in _sales_rows}
|
||||
|
||||
if scope == DATA_SCOPE_ALL:
|
||||
for m in members:
|
||||
member_list.append(_format_member(m))
|
||||
@@ -116,7 +135,7 @@ def build_member_list_payload(request):
|
||||
if row is not None and not row.is_enabled:
|
||||
continue
|
||||
item = _format_member(m, row, club_id=club_id)
|
||||
item['goumai_cishu'] = count_club_member_sales(club_id, m.huiyuan_id)
|
||||
item['goumai_cishu'] = _sales_map.get(m.huiyuan_id, 0)
|
||||
member_list.append(item)
|
||||
|
||||
return {
|
||||
@@ -138,17 +157,18 @@ def build_member_catalog_payload(request):
|
||||
enabled_ids = set(
|
||||
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
|
||||
)
|
||||
all_members = Huiyuan.query.all().order_by('-CreateTime')
|
||||
catalog = []
|
||||
for m in all_members:
|
||||
if m.huiyuan_id in enabled_ids:
|
||||
continue
|
||||
catalog.append({
|
||||
'huiyuan_id': m.huiyuan_id,
|
||||
'jieshao': m.jieshao,
|
||||
'jtjieshao': m.jtjieshao,
|
||||
'is_bundle': bool(getattr(m, 'is_bundle', False)),
|
||||
})
|
||||
# P1 优化:DB 端排除已上架会员,限制字段,避免全表加载 + Python 过滤
|
||||
catalog_qs = Huiyuan.query.all()
|
||||
if enabled_ids:
|
||||
catalog_qs = catalog_qs.exclude(huiyuan_id__in=list(enabled_ids))
|
||||
catalog = list(
|
||||
catalog_qs.only('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
|
||||
.order_by('-CreateTime')
|
||||
.values('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
|
||||
)
|
||||
# values 返回 dict 列表,需补 is_bundle bool 化
|
||||
for row in catalog:
|
||||
row['is_bundle'] = bool(row.get('is_bundle'))
|
||||
return {'club_id': club_id, 'catalog': catalog}
|
||||
|
||||
|
||||
|
||||
@@ -218,14 +218,22 @@ def build_exam_bundle(request, user):
|
||||
return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)'
|
||||
|
||||
selected_ids = random.sample(all_ids, draw)
|
||||
# 批量预取题目和图片,避免循环内 N+1
|
||||
_questions_map = {
|
||||
q.id: q for q in ClubDashouExamQuestion.query.filter(id__in=selected_ids)
|
||||
}
|
||||
_imgs_map = {}
|
||||
for img in ClubDashouExamQuestionImage.query.filter(
|
||||
club_id=club_id, question_id__in=selected_ids,
|
||||
).order_by('question_id', 'sort_order', 'id'):
|
||||
_imgs_map.setdefault(img.question_id, []).append(img.image_url)
|
||||
|
||||
questions = []
|
||||
for qid in selected_ids:
|
||||
q = ClubDashouExamQuestion.query.get(id=qid)
|
||||
imgs = [
|
||||
r.image_url for r in ClubDashouExamQuestionImage.query.filter(
|
||||
club_id=club_id, question_id=qid,
|
||||
).order_by('sort_order', 'id')
|
||||
]
|
||||
q = _questions_map.get(qid)
|
||||
if q is None:
|
||||
continue
|
||||
imgs = _imgs_map.get(qid, [])
|
||||
questions.append(build_question_bundle_item(q, imgs))
|
||||
|
||||
return {
|
||||
@@ -284,8 +292,14 @@ def build_admin_config_payload(request, user, data=None):
|
||||
if err:
|
||||
return {**clubs_info, 'scope_error': err}
|
||||
cfg = get_or_create_config(club_id)
|
||||
pool = ClubDashouExamQuestion.query.filter(club_id=club_id).count()
|
||||
enabled_pool = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count()
|
||||
# 合并 2 次独立 count → 1 次 aggregate with 条件 Count
|
||||
from django.db.models import Count, Q
|
||||
_pool_agg = ClubDashouExamQuestion.query.filter(club_id=club_id).aggregate(
|
||||
pool=Count('id'),
|
||||
enabled_pool=Count('id', filter=Q(is_enabled=True)),
|
||||
)
|
||||
pool = _pool_agg['pool'] or 0
|
||||
enabled_pool = _pool_agg['enabled_pool'] or 0
|
||||
return {
|
||||
**clubs_info,
|
||||
'club_id': club_id,
|
||||
@@ -319,14 +333,20 @@ def list_admin_questions(request, user, data=None):
|
||||
if err:
|
||||
return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err}
|
||||
rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
||||
out = []
|
||||
for q in rows:
|
||||
imgs = [
|
||||
rows_list = list(rows)
|
||||
_q_ids = [q.id for q in rows_list]
|
||||
# 批量预取所有题目图片,避免循环内 N+1
|
||||
_imgs_map = {}
|
||||
for img in ClubDashouExamQuestionImage.query.filter(
|
||||
club_id=club_id, question_id__in=_q_ids,
|
||||
).order_by('question_id', 'sort_order', 'id'):
|
||||
_imgs_map.setdefault(img.question_id, []).append(
|
||||
{'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order}
|
||||
for img in ClubDashouExamQuestionImage.query.filter(
|
||||
club_id=club_id, question_id=q.id,
|
||||
).order_by('sort_order', 'id')
|
||||
]
|
||||
)
|
||||
|
||||
out = []
|
||||
for q in rows_list:
|
||||
imgs = _imgs_map.get(q.id, [])
|
||||
out.append(question_to_admin_dict(q, imgs))
|
||||
return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out}
|
||||
|
||||
|
||||
@@ -203,19 +203,31 @@ def build_order_finance_payload(
|
||||
})
|
||||
|
||||
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
|
||||
total_order_count = summary_qs.count()
|
||||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
||||
total_success_count = summary_qs.filter(Status=3).count()
|
||||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
total_refund_count = summary_qs.filter(Status=5).count()
|
||||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
||||
s=Sum('pingtai_kuozhan__ShopIncome'),
|
||||
)['s'] or 0
|
||||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
# 合并 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
|
||||
@@ -392,10 +404,16 @@ def build_huiyuan_bankuai_payload(request):
|
||||
'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.query.all():
|
||||
for bk in _bankuai_qs:
|
||||
members = []
|
||||
for m in Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id'):
|
||||
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']]
|
||||
|
||||
@@ -188,11 +188,11 @@ def zero_all_income_statistics():
|
||||
except _PLATFORM_LOG_ERRORS:
|
||||
pass
|
||||
DailyIncomeStat.objects.all().delete()
|
||||
for szjilu in Szjilu.objects.select_for_update().all():
|
||||
szjilu.TotalIncome = _ZERO
|
||||
szjilu.TotalFlow = _ZERO
|
||||
szjilu.DailyFlow = _ZERO
|
||||
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
|
||||
from django.utils import timezone
|
||||
Szjilu.objects.update(
|
||||
TotalIncome=_ZERO, TotalFlow=_ZERO, DailyFlow=_ZERO,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -689,13 +689,12 @@ def get_szjilu_snapshot(club_id=None):
|
||||
|
||||
def reset_all_daily_szjilu():
|
||||
"""每日清零:所有俱乐部的今日流水/今日支出。"""
|
||||
updated = 0
|
||||
with transaction.atomic():
|
||||
for szjilu in Szjilu.objects.select_for_update().all():
|
||||
szjilu.DailyExpense = _ZERO
|
||||
szjilu.DailyFlow = _ZERO
|
||||
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
|
||||
updated += 1
|
||||
from django.utils import timezone
|
||||
updated = Szjilu.objects.update(
|
||||
DailyExpense=_ZERO, DailyFlow=_ZERO,
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,13 @@ class ClubKhgglAddView(APIView):
|
||||
if not user_belongs_to_request_club(user, request):
|
||||
return Response({'code': 403, 'msg': '该用户不属于当前俱乐部,无法添加为考核官'}, status=403)
|
||||
|
||||
# 一次性查询所有板块是否存在,避免循环内 N+1 exists
|
||||
_existing_bids = set(
|
||||
Bankuai.query.filter(bankuai_id__in=bankuai_ids)
|
||||
.values_list('bankuai_id', flat=True)
|
||||
)
|
||||
for bid in bankuai_ids:
|
||||
if not Bankuai.query.filter(bankuai_id=bid).exists():
|
||||
if bid not in _existing_bids:
|
||||
return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400)
|
||||
|
||||
created_new = False
|
||||
|
||||
Reference in New Issue
Block a user