From 5c7bb82a7bfec1600751d048e1e74d8200575f37 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sun, 28 Jun 2026 18:22:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=81=87=E5=8D=95=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=85=A8=E5=B1=802=E5=88=86=E9=92=9F=E6=89=93=E4=B9=B1?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=EF=BC=8C=E5=85=A8=E7=94=A8=E6=88=B7=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../0012_fakegrabordershufflestate.py | 23 +++++++++ orders/models.py | 18 +++++++ orders/services/fake_order_pool.py | 51 +++++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 orders/migrations/0012_fakegrabordershufflestate.py diff --git a/orders/migrations/0012_fakegrabordershufflestate.py b/orders/migrations/0012_fakegrabordershufflestate.py new file mode 100644 index 0000000..0444c55 --- /dev/null +++ b/orders/migrations/0012_fakegrabordershufflestate.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0011_fakegraborder_merchantnickname'), + ] + + operations = [ + migrations.CreateModel( + name='FakeGrabOrderShuffleState', + fields=[ + ('id', models.PositiveSmallIntegerField(default=1, primary_key=True, serialize=False, verbose_name='固定主键=1')), + ('LastShuffleTime', models.DateTimeField(verbose_name='上次打乱时刻')), + ('IntervalSeconds', models.PositiveIntegerField(default=120, verbose_name='打乱间隔秒数(默认2分钟)')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'fake_grab_order_shuffle', + }, + ), + ] diff --git a/orders/models.py b/orders/models.py index 632e426..4455ada 100644 --- a/orders/models.py +++ b/orders/models.py @@ -1094,6 +1094,24 @@ class FakeGrabOrder(QModel): ] +class FakeGrabOrderShuffleState(QModel): + """假单列表全局打乱时刻(仅 id=1 一条记录,全用户共用)。""" + + id = models.PositiveSmallIntegerField( + primary_key=True, default=1, verbose_name='固定主键=1', + ) + LastShuffleTime = models.DateTimeField( + verbose_name='上次打乱时刻', + ) + IntervalSeconds = models.PositiveIntegerField( + default=120, verbose_name='打乱间隔秒数(默认2分钟)', + ) + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fake_grab_order_shuffle' + + # =========================================================================== # 兼容别名(渐进迁移,全部迁移完成后可删除) # =========================================================================== diff --git a/orders/services/fake_order_pool.py b/orders/services/fake_order_pool.py index f2d24e3..364d771 100644 --- a/orders/services/fake_order_pool.py +++ b/orders/services/fake_order_pool.py @@ -4,15 +4,17 @@ from datetime import timedelta from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction from django.utils import timezone from rest_framework.response import Response from backend.utils import fmt_datetime, pick_leixing_id from jituan.services.club_context import resolve_effective_club_id from jituan.services.dashou_exam import user_has_active_member -from orders.models import FakeGrabOrder +from orders.models import FakeGrabOrder, FakeGrabOrderShuffleState DEFAULT_DISPATCHER_AVATAR = 'beijing/morentouxiang.jpg' +SHUFFLE_STATE_PK = 1 def dashou_should_use_fake_pool(user): @@ -46,6 +48,49 @@ def _random_display_time_str(): return fmt_datetime(dt) +def _resolve_shuffle_epoch(): + """ + 全用户共用一条打乱状态(id=1): + - 距上次打乱 >= IntervalSeconds:更新 LastShuffleTime,进入新打乱窗口 + - 否则:沿用上次打乱时刻,本窗口内顺序不变 + 返回用于固定顺序的 epoch 时间戳;若应使用库表顺序则返回 None。 + """ + 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, + }, + ) + interval = max(1, int(state.IntervalSeconds or 120)) + elapsed = (now - state.LastShuffleTime).total_seconds() + if created or elapsed >= interval: + state.LastShuffleTime = now + state.save(update_fields=['LastShuffleTime', 'UpdateTime']) + return int(state.LastShuffleTime.timestamp()) + return None + + +def _order_fake_rows(rows, shuffle_epoch): + """shuffle_epoch 有值:按全局 epoch 固定打乱;None:按库表 SortOrder/CreateTime。""" + items = list(rows) + if shuffle_epoch is None: + items.sort( + key=lambda r: ( + -(r.SortOrder or 0), + r.CreateTime or timezone.datetime.min.replace(tzinfo=timezone.utc), + r.id or 0, + ), + ) + return items + base = sorted(items, 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 真实单列表保持一致。""" # 平台单用 tupian、商家单用 tupian + sj_avatar,统一默认头像 @@ -97,8 +142,8 @@ def build_fake_order_pool_response(request): if leixing_id is not None: qs = qs.filter(ProductTypeID=leixing_id) - rows = list(qs) - random.shuffle(rows) + shuffle_epoch = _resolve_shuffle_epoch() + rows = _order_fake_rows(qs, shuffle_epoch) total = len(rows) offset = (page - 1) * page_size