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(
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='更新时间')
class Meta:

View File

@@ -6,6 +6,7 @@ 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
@@ -41,53 +42,105 @@ def dashou_should_use_fake_pool(user):
return True
def _random_display_time_str():
"""返回给前端的创建时间:当前请求时刻 2~5 分钟前的随机时间。"""
seconds_ago = random.randint(120, 300)
dt = timezone.now() - timedelta(seconds=seconds_ago)
return fmt_datetime(dt)
def _scope_key(club_id, leixing_id):
lid = leixing_id if leixing_id is not None else 'all'
return f'{club_id}|{lid}'
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 一条记录
- 满 IntervalSeconds默认120秒更新 LastShuffleTime
- 满:不更新,沿用上次时刻
同一窗口内所有用户用相同 seed刷新不会变顺序。
读取/更新 id=1 打乱状态表
- 满 IntervalSeconds:按库中 order_ids 顺序返回(全用户一致)
- 满:重新打乱并写回 order_ids
- 新增假单:追加到顺序末尾并写回;已删假单:自动跳过
返回 (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()
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 = max(1, int(state.IntervalSeconds or 120))
elapsed = (now - state.LastShuffleTime).total_seconds()
if created or elapsed >= interval:
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 = 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.save(update_fields=['LastShuffleTime', 'UpdateTime'])
return int(state.LastShuffleTime.timestamp())
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 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):
"""按全局 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):
def _format_fake_row(row, anchor_dt=None):
"""字段与 DashouDingdanHuoquView 真实单列表保持一致。"""
# 平台单用 tupian、商家单用 tupian + sj_avatar统一默认头像
avatar = DEFAULT_DISPATCHER_AVATAR
platform = int(row.Platform or 1)
oid = row.OrderID
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()
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))
qs = FakeGrabOrder.query.filter(ClubID=club_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)
rows, anchor_dt = _load_fake_rows_in_saved_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) for r in page_rows]
formatted_list = [_format_fake_row(r, anchor_dt) for r in page_rows]
has_more = (page * page_size) < total
return Response({