Files
Django/jituan/services/huiyuan_bundle.py
XingQue 3ec821ae6a fix: 抢单池按订单会员类型逐单校验,取消任意会员放开全部单
会员单不再统一 yaoqiuleixing=0;clumber 裁剪为仅有权限的会员;ddhq 返回 hasRequiredMember。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:59:00 +08:00

428 lines
15 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.
"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
from datetime import timedelta
from django.utils import timezone
from products.models import Huiyuan, Huiyuangoumai
def normalize_huiyuan_id(huiyuan_id):
if huiyuan_id is None:
return ''
return str(huiyuan_id).strip()
def huiyuan_ids_match(a, b):
"""会员 ID 等价比较忽略首尾空格001 与 1 视为相同)。"""
left = normalize_huiyuan_id(a)
right = normalize_huiyuan_id(b)
if not left or not right:
return False
if left == right:
return True
left_core = left.lstrip('0') or '0'
right_core = right.lstrip('0') or '0'
return left_core == right_core
def expand_huiyuan_id_variants(huiyuan_id):
"""老小程序用 == 比较会员 ID补齐 1 / 01 / 001 等写法。"""
hid = normalize_huiyuan_id(huiyuan_id)
if not hid:
return []
variants = []
for candidate in (hid, hid.lstrip('0') or '0'):
if candidate and candidate not in variants:
variants.append(candidate)
core = hid.lstrip('0') or '0'
if core.isdigit():
for width in (3, 4, 6):
padded = core.zfill(width)
if padded not in variants:
variants.append(padded)
return variants
def format_legacy_daoqi(dt):
if not dt:
return ''
if timezone.is_aware(dt):
dt = timezone.localtime(dt)
return dt.strftime('%Y-%m-%d %H:%M:%S')
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 的会员单(含总会员覆盖、体验会员)。"""
req = normalize_huiyuan_id(required_huiyuan_id)
if not req:
return True
def _record_valid(rec):
return (
rec
and not rec.jiance_shifou_daoqi()
and rec.huiyuan_zhuangtai == 1
)
for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id):
if _record_valid(rec) and huiyuan_ids_match(rec.huiyuan_id, req):
return True
bundle_ids = set(
Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True)
)
if not bundle_ids:
return False
req_set = {req, req.lstrip('0') or '0'}
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)
included_set = set()
for x in included:
sx = normalize_huiyuan_id(x)
if sx:
included_set.add(sx)
included_set.add(sx.lstrip('0') or '0')
if req_set & included_set:
return True
if _czjilu_grants_huiyuan_access(yonghu_id, req, club_id):
return True
return False
def _czjilu_grants_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None):
"""已支付会员充值单(含体验)在有效期内,视同已开通。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.member_recharge import (
MEMBER_LEIXING,
MEMBER_PAID_STATUS,
_order_is_trial_like,
club_huiyuan_sellable,
)
from products.models import Czjilu
req = normalize_huiyuan_id(required_huiyuan_id)
if not req:
return False
now = timezone.now()
for order in Czjilu.query.filter(
yonghuid=yonghu_id,
leixing=MEMBER_LEIXING,
zhuangtai=MEMBER_PAID_STATUS,
).order_by('-CreateTime')[:30]:
if not huiyuan_ids_match(order.huiyuan_id, req):
continue
cid = getattr(order, 'club_id', None) or club_id or CLUB_ID_DEFAULT
is_trial = _order_is_trial_like(order, cid)
days = int(getattr(order, 'purchase_days', None) or 0)
if days <= 0:
_, _, days = club_huiyuan_sellable(cid, order.huiyuan_id, is_trial=is_trial)
days = max(int(days or 0), 1)
start = order.CreateTime or now
if timezone.is_naive(start):
start = timezone.make_aware(start, timezone.get_current_timezone())
if start + timedelta(days=days) > now:
return True
return False
def user_has_huiyuan_access_any(yonghu_id, required_ids, club_id=None):
for rid in required_ids or []:
if user_has_huiyuan_access(yonghu_id, rid, club_id=club_id):
return True
return False
def resolve_order_membership_ids(order_dict):
"""订单可能关联的会员 ID订单字段 + 商品类型配置,兼容老数据)。"""
ids = []
mid = normalize_huiyuan_id(order_dict.get('MembershipID'))
if mid:
ids.append(mid)
grab_req = int(order_dict.get('GrabRequirement') or 0)
if grab_req != 1:
return ids
leixing_id = order_dict.get('ProductTypeID')
if not leixing_id:
return ids
try:
from products.models import ShangpinLeixing
row = ShangpinLeixing.query.filter(id=leixing_id).values('huiyuan_id').first()
if row:
lx_hid = normalize_huiyuan_id(row.get('huiyuan_id'))
if lx_hid and not any(huiyuan_ids_match(lx_hid, x) for x in ids):
ids.append(lx_hid)
except Exception:
pass
return ids
def resolve_order_membership_id(order_dict):
ids = resolve_order_membership_ids(order_dict)
return ids[0] if ids else ''
def _prune_clumber_items(yonghu_id, items, club_id=None):
"""clumber 只保留用户当前真有权限的会员(含总会员子类型)。"""
if not items:
return []
from collections import defaultdict
groups = defaultdict(list)
for item in items:
hid = normalize_huiyuan_id(item.get('huiyuanid') or item.get('huiyuan_id'))
if not hid:
continue
core = hid.lstrip('0') or '0'
groups[core].append(item)
kept = []
for group_items in groups.values():
sample_hid = group_items[0].get('huiyuanid') or group_items[0].get('huiyuan_id')
if user_has_huiyuan_access(yonghu_id, sample_hid, club_id=club_id):
kept.extend(group_items)
return kept
def resolve_grab_pool_member_display(order_dict, yonghu_id, club_id=None, user_clumber=None):
"""
老小程序抢单池:按「本单所需会员类型」逐单判断,禁止有任意会员就放开全部订单。
会员单固定返回 yaoqiuleixing=1 + 本单 huiyuan_idhas_access 供 hasRequiredMember。
"""
grab_req = int(order_dict.get('GrabRequirement') or 0)
membership_id = resolve_order_membership_id(order_dict)
if grab_req != 1:
return grab_req, membership_id or '', True
required_ids = resolve_order_membership_ids(order_dict)
has_access = user_has_huiyuan_access_any(yonghu_id, required_ids, club_id=club_id)
return 1, membership_id or '', has_access
def enrich_clumber_item(record):
"""老端 clumber有效期内体验会员与正式会员返回格式完全一致。"""
daoqi_str = format_legacy_daoqi(record.daoqi_time)
item = {
'huiyuanid': record.huiyuan_id,
'huiyuan_id': record.huiyuan_id,
'huiyuanming': record.jieshao or f'会员{record.huiyuan_id}',
'daoqi': daoqi_str,
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
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 _append_clumber_id_variants(items, seen, base_item, huiyuan_id):
"""为老端 == 比较补齐多种会员 ID 写法。"""
for vid in expand_huiyuan_id_variants(huiyuan_id):
if vid in seen:
continue
seen.add(vid)
items.append({
**base_item,
'huiyuanid': vid,
'huiyuan_id': vid,
})
def _append_bundle_sub_clumber(items, seen, record, base_item):
"""总会员有效时,为抢单页展开子会员虚拟条目。"""
if not base_item.get('is_bundle'):
return
bundle_hid = str(record.huiyuan_id)
for sub_id in base_item.get('included_huiyuan_ids') or []:
sid = str(sub_id).strip()
if not sid:
continue
sub_base = {
'huiyuanming': base_item.get('huiyuanming', ''),
'daoqi': base_item.get('daoqi', ''),
'from_bundle': bundle_hid,
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
_append_clumber_id_variants(items, seen, sub_base, sid)
def _build_user_clumber_core(yonghu_id):
items = []
seen = set()
for record in Huiyuangoumai.query.filter(yonghu_id=yonghu_id):
if record.jiance_shifou_daoqi():
continue
if record.huiyuan_zhuangtai != 1:
continue
base = enrich_clumber_item(record)
_append_clumber_id_variants(items, seen, base, record.huiyuan_id)
_append_bundle_sub_clumber(items, seen, record, base)
return items
def _build_clumber_from_paid_czjilu(yonghu_id):
"""huiyuangoumai 缺失时,用已支付会员充值单合成 clumber体验=正式)。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.member_recharge import (
MEMBER_LEIXING,
MEMBER_PAID_STATUS,
_order_is_trial_like,
club_huiyuan_sellable,
)
from products.models import Czjilu, Huiyuan
items = []
seen = set()
now = timezone.now()
for order in Czjilu.query.filter(
yonghuid=yonghu_id,
leixing=MEMBER_LEIXING,
zhuangtai=MEMBER_PAID_STATUS,
).order_by('-CreateTime')[:30]:
hid = normalize_huiyuan_id(order.huiyuan_id)
if not hid:
continue
cid = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
is_trial = _order_is_trial_like(order, cid)
days = int(getattr(order, 'purchase_days', None) or 0)
if days <= 0:
_, _, days = club_huiyuan_sellable(cid, hid, is_trial=is_trial)
days = max(int(days or 0), 1)
start = order.CreateTime or now
if timezone.is_naive(start):
start = timezone.make_aware(start, timezone.get_current_timezone())
daoqi = start + timedelta(days=days)
if daoqi <= now:
continue
try:
hy = Huiyuan.query.get(huiyuan_id=hid)
ming = hy.jieshao or f'会员{hid}'
except Huiyuan.DoesNotExist:
ming = f'会员{hid}'
base = {
'huiyuanming': ming,
'daoqi': format_legacy_daoqi(daoqi),
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
_append_clumber_id_variants(items, seen, base, hid)
return items
def ensure_grab_pool_clumber(yonghu_id, club_id=None):
"""
【仅抢单池展示】同步权益并构造 clumber供老小程序按 huiyuan_id 逐单匹配。
不得用于提现/收款等权限校验;提现须走 check_dashou_has_formal_huiyuan_for_withdraw。
"""
from jituan.services.member_recharge import sync_member_entitlements_for_user
sync_member_entitlements_for_user(yonghu_id)
items = _build_user_clumber_core(yonghu_id)
if not items:
items = _build_clumber_from_paid_czjilu(yonghu_id)
items = _prune_clumber_items(yonghu_id, items, club_id=club_id)
return bool(items), items
def build_user_clumber(yonghu_id):
"""
老小程序抢单页 clumber
- 字段 huiyuanid必须+ daoqi 字符串
- 体验会员、总会员子类型均展开
- 同一会员补齐 1/001 等 ID 变体
"""
_, items = ensure_grab_pool_clumber(yonghu_id)
return items
def build_auth_clumber(yonghu_id):
"""登录接口 clumber老端认 huiyuanid保留原状态字段。"""
from jituan.services.member_recharge import sync_member_entitlements_for_user
sync_member_entitlements_for_user(yonghu_id)
items = []
seen = set()
for record in Huiyuangoumai.query.filter(yonghu_id=yonghu_id):
shifou_daoqi = record.jiance_shifou_daoqi()
base = enrich_clumber_item(record)
base.update({
'huiyuan_zhuangtai': 1 if (not shifou_daoqi and record.huiyuan_zhuangtai == 1) else record.huiyuan_zhuangtai,
'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
'shifou_daoqi': shifou_daoqi,
'jieshao': record.jieshao,
})
_append_clumber_id_variants(items, seen, base, record.huiyuan_id)
if not shifou_daoqi and record.huiyuan_zhuangtai == 1:
_append_bundle_sub_clumber(items, seen, record, base)
return items
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