From 3ba3536b05b68e2c913d23859f4cbca40f8ad536 Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 31 Jul 2026 10:17:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=81=87=E5=8D=95=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=85=B1=E4=BA=AB=E6=89=93=E4=B9=B1=E5=86=99?= =?UTF-8?q?=E5=BA=93=EF=BC=8C=E9=81=BF=E5=85=8D=E5=88=B7=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E7=A9=BA=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orders/services/fake_order_pool.py | 162 ++++++++++------------------- orders/views/dashou.py | 4 + 2 files changed, 60 insertions(+), 106 deletions(-) diff --git a/orders/services/fake_order_pool.py b/orders/services/fake_order_pool.py index 4dffecb..91c40c6 100644 --- a/orders/services/fake_order_pool.py +++ b/orders/services/fake_order_pool.py @@ -1,13 +1,13 @@ """抢单大厅假单:按俱乐部开关混排/过滤。""" +import hashlib import logging import random -from datetime import datetime, timedelta +import time +from datetime import datetime from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction from django.utils import timezone -from django.utils.dateparse import parse_datetime from rest_framework.response import Response from backend.utils import fmt_datetime, pick_leixing_id @@ -17,12 +17,13 @@ from jituan.services.fake_grab_switch import ( is_club_fake_grab_enabled, ) from jituan.services.huiyuan_bundle import normalize_huiyuan_id, user_has_huiyuan_access -from orders.models import FakeGrabOrder, FakeGrabOrderShuffleState +from orders.models import FakeGrabOrder logger = logging.getLogger(__name__) DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg' -SHUFFLE_STATE_PK = 1 +# 展示顺序每 N 秒换一桶;同桶内全用户/全 worker 一致(不写库,无并发覆盖) +DEFAULT_SHUFFLE_INTERVAL = 120 MODE_OFF = 'off' MODE_FAKE_ONLY = 'fake_only' @@ -113,58 +114,39 @@ def dashou_should_use_fake_pool(user, club_id=None, leixing_id=None): def list_formatted_fake_orders(club_id, leixing_id): - rows, anchor_dt = _load_fake_rows_in_saved_order(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 _scope_key(club_id, leixing_id): - lid = leixing_id if leixing_id is not None else 'all' - return f'{club_id}|{lid}' - -def _ensure_aware(dt): - if dt is None: - return None - if timezone.is_naive(dt): - return timezone.make_aware(dt, timezone.get_current_timezone()) - return dt +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 _to_unix_ts(dt): - """统一转为 Unix 时间戳,避免 naive/aware 直接相减报错。""" - if dt is None: - return None - if timezone.is_aware(dt): - return dt.timestamp() +def _shuffle_interval_seconds(): try: - return timezone.make_aware(dt, timezone.get_current_timezone()).timestamp() + 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: - return dt.timestamp() - - -def _elapsed_seconds(since_dt, until_dt): - since_ts = _to_unix_ts(since_dt) - until_ts = _to_unix_ts(until_dt) - if since_ts is None or until_ts is None: - return None - return until_ts - since_ts - - -def _parse_shuffle_time(value): - if not value: - return None - if isinstance(value, str): - dt = parse_datetime(value) - return _ensure_aware(dt) - return _ensure_aware(value) + pass + return DEFAULT_SHUFFLE_INTERVAL def _display_time_str_for_order(order_id, anchor_dt): - """同一假单在打乱窗口内展示时间固定。""" - seed = abs(hash(str(order_id))) % (2 ** 32) + """同一假单在打乱窗口内展示时间固定(md5,不依赖 PYTHONHASHSEED)。""" + seed = _stable_int_seed(order_id) seconds_ago = 120 + (seed % 181) - base_ts = _to_unix_ts(_ensure_aware(anchor_dt) or timezone.now()) - if base_ts is None: - base_ts = _to_unix_ts(timezone.now()) or 0 + 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(), @@ -172,73 +154,41 @@ def _display_time_str_for_order(order_id, anchor_dt): return fmt_datetime(display_dt) -def _load_fake_rows_in_saved_order(club_id, leixing_id): +def _load_fake_rows_stable_order(club_id, leixing_id): """ - 读取/更新 id=1 打乱状态表: - - 未满 IntervalSeconds:按库中 order_ids 顺序返回(全用户一致) - - 已满:重新打乱并写回 order_ids - - 新增假单:追加到顺序末尾并写回;已删假单:自动跳过 - 返回 (rows, anchor_dt) 供展示时间固定。 + 按俱乐部+类型加载假单,用「时间桶 + md5」稳定打乱: + - 同桶内所有进程/用户顺序一致 + - 不读写共享 JSON,避免并发刷新把列表写空/写丢 """ - qs = FakeGrabOrder.query.filter(ClubID=club_id) + cid = (club_id or '').strip() + qs = FakeGrabOrder.query.filter(ClubID=cid) if leixing_id is not None: - qs = qs.filter(ProductTypeID=leixing_id) + try: + qs = qs.filter(ProductTypeID=int(leixing_id)) + except (TypeError, ValueError): + qs = qs.filter(ProductTypeID=leixing_id) - all_rows = list(qs) + # 先稳定取出,再按桶打乱(避免 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()) - scope = _scope_key(club_id, leixing_id) - now = timezone.now() - with transaction.atomic(): - state, created = FakeGrabOrderShuffleState.objects.select_for_update().get_or_create( - pk=SHUFFLE_STATE_PK, - defaults={ - 'LastShuffleTime': now - timedelta(days=1), - 'IntervalSeconds': 120, - 'OrderSequence': {}, - }, + 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), ) - interval = max(1, int(state.IntervalSeconds or 120)) - seq_map = dict(state.OrderSequence or {}) - entry = dict(seq_map.get(scope) or {}) - stored_ids = list(entry.get('order_ids') or []) - last_dt = _parse_shuffle_time(entry.get('last_shuffle_time')) - if not last_dt and not created: - last_dt = _ensure_aware(state.LastShuffleTime) - - elapsed = _elapsed_seconds(last_dt, now) - need_reshuffle = ( - not stored_ids - or last_dt is None - or elapsed is None - or elapsed >= interval - ) - - if need_reshuffle: - stored_ids = list(all_ids) - random.shuffle(stored_ids) - entry = { - 'last_shuffle_time': now.isoformat(), - 'order_ids': stored_ids, - } - seq_map[scope] = entry - state.OrderSequence = seq_map - state.LastShuffleTime = now - state.save(update_fields=['OrderSequence', 'LastShuffleTime', 'UpdateTime']) - else: - valid_ids = [oid for oid in stored_ids if oid in id_to_row] - new_ids = [oid for oid in all_ids if oid not in set(valid_ids)] - if len(valid_ids) != len(stored_ids) or new_ids: - valid_ids.extend(new_ids) - entry['order_ids'] = valid_ids - seq_map[scope] = entry - state.OrderSequence = seq_map - state.save(update_fields=['OrderSequence', 'UpdateTime']) - stored_ids = valid_ids - - anchor_dt = _parse_shuffle_time(entry.get('last_shuffle_time')) or _ensure_aware(now) - ordered = [id_to_row[oid] for oid in stored_ids if oid in id_to_row] return ordered, anchor_dt @@ -292,7 +242,7 @@ def build_fake_order_pool_response(request): club_id = resolve_effective_club_id(request, getattr(request, 'user', None)) - rows, anchor_dt = _load_fake_rows_in_saved_order(club_id, leixing_id) + rows, anchor_dt = _load_fake_rows_stable_order(club_id, leixing_id) total = len(rows) offset = (page - 1) * page_size diff --git a/orders/views/dashou.py b/orders/views/dashou.py index 0d449ea..8c20f9e 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -123,6 +123,10 @@ class DashouDingdanHuoquView(APIView): fake_mode = resolve_fake_pool_mode( request.user, club_id=club_id, leixing_id=leixing_id, ) + logger.info( + 'ddhq fake_mode=%s club=%s leixing=%s uid=%s', + fake_mode, club_id, leixing_id, getattr(request.user, 'UserUID', ''), + ) if fake_mode == MODE_FAKE_ONLY: return build_fake_order_pool_response(request)