fix: 假单列表去掉共享打乱写库,避免刷新时空单

This commit is contained in:
XingQue
2026-07-31 10:17:54 +08:00
parent 62f121f438
commit 3ba3536b05
2 changed files with 60 additions and 106 deletions

View File

@@ -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

View File

@@ -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)