diff --git a/orders/migrations/0013_fakegrabordershufflestate_ordersequence.py b/orders/migrations/0013_fakegrabordershufflestate_ordersequence.py new file mode 100644 index 0000000..3fed808 --- /dev/null +++ b/orders/migrations/0013_fakegrabordershufflestate_ordersequence.py @@ -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='各俱乐部+类型已打乱顺序', + ), + ), + ] diff --git a/orders/models.py b/orders/models.py index 4455ada..18ada1d 100644 --- a/orders/models.py +++ b/orders/models.py @@ -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: diff --git a/orders/services/fake_order_pool.py b/orders/services/fake_order_pool.py index e3bc7ed..968727f 100644 --- a/orders/services/fake_order_pool.py +++ b/orders/services/fake_order_pool.py @@ -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({