Files
Django/jituan/services/huiyuan_bundle.py
XingQue bcfa30d5de fix: 抢单池展示只校验对应会员未过期,体验与正式同等
ddhq 不区分体验/正式;qiangdan 校验会员+过期;提现仍禁体验会员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:21:01 +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.
"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
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 _normalize_datetime_for_compare(dt):
"""统一 naive/aware避免 daoqi 与 now 比较时报 TypeError。"""
from django.conf import settings
if dt is None:
return timezone.now()
if settings.USE_TZ:
if timezone.is_naive(dt):
return timezone.make_aware(dt, timezone.get_current_timezone())
return dt
if timezone.is_aware(dt):
return timezone.make_naive(dt, timezone.get_current_timezone())
return dt
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):
"""
抢单 /qiangdan 校验:有无对应会员且未过期(体验会员可抢对应类型单)。
总会员按包含子会员判断。提现不走此函数。
"""
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 bundle_ids:
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 = _normalize_datetime_for_compare(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 = _normalize_datetime_for_compare(order.CreateTime or now)
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') or order_dict.get('huiyuan_id')
)
if mid:
ids.append(mid)
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
if grab_req != 1:
return ids
leixing_id = order_dict.get('ProductTypeID') or order_dict.get('leixing_id')
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 pool_display_has_valid_member(yonghu_id, required_huiyuan_id, club_id=None):
"""
【仅 dddhq 抢单池展示】有无对应会员且未过期。
体验会员与正式会员完全同等;不读 is_trial不拦截体验。
总会员按包含子会员判断。
"""
return user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=club_id)
def pool_display_has_valid_member_for_order(order_dict, yonghu_id, club_id=None):
"""抢单池:本单所需会员类型是否有效(只看类型+过期,体验=正式)。"""
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
if grab_req != 1:
return True
required_ids = resolve_order_membership_ids(order_dict)
if not required_ids:
return False
for rid in required_ids:
if pool_display_has_valid_member(yonghu_id, rid, club_id=club_id):
return True
return False
def resolve_grab_pool_member_display(order_dict, yonghu_id, club_id=None, user_clumber=None):
"""
【仅 dddhq 展示,骗老前端】有对应未过期会员(体验=正式)→ yaoqiuleixing=0。
无对应/已过期 → yaoqiuleixing=1。不区分体验/正式。
真实抢单 /dingdan/qiangdan提现另走严格校验。
"""
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
membership_id = resolve_order_membership_id(order_dict)
if grab_req != 1:
return grab_req, membership_id or '', True
has_valid = pool_display_has_valid_member_for_order(order_dict, yonghu_id, club_id=club_id)
if has_valid:
return 0, membership_id or '', True
return 1, membership_id or '', False
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 = _normalize_datetime_for_compare(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 = _normalize_datetime_for_compare(order.CreateTime or now)
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):
"""
【仅抢单池 dddhq 展示】体验会员与正式会员同格式写入 clumber。
不得用于提现;提现须 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)
extra = _build_clumber_from_paid_czjilu(yonghu_id)
if extra:
seen_cores = set()
for it in items:
hid = normalize_huiyuan_id(it.get('huiyuanid') or it.get('huiyuan_id'))
if hid:
seen_cores.add(hid.lstrip('0') or '0')
for it in extra:
hid = normalize_huiyuan_id(it.get('huiyuanid') or it.get('huiyuan_id'))
if not hid:
continue
core = hid.lstrip('0') or '0'
if core not in seen_cores:
items.append(it)
seen_cores.add(core)
return bool(items), items
def build_user_clumber(yonghu_id, club_id=None):
"""
老小程序抢单页 clumber体验=正式格式)。
仅展示用;只含未过期会员。
"""
_, items = ensure_grab_pool_clumber(yonghu_id, club_id=club_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