Files
Django/jituan/services/club_member_admin.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

455 lines
16 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.
"""后台会员管理:全局 Huiyuan 定义 + 各俱乐部 ClubHuiyuanPrice 售价/分成/体验配置。"""
import logging
import random
import string
from decimal import Decimal
from django.db import transaction, IntegrityError
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.models import ClubHuiyuanPrice
from jituan.services.club_context import club_id_for_write, resolve_club_id_from_request, resolve_club_scope
from jituan.services.club_user_access import list_response_meta
from products.models import Huiyuan
logger = logging.getLogger(__name__)
CLUB_WRITE_SCOPE_MSG = '当前为集团汇总视图,请先在顶栏切换到具体俱乐部后再操作'
def club_write_blocked(request):
return resolve_club_scope(request) == DATA_SCOPE_ALL
def _price_row_to_dict(row):
if not row:
return {}
return {
'formal_days': int(row.formal_days or 30),
'trial_enabled': bool(row.trial_enabled),
'trial_price': str(row.trial_price or 0),
'trial_days': int(row.trial_days or 0),
'trial_guanshifc': str(row.trial_guanshifc or 0),
'trial_zuzhangfc': str(row.trial_zuzhangfc or 0),
'card_image': (row.card_image or '').strip(),
'card_bg': (row.card_bg or '').strip(),
}
def _format_member(m, price_row=None, club_id=None):
global_jiage = str(m.jiage)
global_guanshifc = str(m.guanshifc)
global_zuzhangfc = str(m.zuzhangfc)
item = {
'huiyuan_id': m.huiyuan_id,
'jieshao': m.jieshao,
'jtjieshao': m.jtjieshao,
'jiage': global_jiage,
'guanshifc': global_guanshifc,
'zuzhangfc': global_zuzhangfc,
'global_jiage': global_jiage,
'global_guanshifc': global_guanshifc,
'global_zuzhangfc': global_zuzhangfc,
'goumai_cishu': m.goumai_cishu,
'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None,
'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime else None,
'bankuai_id': m.bankuai_id,
'club_enabled': True,
'price_source': 'global',
'formal_days': 30,
'trial_enabled': False,
'trial_price': '0.00',
'trial_days': 0,
'trial_guanshifc': '0.00',
'trial_zuzhangfc': '0.00',
'is_bundle': bool(getattr(m, 'is_bundle', False)),
'included_huiyuan_ids': [],
}
if price_row is not None:
item['jiage'] = str(price_row.jiage)
item['guanshifc'] = str(price_row.guanshifc)
item['zuzhangfc'] = str(price_row.zuzhangfc)
item['club_enabled'] = bool(price_row.is_enabled)
item['price_source'] = 'club'
item.update(_price_row_to_dict(price_row))
if item.get('is_bundle') and club_id:
from jituan.services.huiyuan_bundle import get_bundle_included_ids
item['included_huiyuan_ids'] = get_bundle_included_ids(club_id, m.huiyuan_id)
return item
def count_club_member_sales(club_id, huiyuan_id, formal_only=True):
"""本俱乐部已支付会员单数(用于后台展示,非全局 goumai_cishu"""
from products.models import Czjilu
qs = Czjilu.query.filter(
club_id=club_id,
huiyuan_id=huiyuan_id,
leixing=1,
zhuangtai=3,
)
if formal_only:
qs = qs.filter(is_trial=False)
return qs.count()
def build_member_list_payload(request):
scope = resolve_club_scope(request)
club_id = resolve_club_id_from_request(request)
# 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 = {}
if scope != DATA_SCOPE_ALL:
price_map = {
r.huiyuan_id: r
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))
else:
for m in members:
row = price_map.get(m.huiyuan_id)
if row is None and club_id != CLUB_ID_DEFAULT:
continue
if row is not None and not row.is_enabled:
continue
item = _format_member(m, row, club_id=club_id)
item['goumai_cishu'] = _sales_map.get(m.huiyuan_id, 0)
member_list.append(item)
return {
'list': member_list,
'club_id': club_id,
'scope': scope,
'price_note': (
f'当前俱乐部 {club_id} 专属售价club_huiyuan_price'
if scope != DATA_SCOPE_ALL else
'集团汇总:展示全局默认价;切换具体俱乐部可配置售价与体验会员。'
),
**list_response_meta(request),
}
def build_member_catalog_payload(request):
"""集团会员类型 + 本俱乐部未上架的可选列表。"""
club_id = resolve_club_id_from_request(request)
enabled_ids = set(
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
)
# 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}
def _parse_trial_fields(data, jiage, guanshifc, zuzhangfc):
formal_days = int(data.get('formal_days') or 30)
trial_enabled = bool(data.get('trial_enabled'))
trial_price = Decimal(str(data.get('trial_price') or 0))
trial_days = int(data.get('trial_days') or 0)
trial_guanshifc = Decimal(str(data.get('trial_guanshifc') or 0))
trial_zuzhangfc = Decimal(str(data.get('trial_zuzhangfc') or 0))
if formal_days < 1 or formal_days > 3650:
raise ValueError('正式会员天数无效')
if trial_enabled:
if trial_price <= 0 or trial_days <= 0:
raise ValueError('开启体验会员须配置体验价格与天数')
if trial_guanshifc + trial_zuzhangfc > trial_price:
raise ValueError('体验管事+组长分成不能大于体验价格')
if guanshifc + zuzhangfc > jiage:
raise ValueError('管事分成+组长分成不能大于会员价格')
return {
'formal_days': formal_days,
'trial_enabled': trial_enabled,
'trial_price': trial_price,
'trial_days': trial_days,
'trial_guanshifc': trial_guanshifc,
'trial_zuzhangfc': trial_zuzhangfc,
}
def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=None):
if club_write_blocked(request):
return None, CLUB_WRITE_SCOPE_MSG
club_id = club_id_for_write(request)
extra = extra or {}
try:
member_preview = Huiyuan.objects.get(huiyuan_id=huiyuan_id)
if getattr(member_preview, 'is_bundle', False):
extra = dict(extra)
extra['trial_enabled'] = False
except Huiyuan.DoesNotExist:
pass
try:
trial_fields = _parse_trial_fields(extra, jiage, guanshifc, zuzhangfc)
except ValueError as e:
return None, str(e)
with transaction.atomic():
try:
member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id)
except Huiyuan.DoesNotExist:
return None, '会员不存在'
member.jieshao = jieshao
member.jtjieshao = jtjieshao
member.save(update_fields=['jieshao', 'jtjieshao', 'UpdateTime'])
row, created = ClubHuiyuanPrice.query.get_or_create(
club_id=club_id,
huiyuan_id=huiyuan_id,
defaults={
'jiage': jiage,
'guanshifc': guanshifc,
'zuzhangfc': zuzhangfc,
'is_enabled': True,
'bankuai_id': member.bankuai_id,
**trial_fields,
},
)
if not created:
row.jiage = jiage
row.guanshifc = guanshifc
row.zuzhangfc = zuzhangfc
row.is_enabled = True
for k, v in trial_fields.items():
setattr(row, k, v)
if 'card_image' in extra:
row.card_image = (extra.get('card_image') or '').strip()
if 'card_bg' in extra:
row.card_bg = (extra.get('card_bg') or '').strip()
row.save()
if 'included_huiyuan_ids' in extra and getattr(member, 'is_bundle', False):
from jituan.services.huiyuan_bundle import save_bundle_includes
_, err = save_bundle_includes(club_id, huiyuan_id, extra.get('included_huiyuan_ids'))
if err:
return None, err
if club_id == CLUB_ID_DEFAULT:
member.jiage = jiage
member.guanshifc = guanshifc
member.zuzhangfc = zuzhangfc
member.save(update_fields=['jiage', 'guanshifc', 'zuzhangfc', 'UpdateTime'])
return {'huiyuan_id': huiyuan_id}, None
MEMBER_CARD_FOLDER = 'beijing/huiyuan'
def upload_member_card_image(request, huiyuan_id, field, file_obj):
"""上传会员充值页卡片图,写入 club_huiyuan_price.card_image / card_bg。"""
import os
from utils.oss_utils import validate_image, upload_to_oss
if club_write_blocked(request):
return None, CLUB_WRITE_SCOPE_MSG
if field not in ('card_image', 'card_bg'):
return None, '无效的图字段'
ok, msg = validate_image(file_obj)
if not ok:
return None, msg
club_id = club_id_for_write(request)
ext = os.path.splitext(getattr(file_obj, 'name', '') or '')[1].lower()
if ext not in ('.png', '.jpg', '.jpeg', '.webp', '.gif'):
ext = '.png'
rel_path = f'{MEMBER_CARD_FOLDER}/{club_id}/{huiyuan_id}_{field}{ext}'
if not upload_to_oss(file_obj, rel_path):
return None, '上传 OSS 失败'
row = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id).first()
if not row:
return None, '请先在当前俱乐部上架该会员'
setattr(row, field, rel_path)
row.save(update_fields=[field, 'UpdateTime'])
return {field: rel_path, 'huiyuan_id': huiyuan_id}, None
def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=None):
"""俱乐部上架集团会员类型。"""
if club_write_blocked(request):
return None, CLUB_WRITE_SCOPE_MSG
club_id = club_id_for_write(request)
extra = extra or {}
try:
member = Huiyuan.query.get(huiyuan_id=huiyuan_id)
except Huiyuan.DoesNotExist:
return None, '集团会员类型不存在'
if getattr(member, 'is_bundle', False):
extra = dict(extra or {})
extra['trial_enabled'] = False
try:
jiage = Decimal(str(jiage))
guanshifc = Decimal(str(guanshifc))
zuzhangfc = Decimal(str(zuzhangfc))
if jiage <= 0:
raise ValueError
trial_fields = _parse_trial_fields(extra, jiage, guanshifc, zuzhangfc)
except ValueError as e:
return None, str(e) if str(e) else '金额或体验配置错误'
row, created = ClubHuiyuanPrice.query.get_or_create(
club_id=club_id,
huiyuan_id=huiyuan_id,
defaults={
'jiage': jiage,
'guanshifc': guanshifc,
'zuzhangfc': zuzhangfc,
'is_enabled': True,
'bankuai_id': member.bankuai_id,
**trial_fields,
},
)
if not created:
row.jiage = jiage
row.guanshifc = guanshifc
row.zuzhangfc = zuzhangfc
row.is_enabled = True
for k, v in trial_fields.items():
setattr(row, k, v)
row.save()
if getattr(member, 'is_bundle', False) and extra.get('included_huiyuan_ids') is not None:
from jituan.services.huiyuan_bundle import save_bundle_includes
_, err = save_bundle_includes(club_id, huiyuan_id, extra.get('included_huiyuan_ids'))
if err:
return None, err
return {'huiyuan_id': huiyuan_id}, None
def disable_member_for_club(request, huiyuan_id):
"""俱乐部软下架。"""
if club_write_blocked(request):
return None, CLUB_WRITE_SCOPE_MSG
club_id = club_id_for_write(request)
updated = ClubHuiyuanPrice.objects.filter(
club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True,
).update(is_enabled=False)
if not updated:
return None, '该会员在本俱乐部未上架或已下架'
return {'huiyuan_id': huiyuan_id}, None
def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None, is_bundle=False):
"""集团创建全局会员类型(仅集团后台)。"""
def generate_huiyuan_id():
while True:
new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
if not Huiyuan.query.filter(huiyuan_id=new_id).exists():
return new_id
try:
with transaction.atomic():
new_id = generate_huiyuan_id()
create_kwargs = dict(
huiyuan_id=new_id,
jieshao=jieshao,
jiage=jiage,
guanshifc=guanshifc,
zuzhangfc=zuzhangfc,
jtjieshao=jtjieshao or '',
goumai_cishu=0,
is_bundle=bool(is_bundle),
)
if bankuai_id is not None:
create_kwargs['bankuai_id'] = bankuai_id
member = Huiyuan.query.create(**create_kwargs)
except IntegrityError as e:
logger.exception('创建全局会员失败')
return None, f'创建失败:{e}'
except Exception as e:
logger.exception('创建全局会员失败')
return None, f'创建失败:{e}'
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):
return None, CLUB_WRITE_SCOPE_MSG
return None, '俱乐部不可新建会员类型,请从集团会员 catalog 上架'