"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。""" 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 [] for bid in expand_huiyuan_id_variants(bundle_huiyuan_id): ids = list( ClubHuiyuanBundleInclude.query.filter( club_id=club_id, bundle_huiyuan_id=bid, ).values_list('included_huiyuan_id', flat=True) ) if ids: return ids stored_bids = ClubHuiyuanBundleInclude.objects.filter( club_id=club_id, ).values_list('bundle_huiyuan_id', flat=True).distinct() for stored_bid in stored_bids: if huiyuan_ids_match(stored_bid, bundle_huiyuan_id): return list( ClubHuiyuanBundleInclude.query.filter( club_id=club_id, bundle_huiyuan_id=stored_bid, ).values_list('included_huiyuan_id', flat=True) ) return [] def _membership_club_candidates(yonghu_id, club_id=None, record_club_id=None): """ 会员/总会员子类型解析用的俱乐部候选(有序)。 Huiyuangoumai/Czjilu.club_id 历史上默认 xq,星之界用户落库为 xq 时不能盲信。 """ from jituan.constants import CLUB_ID_DEFAULT record_cid = (record_club_id or '').strip() request_cid = (club_id or '').strip() user_cid = _resolve_user_club_id(yonghu_id, club_id=club_id) candidates = [] def add(cid): c = (cid or '').strip() if c and c not in candidates: candidates.append(c) if record_cid and record_cid != CLUB_ID_DEFAULT: add(record_cid) add(request_cid) add(user_cid) if record_cid: add(record_cid) add(CLUB_ID_DEFAULT) return candidates def resolve_bundle_included_ids(bundle_huiyuan_id, club_id=None, yonghu_id=None, record_club_id=None): """按候选俱乐部合并总会员包含的子会员(去重,只读)。""" if not bundle_huiyuan_id: return [] merged = [] seen = set() for cid in _membership_club_candidates( yonghu_id, club_id=club_id, record_club_id=record_club_id, ): for hid in get_bundle_included_ids(cid, bundle_huiyuan_id): norm = normalize_huiyuan_id(hid) core = norm.lstrip('0') or '0' if norm else '' dedupe_key = core or norm if dedupe_key and dedupe_key not in seen: seen.add(dedupe_key) merged.append(hid) return merged def _included_huiyuan_id_set(included_ids): included_set = set() for x in included_ids or []: for variant in expand_huiyuan_id_variants(x): sx = normalize_huiyuan_id(variant) if sx: included_set.add(sx) included_set.add(sx.lstrip('0') or '0') return included_set def _resolve_user_club_id(yonghu_id, club_id=None): """解析用户所属俱乐部(体验/正式同一套;总会员子类型依赖此值)。""" from jituan.constants import CLUB_ID_DEFAULT from jituan.services.club_user import get_user_club_id cid = (club_id or '').strip() if cid: return cid try: from users.business_models import User user = User.query.filter(UserUID=yonghu_id).first() return get_user_club_id(user) or CLUB_ID_DEFAULT except Exception: return CLUB_ID_DEFAULT def _record_club_id(record, club_id=None): candidates = _membership_club_candidates( getattr(record, 'yonghu_id', ''), club_id=club_id, record_club_id=getattr(record, 'club_id', None), ) if candidates: return candidates[0] from jituan.constants import CLUB_ID_DEFAULT return CLUB_ID_DEFAULT def is_bundle_huiyuan(huiyuan_id): hid = normalize_huiyuan_id(huiyuan_id) if not hid: return False for bid in Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True): if huiyuan_ids_match(bid, hid): return True return False def _membership_record_active(record): """会员记录是否未过期(体验=正式;时区安全,不依赖 jiance 副作用)。""" if not record or int(record.huiyuan_zhuangtai or 0) != 1: return False if not record.daoqi_time: return False now = _normalize_datetime_for_compare(timezone.now()) daoqi = _normalize_datetime_for_compare(record.daoqi_time) return daoqi > now 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 _membership_record_active(rec) req_set = {req, req.lstrip('0') or '0'} for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id): if not _record_valid(rec): continue if huiyuan_ids_match(rec.huiyuan_id, req): return True if is_bundle_huiyuan(rec.huiyuan_id): included = resolve_bundle_included_ids( rec.huiyuan_id, club_id=club_id, yonghu_id=yonghu_id, record_club_id=rec.club_id, ) if req_set & _included_huiyuan_id_set(included): return True if _czjilu_grants_huiyuan_access(yonghu_id, req, club_id): return True from jituan.services.gift_huiyuan import user_has_gift_huiyuan_access if user_has_gift_huiyuan_access(yonghu_id, req, club_id=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, _czjilu_entitlement_revoked, czjilu_entitlement_active, ) from products.models import Czjilu req = normalize_huiyuan_id(required_huiyuan_id) if not req: return False req_set = {req, req.lstrip('0') or '0'} effective_club = _resolve_user_club_id(yonghu_id, club_id) for order in Czjilu.query.filter( yonghuid=yonghu_id, leixing=MEMBER_LEIXING, zhuangtai=MEMBER_PAID_STATUS, ).order_by('-CreateTime')[:30]: if _czjilu_entitlement_revoked(order): continue cid = getattr(order, 'club_id', None) or club_id or effective_club or CLUB_ID_DEFAULT if not czjilu_entitlement_active(order, cid): continue hid = normalize_huiyuan_id(order.huiyuan_id) if not hid: continue if huiyuan_ids_match(hid, req): return True if is_bundle_huiyuan(hid): included = resolve_bundle_included_ids( hid, club_id=club_id, yonghu_id=yonghu_id, record_club_id=getattr(order, 'club_id', None), ) if req_set & _included_huiyuan_id_set(included): 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 leixing_id: 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:原样返回抢单要求类型 1/2 + 对应 huiyuan_id(不改写成 0)。 体验会员与正式会员同一套 clumber;老端用 clumber.huiyuanid 对订单 huiyuan_id。 """ 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) return 1, membership_id or '', has_valid def enrich_clumber_item(record, club_id=None): """老端 clumber:体验会员与正式会员返回格式完全一致(不暴露 is_trial)。""" 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['bundle_huiyuan_id'] = record.huiyuan_id item['included_huiyuan_ids'] = resolve_bundle_included_ids( record.huiyuan_id, club_id=club_id, yonghu_id=record.yonghu_id, record_club_id=record.club_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, base_item): """总会员有效时,为抢单页展开子会员虚拟条目(体验=正式)。""" if not base_item.get('is_bundle'): return bundle_hid = str( base_item.get('bundle_huiyuan_id') or base_item.get('huiyuanid') or base_item.get('huiyuan_id') or '', ) 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, club_id=None): """huiyuangoumai 未过期会员 → clumber(体验与正式完全同等,不读 is_trial)。""" items = [] seen = set() effective_club = _resolve_user_club_id(yonghu_id, club_id) for record in Huiyuangoumai.query.filter(yonghu_id=yonghu_id): if not _membership_record_active(record): continue base = enrich_clumber_item(record, club_id=effective_club) _append_clumber_id_variants(items, seen, base, record.huiyuan_id) _append_bundle_sub_clumber(items, seen, base) return items def _merge_clumber_extra_items(items, extra): """只读合并:有效期内充值单合成的会员条目补进 clumber(体验=正式)。""" if not extra: return items 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 in seen_cores: continue items.append(it) seen_cores.add(core) return items def _build_clumber_from_paid_czjilu(yonghu_id, club_id=None): """只读:有效期内充值单合成 clumber(体验=正式,不写库);总会员展开子类型。""" from jituan.constants import CLUB_ID_DEFAULT from jituan.services.member_recharge import ( MEMBER_LEIXING, MEMBER_PAID_STATUS, czjilu_entitlement_active, _czjilu_entitlement_end, ) from products.models import Czjilu, Huiyuan items = [] seen = set() effective_club = _resolve_user_club_id(yonghu_id, club_id) 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 effective_club or CLUB_ID_DEFAULT if not czjilu_entitlement_active(order, cid): continue try: hy = Huiyuan.query.get(huiyuan_id=hid) ming = hy.jieshao or f'会员{hid}' except Huiyuan.DoesNotExist: ming = f'会员{hid}' daoqi = _czjilu_entitlement_end(order, cid) base = { 'huiyuanming': ming, 'daoqi': format_legacy_daoqi(daoqi), 'huiyuan_zhuangtai': 1, 'shifou_daoqi': False, } if is_bundle_huiyuan(hid): base['is_bundle'] = True base['bundle_huiyuan_id'] = hid base['included_huiyuan_ids'] = resolve_bundle_included_ids( hid, club_id=club_id, yonghu_id=yonghu_id, record_club_id=getattr(order, 'club_id', None), ) _append_clumber_id_variants(items, seen, base, hid) _append_bundle_sub_clumber(items, seen, base) return items def _finalize_clumber_for_legacy_client(items): """老端只认 huiyuanid;去掉 is_trial / is_gift 等内部字段。""" cleaned = [] for it in items or []: row = dict(it) row.pop('is_trial', None) row.pop('is_gift', None) cleaned.append(row) return cleaned def ensure_grab_pool_clumber(yonghu_id, club_id=None): """ dddhq / dashouxinxi:只读 huiyuangoumai + 有效期内充值单 + 赠送会员 生成 clumber。 总会员展开子会员虚拟条目供老端 clumber 比对;不写库、不补开通。 """ from jituan.services.gift_huiyuan import build_gift_clumber_items effective_club = _resolve_user_club_id(yonghu_id, club_id) items = _build_user_clumber_core(yonghu_id, club_id=effective_club) items = _merge_clumber_extra_items( items, _build_clumber_from_paid_czjilu(yonghu_id, club_id=effective_club), ) items = _merge_clumber_extra_items( items, build_gift_clumber_items(yonghu_id, club_id=effective_club), ) items = _finalize_clumber_for_legacy_client(items) return bool(items), items def refresh_grab_pool_membership_state(yonghu_id, club_id=None): """ 抢单池/打手资料:只读生成 clumber(含总会员子类型展开)。 供 /dingdan/ddhq、/yonghu/dashouxinxi、登录 clumber 调用;禁止写库补会员。 """ return ensure_grab_pool_clumber(yonghu_id, club_id=club_id) 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, club_id=None): """登录 clumber:与 dddhq / dashouxinxi 同源,每次刷新会员状态。""" return refresh_grab_pool_membership_state(yonghu_id, club_id=club_id)[1] 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