"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。""" 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 resolve_grab_pool_member_display(order_dict, yonghu_id, club_id=None, user_clumber=None): """ 老小程序抢单池:yaoqiuleixing==1 且 huiyuan_id 有值才会显示「未开通会员」。 用户只要有任意有效会员(体验=正式),会员单一律返回 yaoqiuleixing=0。 """ 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 if user_clumber is None: _, user_clumber = ensure_grab_pool_clumber(yonghu_id) if user_clumber: return 0, '', 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 = 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): """ 【仅抢单池展示】同步权益并构造 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) if not items: items = _build_clumber_from_paid_czjilu(yonghu_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