fix: 假单打乱顺序持久化到OrderSequence,2分钟内全用户一致

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-28 18:33:36 +08:00
parent 872dbedb6e
commit 22f1781ea2
3 changed files with 106 additions and 32 deletions

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0012_fakegrabordershufflestate'),
]
operations = [
migrations.AddField(
model_name='fakegrabordershufflestate',
name='OrderSequence',
field=models.JSONField(
blank=True,
default=dict,
help_text='形如 {"xq|1": {"last_shuffle_time": "...", "order_ids": ["JD...", ...]}}',
verbose_name='各俱乐部+类型已打乱顺序',
),
),
]

View File

@@ -1106,6 +1106,11 @@ class FakeGrabOrderShuffleState(QModel):
IntervalSeconds = models.PositiveIntegerField( IntervalSeconds = models.PositiveIntegerField(
default=120, verbose_name='打乱间隔秒数默认2分钟', default=120, verbose_name='打乱间隔秒数默认2分钟',
) )
OrderSequence = models.JSONField(
default=dict, blank=True,
verbose_name='各俱乐部+类型已打乱顺序',
help_text='形如 {"xq|1": {"last_shuffle_time": "...", "order_ids": ["JD...", ...]}}',
)
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta: class Meta:

View File

@@ -6,6 +6,7 @@ from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from django.utils.dateparse import parse_datetime
from rest_framework.response import Response from rest_framework.response import Response
from backend.utils import fmt_datetime, pick_leixing_id from backend.utils import fmt_datetime, pick_leixing_id
@@ -41,53 +42,105 @@ def dashou_should_use_fake_pool(user):
return True return True
def _random_display_time_str(): def _scope_key(club_id, leixing_id):
"""返回给前端的创建时间:当前请求时刻 2~5 分钟前的随机时间。""" lid = leixing_id if leixing_id is not None else 'all'
seconds_ago = random.randint(120, 300) return f'{club_id}|{lid}'
dt = timezone.now() - timedelta(seconds=seconds_ago)
return fmt_datetime(dt)
def _resolve_shuffle_epoch(): def _parse_shuffle_time(value):
if not value:
return None
if isinstance(value, str):
dt = parse_datetime(value)
if dt and timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone.get_current_timezone())
return dt
return value
def _display_time_str_for_order(order_id, anchor_dt):
"""同一假单在打乱窗口内展示时间固定。"""
seed = abs(hash(str(order_id))) % (2 ** 32)
seconds_ago = 120 + (seed % 181)
base = anchor_dt or timezone.now()
return fmt_datetime(base - timedelta(seconds=seconds_ago))
def _load_fake_rows_in_saved_order(club_id, leixing_id):
""" """
全用户共用 id=1 一条记录 读取/更新 id=1 打乱状态表
- 满 IntervalSeconds默认120秒更新 LastShuffleTime - 满 IntervalSeconds:按库中 order_ids 顺序返回(全用户一致)
- 满:不更新,沿用上次时刻 - 满:重新打乱并写回 order_ids
同一窗口内所有用户用相同 seed刷新不会变顺序。 - 新增假单:追加到顺序末尾并写回;已删假单:自动跳过
返回 (rows, anchor_dt) 供展示时间固定。
""" """
qs = FakeGrabOrder.query.filter(ClubID=club_id)
if leixing_id is not None:
qs = qs.filter(ProductTypeID=leixing_id)
all_rows = list(qs)
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() now = timezone.now()
with transaction.atomic(): with transaction.atomic():
state, created = FakeGrabOrderShuffleState.objects.select_for_update().get_or_create( state, created = FakeGrabOrderShuffleState.objects.select_for_update().get_or_create(
pk=SHUFFLE_STATE_PK, pk=SHUFFLE_STATE_PK,
defaults={ defaults={
'LastShuffleTime': now - timedelta(days=1), 'LastShuffleTime': now - timedelta(days=1),
'IntervalSeconds': 120, 'IntervalSeconds': 120,
'OrderSequence': {},
}, },
) )
interval = max(1, int(state.IntervalSeconds or 120)) interval = max(1, int(state.IntervalSeconds or 120))
elapsed = (now - state.LastShuffleTime).total_seconds() seq_map = dict(state.OrderSequence or {})
if created or elapsed >= interval: 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 = state.LastShuffleTime
need_reshuffle = (
not stored_ids
or last_dt is None
or (now - last_dt).total_seconds() >= 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.LastShuffleTime = now
state.save(update_fields=['LastShuffleTime', 'UpdateTime']) state.save(update_fields=['OrderSequence', 'LastShuffleTime', 'UpdateTime'])
return int(state.LastShuffleTime.timestamp()) 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 now
ordered = [id_to_row[oid] for oid in stored_ids if oid in id_to_row]
return ordered, anchor_dt
def _order_fake_rows(rows, shuffle_epoch): def _format_fake_row(row, anchor_dt=None):
"""按全局 epoch 固定打乱2 分钟内所有人顺序一致。"""
base = sorted(list(rows), key=lambda r: (r.id or 0, r.OrderID or ''))
rng = random.Random(shuffle_epoch)
rng.shuffle(base)
return base
def _format_fake_row(row, display_time=None):
"""字段与 DashouDingdanHuoquView 真实单列表保持一致。""" """字段与 DashouDingdanHuoquView 真实单列表保持一致。"""
# 平台单用 tupian、商家单用 tupian + sj_avatar统一默认头像
avatar = DEFAULT_DISPATCHER_AVATAR avatar = DEFAULT_DISPATCHER_AVATAR
platform = int(row.Platform or 1) platform = int(row.Platform or 1)
oid = row.OrderID oid = row.OrderID
price = float(row.Amount or 0) price = float(row.Amount or 0)
ct = display_time or _random_display_time_str() ct = _display_time_str_for_order(oid, anchor_dt)
zid = (row.DispatcherUserID or '').strip() zid = (row.DispatcherUserID or '').strip()
sj_name = (getattr(row, 'MerchantNickname', None) or '').strip() sj_name = (getattr(row, 'MerchantNickname', None) or '').strip()
@@ -127,18 +180,13 @@ def build_fake_order_pool_response(request):
club_id = resolve_effective_club_id(request, getattr(request, 'user', None)) club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
qs = FakeGrabOrder.query.filter(ClubID=club_id) rows, anchor_dt = _load_fake_rows_in_saved_order(club_id, leixing_id)
if leixing_id is not None:
qs = qs.filter(ProductTypeID=leixing_id)
shuffle_epoch = _resolve_shuffle_epoch()
rows = _order_fake_rows(qs, shuffle_epoch)
total = len(rows) total = len(rows)
offset = (page - 1) * page_size offset = (page - 1) * page_size
page_rows = rows[offset:offset + page_size] page_rows = rows[offset:offset + page_size]
formatted_list = [_format_fake_row(r) for r in page_rows] formatted_list = [_format_fake_row(r, anchor_dt) for r in page_rows]
has_more = (page * page_size) < total has_more = (page * page_size) < total
return Response({ return Response({