fix: 假单列表去掉共享打乱写库,避免刷新时空单
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user