会员单不再统一 yaoqiuleixing=0;clumber 裁剪为仅有权限的会员;ddhq 返回 hasRequiredMember。 Co-authored-by: Cursor <cursoragent@cursor.com>
428 lines
15 KiB
Python
428 lines
15 KiB
Python
"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
|
||
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_id;has_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
|