"""后台会员管理:全局 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 上架'