feat: 总会员(包含式)模型、迁移、抢单校验与后台配置

- Huiyuan.is_bundle + club_huiyuan_bundle_include 表
- 购买/到期逻辑不变,抢单时总会员覆盖子会员类型
- dshyhq/clumber 返回 is_bundle 与 included_huiyuan_ids

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-05 18:12:57 +08:00
parent 5754a69e0b
commit 37bc8f960a
10 changed files with 228 additions and 21 deletions

View File

@@ -36,7 +36,7 @@ def _price_row_to_dict(row):
}
def _format_member(m, price_row=None):
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)
@@ -62,6 +62,8 @@ def _format_member(m, price_row=None):
'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)
@@ -70,6 +72,9 @@ def _format_member(m, price_row=None):
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
@@ -110,7 +115,7 @@ def build_member_list_payload(request):
continue
if row is not None and not row.is_enabled:
continue
item = _format_member(m, row)
item = _format_member(m, row, club_id=club_id)
item['goumai_cishu'] = count_club_member_sales(club_id, m.huiyuan_id)
member_list.append(item)
@@ -142,6 +147,7 @@ def build_member_catalog_payload(request):
'huiyuan_id': m.huiyuan_id,
'jieshao': m.jieshao,
'jtjieshao': m.jtjieshao,
'is_bundle': bool(getattr(m, 'is_bundle', False)),
})
return {'club_id': club_id, 'catalog': catalog}
@@ -179,6 +185,14 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha
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:
@@ -219,6 +233,12 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha
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
@@ -272,6 +292,10 @@ def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, ext
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))
@@ -302,6 +326,11 @@ def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, ext
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
@@ -318,7 +347,7 @@ def disable_member_for_club(request, huiyuan_id):
return {'huiyuan_id': huiyuan_id}, None
def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None):
def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None, is_bundle=False):
"""集团创建全局会员类型(仅集团后台)。"""
def generate_huiyuan_id():
@@ -338,6 +367,7 @@ def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_i
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

View File

@@ -0,0 +1,117 @@
"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
from products.models import Huiyuan, Huiyuangoumai
def get_bundle_included_ids(club_id, bundle_huiyuan_id):
from jituan.models import ClubHuiyuanBundleInclude
if not club_id or not bundle_huiyuan_id:
return []
return list(
ClubHuiyuanBundleInclude.query.filter(
club_id=club_id,
bundle_huiyuan_id=bundle_huiyuan_id,
).values_list('included_huiyuan_id', flat=True)
)
def is_bundle_huiyuan(huiyuan_id):
try:
return bool(Huiyuan.query.get(huiyuan_id=huiyuan_id).is_bundle)
except Huiyuan.DoesNotExist:
return False
def user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None):
"""用户是否可接要求 required_huiyuan_id 的会员单(含总会员覆盖)。"""
if not required_huiyuan_id:
return True
req = str(required_huiyuan_id).strip()
def _record_valid(rec):
return (
rec
and not rec.jiance_shifou_daoqi()
and rec.huiyuan_zhuangtai == 1
)
try:
direct = Huiyuangoumai.query.get(yonghu_id=yonghu_id, huiyuan_id=req)
if _record_valid(direct):
return True
except Huiyuangoumai.DoesNotExist:
pass
bundle_ids = set(
Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True)
)
if not bundle_ids:
return False
for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id, huiyuan_id__in=bundle_ids):
if not _record_valid(rec):
continue
cid = club_id or rec.club_id or ''
included = get_bundle_included_ids(cid, rec.huiyuan_id)
if req in {str(x) for x in included}:
return True
return False
def enrich_clumber_item(record):
"""为 clumber 条目附加总会员覆盖的子会员 ID 列表(旧端忽略该字段)。"""
item = {
'huiyuanid': record.huiyuan_id,
'huiyuanming': record.jieshao or f'会员{record.huiyuan_id}',
'daoqi': record.daoqi_time,
}
if is_bundle_huiyuan(record.huiyuan_id):
item['is_bundle'] = True
item['included_huiyuan_ids'] = get_bundle_included_ids(
record.club_id or '', record.huiyuan_id,
)
return item
def save_bundle_includes(club_id, bundle_huiyuan_id, included_ids):
"""保存俱乐部总会员包含的子会员(须为本俱乐部已上架的普通会员)。"""
from jituan.models import ClubHuiyuanBundleInclude, ClubHuiyuanPrice
if not is_bundle_huiyuan(bundle_huiyuan_id):
return None, '该会员不是总会员类型'
enabled = set(
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list(
'huiyuan_id', flat=True,
)
)
bundle_ids = set(
Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True)
)
cleaned = []
for hid in included_ids or []:
hid = str(hid).strip()
if not hid or hid == bundle_huiyuan_id:
continue
if hid not in enabled:
return None, f'子会员 {hid} 未在本俱乐部上架'
if hid in bundle_ids:
return None, '总会员不能包含另一个总会员'
if not Huiyuan.query.filter(huiyuan_id=hid).exists():
return None, f'子会员 {hid} 不存在'
cleaned.append(hid)
if not cleaned:
return None, '请至少选择一个包含的子会员'
ClubHuiyuanBundleInclude.query.filter(
club_id=club_id, bundle_huiyuan_id=bundle_huiyuan_id,
).delete()
for hid in cleaned:
ClubHuiyuanBundleInclude.query.create(
club_id=club_id,
bundle_huiyuan_id=bundle_huiyuan_id,
included_huiyuan_id=hid,
)
return {'included_huiyuan_ids': cleaned}, None