新增 hy-recharge-records 查询 czjilu 会员单并关联 gsfenhong 分红;hy-bundle-create 支持俱乐部一步创建总会员;会员数据分析页权限改为 3300a。 Co-authored-by: Cursor <cursoragent@cursor.com>
309 lines
11 KiB
Python
309 lines
11 KiB
Python
"""后台会员充值记录明细(czjilu leixing=1)+ 管事/组长分红关联查询。"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
|
||
from django.db.models import Q, Sum, Count
|
||
from django.utils import timezone
|
||
|
||
from jituan.services.club_context import filter_queryset_by_club, resolve_club_scope, resolve_club_id_from_request
|
||
from jituan.services.club_user_access import list_response_meta
|
||
from jituan.services.member_recharge import (
|
||
MEMBER_LEIXING,
|
||
MEMBER_PAID_STATUS,
|
||
MEMBER_PENDING_STATUS,
|
||
resolve_formal_purchase_cishu,
|
||
)
|
||
from jituan.constants import DATA_SCOPE_ALL
|
||
from products.models import Czjilu, Gsfenhong, Huiyuan
|
||
from users.models import UserDashou, UserBoss
|
||
|
||
MEMBER_STATUS_LABELS = {
|
||
MEMBER_PAID_STATUS: '已支付',
|
||
MEMBER_PENDING_STATUS: '待支付',
|
||
}
|
||
|
||
|
||
def _parse_dt(value):
|
||
if not value:
|
||
return None
|
||
if isinstance(value, datetime):
|
||
return value
|
||
text = str(value).strip()
|
||
if not text:
|
||
return None
|
||
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d'):
|
||
try:
|
||
dt = datetime.strptime(text[:19] if 'T' not in text else text.replace('Z', '')[:19], fmt)
|
||
if timezone.is_naive(dt):
|
||
return timezone.make_aware(dt, timezone.get_current_timezone())
|
||
return dt
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
|
||
|
||
def _batch_user_display_names(user_ids):
|
||
user_ids = [x for x in {str(i).strip() for i in user_ids if i} if x]
|
||
if not user_ids:
|
||
return {}
|
||
result = {uid: uid for uid in user_ids}
|
||
dashou_map = {
|
||
r.user.UserUID: (r.nicheng or '').strip()
|
||
for r in UserDashou.query.filter(user__UserUID__in=user_ids).select_related('user')
|
||
if getattr(r.user, 'UserUID', None)
|
||
}
|
||
boss_map = {
|
||
r.user.UserUID: (r.nickname or '').strip()
|
||
for r in UserBoss.query.filter(user__UserUID__in=user_ids).select_related('user')
|
||
if getattr(r.user, 'UserUID', None)
|
||
}
|
||
for uid in user_ids:
|
||
name = dashou_map.get(uid) or boss_map.get(uid)
|
||
if name:
|
||
result[uid] = name
|
||
return result
|
||
|
||
|
||
def _build_base_queryset(request):
|
||
qs = Czjilu.query.filter(leixing=MEMBER_LEIXING)
|
||
return filter_queryset_by_club(qs, request, club_field='club_id')
|
||
|
||
|
||
def _apply_filters(qs, filters):
|
||
f = filters or {}
|
||
|
||
dingdan_id = (f.get('dingdan_id') or '').strip()
|
||
if dingdan_id:
|
||
qs = qs.filter(dingdan_id__icontains=dingdan_id)
|
||
|
||
yonghuid = (f.get('yonghuid') or '').strip()
|
||
if yonghuid:
|
||
qs = qs.filter(yonghuid__icontains=yonghuid)
|
||
|
||
buyer_keyword = (f.get('buyer_keyword') or f.get('dashou_keyword') or '').strip()
|
||
if buyer_keyword:
|
||
dashou_ids = list(
|
||
UserDashou.query.filter(nicheng__icontains=buyer_keyword).values_list('user__UserUID', flat=True)
|
||
)
|
||
qs = qs.filter(
|
||
Q(yonghuid__icontains=buyer_keyword)
|
||
| Q(shuoming__icontains=buyer_keyword)
|
||
| Q(yonghuid__in=dashou_ids)
|
||
)
|
||
|
||
huiyuan_id = f.get('huiyuan_id')
|
||
if huiyuan_id:
|
||
if isinstance(huiyuan_id, (list, tuple)):
|
||
ids = [str(x).strip() for x in huiyuan_id if str(x).strip()]
|
||
if ids:
|
||
qs = qs.filter(huiyuan_id__in=ids)
|
||
else:
|
||
qs = qs.filter(huiyuan_id=str(huiyuan_id).strip())
|
||
|
||
if f.get('is_trial') is not None and f.get('is_trial') != '':
|
||
qs = qs.filter(is_trial=bool(f.get('is_trial')))
|
||
|
||
zhuangtai = f.get('zhuangtai')
|
||
if zhuangtai is not None and zhuangtai != '':
|
||
qs = qs.filter(zhuangtai=int(zhuangtai))
|
||
|
||
date_from = _parse_dt(f.get('date_from') or f.get('start_time'))
|
||
date_to = _parse_dt(f.get('date_to') or f.get('end_time'))
|
||
if date_from:
|
||
qs = qs.filter(CreateTime__gte=date_from)
|
||
if date_to:
|
||
if len(str(f.get('date_to') or f.get('end_time') or '')) <= 10:
|
||
date_to = date_to.replace(hour=23, minute=59, second=59)
|
||
qs = qs.filter(CreateTime__lte=date_to)
|
||
|
||
jine_min = f.get('jine_min')
|
||
jine_max = f.get('jine_max')
|
||
if jine_min not in (None, ''):
|
||
qs = qs.filter(jine__gte=Decimal(str(jine_min)))
|
||
if jine_max not in (None, ''):
|
||
qs = qs.filter(jine__lte=Decimal(str(jine_max)))
|
||
|
||
guanshi_id = (f.get('guanshi_id') or '').strip()
|
||
if guanshi_id:
|
||
order_ids = Gsfenhong.query.filter(
|
||
guanshi__icontains=guanshi_id,
|
||
fenhong_leixing=1,
|
||
).values_list('dingdan_id', flat=True)
|
||
qs = qs.filter(dingdan_id__in=list(order_ids))
|
||
|
||
zuzhang_id = (f.get('zuzhang_id') or '').strip()
|
||
if zuzhang_id:
|
||
order_ids = Gsfenhong.query.filter(
|
||
zuzhang_id__icontains=zuzhang_id,
|
||
fenhong_leixing=1,
|
||
).values_list('dingdan_id', flat=True)
|
||
qs = qs.filter(dingdan_id__in=list(order_ids))
|
||
|
||
keyword = (f.get('keyword') or '').strip()
|
||
if keyword:
|
||
qs = qs.filter(
|
||
Q(dingdan_id__icontains=keyword)
|
||
| Q(yonghuid__icontains=keyword)
|
||
| Q(shuoming__icontains=keyword)
|
||
| Q(huiyuan_id__icontains=keyword)
|
||
)
|
||
|
||
is_bundle = f.get('is_bundle')
|
||
if is_bundle is not None and is_bundle != '':
|
||
bundle_ids = list(
|
||
Huiyuan.query.filter(is_bundle=bool(is_bundle)).values_list('huiyuan_id', flat=True)
|
||
)
|
||
if bool(is_bundle):
|
||
qs = qs.filter(huiyuan_id__in=bundle_ids)
|
||
else:
|
||
qs = qs.exclude(huiyuan_id__in=bundle_ids)
|
||
|
||
has_fenhong = f.get('has_fenhong')
|
||
if has_fenhong is not None and has_fenhong != '':
|
||
paid_ids = Gsfenhong.query.filter(fenhong_leixing=1).values_list('dingdan_id', flat=True)
|
||
paid_set = set(paid_ids)
|
||
if bool(has_fenhong):
|
||
qs = qs.filter(dingdan_id__in=paid_set)
|
||
else:
|
||
qs = qs.exclude(dingdan_id__in=paid_set)
|
||
|
||
club_id = (f.get('club_id') or '').strip()
|
||
if club_id:
|
||
qs = qs.filter(club_id=club_id)
|
||
|
||
return qs
|
||
|
||
|
||
def _build_summary(qs):
|
||
paid_qs = qs.filter(zhuangtai=MEMBER_PAID_STATUS)
|
||
paid_ids = list(paid_qs.values_list('dingdan_id', flat=True))
|
||
fh_qs = Gsfenhong.query.filter(dingdan_id__in=paid_ids, fenhong_leixing=1)
|
||
agg = paid_qs.aggregate(
|
||
paid_count=Count('dingdan_id'),
|
||
paid_jine=Sum('jine'),
|
||
trial_count=Count('dingdan_id', filter=Q(is_trial=True)),
|
||
)
|
||
fh_agg = fh_qs.aggregate(
|
||
guanshi_total=Sum('fenhong'),
|
||
zuzhang_total=Sum('zuzhang_fenhong'),
|
||
fenhong_count=Count('dingdan_id'),
|
||
)
|
||
return {
|
||
'total_count': qs.count(),
|
||
'paid_count': int(agg.get('paid_count') or 0),
|
||
'pending_count': qs.filter(zhuangtai=MEMBER_PENDING_STATUS).count(),
|
||
'paid_jine': str(agg.get('paid_jine') or Decimal('0')),
|
||
'trial_count': int(agg.get('trial_count') or 0),
|
||
'guanshi_fenhong_total': str(fh_agg.get('guanshi_total') or Decimal('0')),
|
||
'zuzhang_fenhong_total': str(fh_agg.get('zuzhang_total') or Decimal('0')),
|
||
'fenhong_record_count': int(fh_agg.get('fenhong_count') or 0),
|
||
}
|
||
|
||
|
||
def _huiyuan_options(request):
|
||
qs = Huiyuan.query.all().order_by('-CreateTime')
|
||
return [
|
||
{
|
||
'huiyuan_id': m.huiyuan_id,
|
||
'jieshao': m.jieshao,
|
||
'is_bundle': bool(getattr(m, 'is_bundle', False)),
|
||
}
|
||
for m in qs
|
||
]
|
||
|
||
|
||
def build_member_recharge_records_payload(request, filters=None, page=1, page_size=20):
|
||
page = max(1, int(page or 1))
|
||
page_size = min(100, max(1, int(page_size or 20)))
|
||
|
||
qs = _apply_filters(_build_base_queryset(request), filters)
|
||
qs = qs.order_by('-CreateTime')
|
||
total = qs.count()
|
||
offset = (page - 1) * page_size
|
||
rows = list(qs[offset:offset + page_size])
|
||
|
||
dingdan_ids = [r.dingdan_id for r in rows]
|
||
fenhong_map = {
|
||
g.dingdan_id: g
|
||
for g in Gsfenhong.query.filter(dingdan_id__in=dingdan_ids, fenhong_leixing=1)
|
||
}
|
||
|
||
huiyuan_ids = {r.huiyuan_id for r in rows if r.huiyuan_id}
|
||
huiyuan_map = {m.huiyuan_id: m for m in Huiyuan.query.filter(huiyuan_id__in=huiyuan_ids)}
|
||
|
||
user_ids = {r.yonghuid for r in rows if r.yonghuid}
|
||
for g in fenhong_map.values():
|
||
if g.guanshi:
|
||
user_ids.add(g.guanshi)
|
||
if g.zuzhang_id:
|
||
user_ids.add(g.zuzhang_id)
|
||
if g.dashouid:
|
||
user_ids.add(g.dashouid)
|
||
name_map = _batch_user_display_names(user_ids)
|
||
|
||
items = []
|
||
for order in rows:
|
||
hy = huiyuan_map.get(order.huiyuan_id)
|
||
fh = fenhong_map.get(order.dingdan_id)
|
||
formal_cishu = None
|
||
if order.zhuangtai == MEMBER_PAID_STATUS and not order.is_trial:
|
||
formal_cishu = resolve_formal_purchase_cishu(
|
||
order.yonghuid, order.huiyuan_id, order.dingdan_id, order.club_id,
|
||
)
|
||
|
||
fenhong_detail = None
|
||
if fh:
|
||
fenhong_detail = {
|
||
'guanshi_id': fh.guanshi or '',
|
||
'guanshi_nicheng': name_map.get(fh.guanshi, fh.guanshi or ''),
|
||
'guanshi_fenhong': str(fh.fenhong or 0),
|
||
'zuzhang_id': fh.zuzhang_id or '',
|
||
'zuzhang_nicheng': name_map.get(fh.zuzhang_id, fh.zuzhang_id or ''),
|
||
'zuzhang_fenhong': str(fh.zuzhang_fenhong or 0),
|
||
'fenhong_leixing': fh.fenhong_leixing,
|
||
'is_trial': bool(fh.is_trial),
|
||
'fenhong_time': fh.CreateTime.isoformat() if fh.CreateTime else None,
|
||
'formal_cishu': formal_cishu,
|
||
'shuoming': fh.shuoming or '',
|
||
}
|
||
|
||
items.append({
|
||
'dingdan_id': order.dingdan_id,
|
||
'yonghuid': order.yonghuid,
|
||
'buyer_nicheng': name_map.get(order.yonghuid, order.yonghuid),
|
||
'huiyuan_id': order.huiyuan_id,
|
||
'huiyuan_name': hy.jieshao if hy else (order.huiyuan_id or ''),
|
||
'is_bundle': bool(hy and getattr(hy, 'is_bundle', False)),
|
||
'jine': str(order.jine or 0),
|
||
'zhuangtai': order.zhuangtai,
|
||
'zhuangtai_label': MEMBER_STATUS_LABELS.get(order.zhuangtai, f'状态{order.zhuangtai}'),
|
||
'is_trial': bool(order.is_trial),
|
||
'purchase_days': order.purchase_days,
|
||
'shuoming': order.shuoming or '',
|
||
'club_id': order.club_id,
|
||
'CreateTime': order.CreateTime.isoformat() if order.CreateTime else None,
|
||
'UpdateTime': order.UpdateTime.isoformat() if order.UpdateTime else None,
|
||
'formal_cishu': formal_cishu,
|
||
'fenhong': fenhong_detail,
|
||
})
|
||
|
||
return {
|
||
'list': items,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
'summary': _build_summary(_apply_filters(_build_base_queryset(request), filters)),
|
||
'filter_options': {
|
||
'huiyuan_list': _huiyuan_options(request),
|
||
'zhuangtai_options': [
|
||
{'value': MEMBER_PAID_STATUS, 'label': '已支付'},
|
||
{'value': MEMBER_PENDING_STATUS, 'label': '待支付'},
|
||
],
|
||
},
|
||
'scope': resolve_club_scope(request),
|
||
'club_id': resolve_club_id_from_request(request),
|
||
**list_response_meta(request),
|
||
}
|