"""抢单大厅假单:按俱乐部开关混排/过滤。""" import hashlib import logging import random import time from datetime import datetime from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from rest_framework.response import Response from backend.utils import fmt_datetime, pick_leixing_id from jituan.services.club_context import resolve_effective_club_id from jituan.services.fake_grab_switch import ( get_fake_yajin_hide_threshold, is_club_fake_grab_enabled, ) from jituan.services.huiyuan_bundle import normalize_huiyuan_id, user_has_huiyuan_access from orders.models import FakeGrabOrder logger = logging.getLogger(__name__) DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg' # 展示顺序每 N 秒换一桶;同桶内全用户/全 worker 一致(不写库,无并发覆盖) DEFAULT_SHUFFLE_INTERVAL = 120 MODE_OFF = 'off' MODE_FAKE_ONLY = 'fake_only' MODE_MIX = 'mix' MODE_REAL_ONLY = 'real_only' def _leixing_membership_meta(leixing_id): """返回 (required_huiyuan_id, yaoqiuleixing)。""" if leixing_id is None: return '', None try: from products.models import ShangpinLeixing row = ShangpinLeixing.query.filter(id=int(leixing_id)).values( 'huiyuan_id', 'yaoqiuleixing', ).first() if not row: return '', None return ( normalize_huiyuan_id(row.get('huiyuan_id')), row.get('yaoqiuleixing'), ) except Exception: logger.exception('read ShangpinLeixing membership meta failed leixing_id=%s', leixing_id) return '', None def _user_has_type_membership(user, club_id, leixing_id): required, _yaoqiu = _leixing_membership_meta(leixing_id) if not required: return False try: return bool(user_has_huiyuan_access(user.UserUID, required, club_id=club_id)) except Exception: logger.exception( 'user_has_huiyuan_access failed uid=%s required=%s club=%s', getattr(user, 'UserUID', ''), required, club_id, ) return False def resolve_fake_pool_mode(user, club_id=None, leixing_id=None): """ - off:俱乐部未开 → 只真单 - fake_only:封禁 → 只假单 - real_only:有对应会员,或押金≥门槛 → 过滤假单 - mix:未充对应会员且押金未达门槛 → 真单+假单 """ cid = (club_id or '').strip() if not cid: return MODE_OFF try: enabled = is_club_fake_grab_enabled(cid) except Exception: logger.exception('is_club_fake_grab_enabled failed club=%s', cid) return MODE_OFF if not enabled: return MODE_OFF try: dashou = user.DashouProfile except ObjectDoesNotExist: return MODE_FAKE_ONLY except Exception: logger.exception('read DashouProfile failed uid=%s', getattr(user, 'UserUID', '')) return MODE_FAKE_ONLY if int(getattr(dashou, 'zhanghaozhuangtai', 0) or 0) != 1: return MODE_FAKE_ONLY if leixing_id is not None and _user_has_type_membership(user, cid, leixing_id): return MODE_REAL_ONLY try: yajin = Decimal(str(getattr(dashou, 'yajin', 0) or 0)) except Exception: yajin = Decimal('0') if yajin >= get_fake_yajin_hide_threshold(cid): return MODE_REAL_ONLY return MODE_MIX def dashou_should_use_fake_pool(user, club_id=None, leixing_id=None): """兼容旧调用:是否整页只假单。""" return resolve_fake_pool_mode(user, club_id=club_id, leixing_id=leixing_id) == MODE_FAKE_ONLY def list_formatted_fake_orders(club_id, leixing_id): rows, anchor_dt = _load_fake_rows_stable_order(club_id, leixing_id) return [_format_fake_row(r, anchor_dt) for r in rows] def _stable_int_seed(*parts): raw = '|'.join('' if p is None else str(p) for p in parts) return int(hashlib.md5(raw.encode('utf-8')).hexdigest()[:8], 16) def _shuffle_interval_seconds(): try: from orders.models import FakeGrabOrderShuffleState row = FakeGrabOrderShuffleState.objects.filter(pk=1).only('IntervalSeconds').first() if row and row.IntervalSeconds: return max(1, int(row.IntervalSeconds)) except Exception: pass return DEFAULT_SHUFFLE_INTERVAL def _display_time_str_for_order(order_id, anchor_dt): """同一假单在打乱窗口内展示时间固定(md5,不依赖 PYTHONHASHSEED)。""" seed = _stable_int_seed(order_id) seconds_ago = 120 + (seed % 181) if anchor_dt is None: base_ts = time.time() elif timezone.is_aware(anchor_dt): base_ts = anchor_dt.timestamp() else: try: base_ts = timezone.make_aware(anchor_dt, timezone.get_current_timezone()).timestamp() except Exception: base_ts = time.time() display_dt = datetime.fromtimestamp( base_ts - seconds_ago, tz=timezone.get_current_timezone(), ) return fmt_datetime(display_dt) def _load_fake_rows_stable_order(club_id, leixing_id): """ 按俱乐部+类型加载假单,用「时间桶 + md5」稳定打乱: - 同桶内所有进程/用户顺序一致 - 不读写共享 JSON,避免并发刷新把列表写空/写丢 """ cid = (club_id or '').strip() qs = FakeGrabOrder.query.filter(ClubID=cid) if leixing_id is not None: try: qs = qs.filter(ProductTypeID=int(leixing_id)) except (TypeError, ValueError): qs = qs.filter(ProductTypeID=leixing_id) # 先稳定取出,再按桶打乱(避免 DB 顺序漂移) all_rows = list(qs.order_by('-SortOrder', 'OrderID')) id_to_row = {r.OrderID: r for r in all_rows if r.OrderID} all_ids = list(id_to_row.keys()) interval = _shuffle_interval_seconds() bucket = int(time.time()) // interval rng = random.Random(_stable_int_seed(cid, leixing_id, bucket)) shuffled_ids = list(all_ids) rng.shuffle(shuffled_ids) anchor_dt = datetime.fromtimestamp( bucket * interval, tz=timezone.get_current_timezone(), ) ordered = [id_to_row[oid] for oid in shuffled_ids if oid in id_to_row] if len(ordered) != len(all_ids): logger.warning( 'fake pool order mismatch club=%s leixing=%s ids=%s ordered=%s', cid, leixing_id, len(all_ids), len(ordered), ) return ordered, anchor_dt def _format_fake_row(row, anchor_dt=None): """字段与 DashouDingdanHuoquView 真实单列表保持一致。""" avatar = (getattr(row, 'DispatcherAvatar', None) or '').strip() or DEFAULT_DISPATCHER_AVATAR platform = int(row.Platform or 1) oid = row.OrderID price = float(row.Amount or 0) ct = _display_time_str_for_order(oid, anchor_dt) zid = (row.DispatcherUserID or '').strip() sj_name = (getattr(row, 'MerchantNickname', None) or '').strip() return { 'dingdan_id': oid, 'zhuangtai': 1, 'pingtai': platform, 'dashou_fencheng': price, 'leixing_id': row.ProductTypeID or 0, 'tupian': avatar, 'yaoqiuleixing': 0, 'huiyuan_id': '', # 假单始终展示价格(前端 hasRequiredMember=true 才显示金额) 'hasRequiredMember': True, 'has_required_member': True, 'is_fake': True, 'yajin': 0.0, 'jieshao': row.Description or '', 'beizhu': row.Remark or '', 'creat_time': ct, 'create_time': ct, 'sjnicheng': sj_name if platform == 2 else '', 'sj_avatar': avatar if platform == 2 else '', 'zhiding_uid': zid, 'zhiding_avatar': avatar, 'zhiding_nicheng': '', 'xuqiu_biaoqian': [], 'dashou_biaoqian': [], 'shangjia_biaoqian': [], 'shangjia_identity_biaoqian': [], 'zhiding_identity_biaoqian': [], 'shangjia_youzhi': bool(row.IsPremiumMerchant) if platform == 2 else False, } def build_fake_order_pool_response(request): data = request.data leixing_id = pick_leixing_id(data) page = max(1, int(data.get('page', 1))) page_size = max(1, min(int(data.get('page_size', 10)), 50)) club_id = resolve_effective_club_id(request, getattr(request, 'user', None)) rows, anchor_dt = _load_fake_rows_stable_order(club_id, leixing_id) total = len(rows) offset = (page - 1) * page_size page_rows = rows[offset:offset + page_size] formatted_list = [_format_fake_row(r, anchor_dt) for r in page_rows] has_more = (page * page_size) < total return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted_list, 'has_more': has_more, 'total': total, 'current_page': page, 'page_size': page_size, 'total_pages': (total + page_size - 1) // page_size if page_size else 0, 'type_pending_counts': {}, 'pool_pending_total': 0, 'team_pending_count': 0, 'is_fake_pool': True, }, })