Files
Django/jituan/services/huiyuan_bundle.py
XingQue 4eb74ae968 fix: 修复抢单池 clumber 合成时 naive/aware 时间比较报错
统一 czjilu 到期时间与 now 的时区类型,修复 ddhq 500。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:09:48 +08:00

456 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):
"""用户是否可接要求 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 = _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'))
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):
"""
抢单池展示(仅老端 UI不影响 /qiangdan 真实校验):
- 本单有对应会员(含体验)→ yaoqiuleixing=0老端显示分佣/可抢
- 本单无对应会员 → yaoqiuleixing=1 + huiyuan_id老端显示未开通
禁止「有任意会员就放开全部单」。
"""
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)
if has_access:
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):
"""
【仅抢单池展示】同步权益并构造 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)
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)
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