feat: 会员充值明细接口与俱乐部一键创建总会员
新增 hy-recharge-records 查询 czjilu 会员单并关联 gsfenhong 分红;hy-bundle-create 支持俱乐部一步创建总会员;会员数据分析页权限改为 3300a。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -382,6 +382,51 @@ def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_i
|
||||
return {'huiyuan_id': member.huiyuan_id}, None
|
||||
|
||||
|
||||
def create_bundle_member_for_club(
|
||||
request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao,
|
||||
formal_days=30, included_huiyuan_ids=None,
|
||||
):
|
||||
"""俱乐部一键创建总会员:新建全局 is_bundle 类型 + 本俱乐部上架 + 配置包含子会员。"""
|
||||
if club_write_blocked(request):
|
||||
return None, CLUB_WRITE_SCOPE_MSG
|
||||
|
||||
included_huiyuan_ids = included_huiyuan_ids or []
|
||||
if not included_huiyuan_ids:
|
||||
return None, '总会员请至少选择一个包含的子会员'
|
||||
|
||||
try:
|
||||
jiage = Decimal(str(jiage))
|
||||
guanshifc = Decimal(str(guanshifc))
|
||||
zuzhangfc = Decimal(str(zuzhangfc))
|
||||
if jiage <= 0:
|
||||
raise ValueError
|
||||
trial_fields = _parse_trial_fields(
|
||||
{'formal_days': formal_days, 'trial_enabled': False},
|
||||
jiage, guanshifc, zuzhangfc,
|
||||
)
|
||||
except ValueError as e:
|
||||
return None, str(e) if str(e) else '金额或天数配置错误'
|
||||
|
||||
with transaction.atomic():
|
||||
data, err = add_global_member(
|
||||
jieshao, jiage, guanshifc, zuzhangfc, jtjieshao or '', is_bundle=True,
|
||||
)
|
||||
if err:
|
||||
return None, err
|
||||
|
||||
extra = {
|
||||
**trial_fields,
|
||||
'included_huiyuan_ids': included_huiyuan_ids,
|
||||
}
|
||||
_, err = enable_member_for_club(
|
||||
request, data['huiyuan_id'], jiage, guanshifc, zuzhangfc, extra=extra,
|
||||
)
|
||||
if err:
|
||||
return None, err
|
||||
|
||||
return data, None
|
||||
|
||||
|
||||
# 兼容旧名:俱乐部不应再调用
|
||||
def add_member_for_club(request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None):
|
||||
if club_write_blocked(request):
|
||||
|
||||
@@ -73,7 +73,7 @@ KEFU_MENU_ROW_DEFS = [
|
||||
{'page_id': 'member.list', 'name': '会员管理', 'path': '/member/list', 'parent_id': 'member', 'sort_order': 101,
|
||||
'perm_codes': ['3300a']},
|
||||
{'page_id': 'member.data', 'name': '会员数据分析', 'path': '/member/data', 'parent_id': 'member', 'sort_order': 102,
|
||||
'perm_codes': ['huiyuancaiwu']},
|
||||
'perm_codes': ['3300a']},
|
||||
{'page_id': 'miniapp', 'name': '小程序配置', 'path': '', 'parent_id': '', 'sort_order': 110},
|
||||
{'page_id': 'miniapp.carousel', 'name': '轮播图与公告', 'path': '/miniapp/carousel', 'parent_id': 'miniapp', 'sort_order': 111,
|
||||
'perm_codes': ['8080a']},
|
||||
|
||||
308
jituan/services/member_recharge_records.py
Normal file
308
jituan/services/member_recharge_records.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""后台会员充值记录明细(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),
|
||||
}
|
||||
@@ -23,6 +23,8 @@ from backend.view import (
|
||||
ShgxgsjView,
|
||||
UpdateMemberView,
|
||||
MemberCardImageUploadView,
|
||||
MemberRechargeRecordsView,
|
||||
MemberBundleCreateView,
|
||||
UpdateWithdrawSettingsView,
|
||||
)
|
||||
from users.views import KefuPunishmentListView
|
||||
@@ -140,6 +142,8 @@ urlpatterns = [
|
||||
path('houtai/hthysj', ClubMemberEnableView.as_view(), name='jituan_member_enable'),
|
||||
path('houtai/hthyxj', ClubMemberDisableView.as_view(), name='jituan_member_disable'),
|
||||
path('houtai/hy-card-upload', MemberCardImageUploadView.as_view(), name='jituan_member_card_upload'),
|
||||
path('houtai/hy-recharge-records', MemberRechargeRecordsView.as_view(), name='jituan_member_recharge_records'),
|
||||
path('houtai/hy-bundle-create', MemberBundleCreateView.as_view(), name='jituan_member_bundle_create'),
|
||||
path('houtai/club-leixing-list', ClubShangpinLeixingListView.as_view(), name='jituan_club_leixing_list'),
|
||||
path('houtai/club-leixing-catalog', ClubShangpinLeixingCatalogView.as_view(), name='jituan_club_leixing_catalog'),
|
||||
path('houtai/club-leixing-enable', ClubShangpinLeixingEnableView.as_view(), name='jituan_club_leixing_enable'),
|
||||
|
||||
Reference in New Issue
Block a user