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(
|
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:
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user