fix: 假单打乱顺序持久化到OrderSequence,2分钟内全用户一致
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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='各俱乐部+类型已打乱顺序',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user