diff --git a/orders/models.py b/orders/models.py index 17c1101..6b2d1cb 100644 --- a/orders/models.py +++ b/orders/models.py @@ -1291,13 +1291,13 @@ class TodayOrderFake(QModel): class TodayOrderShowConfig(QModel): - """俱乐部「今日订单」真假混合与随机批规则。""" + """俱乐部「今日订单」真假混合:按间隔累加假单数(不整批重抽)。""" id = models.BigAutoField(primary_key=True) club_id = models.CharField(max_length=16, unique=True, db_index=True) mix_enabled = models.BooleanField(default=False, verbose_name='真假混合') - batch_size = models.PositiveIntegerField(default=5, verbose_name='每批随机假单数') - interval_seconds = models.PositiveIntegerField(default=120, verbose_name='换批间隔秒') + batch_size = models.PositiveIntegerField(default=5, verbose_name='每间隔累加假单数') + interval_seconds = models.PositiveIntegerField(default=120, verbose_name='累加间隔秒') CreateTime = models.DateTimeField(auto_now_add=True) UpdateTime = models.DateTimeField(auto_now=True) @@ -1306,12 +1306,16 @@ class TodayOrderShowConfig(QModel): class TodayOrderFakeBatchState(QModel): - """当前展示中的假单批次(按俱乐部)。""" + """ + 当日假单累加状态(按俱乐部一行,不记历史日): + - fake_ids:已展示假单主键(只增不换) + - last_shuffle_at:上次累加时间(跨日则重置) + """ id = models.BigAutoField(primary_key=True) club_id = models.CharField(max_length=16, unique=True, db_index=True) - last_shuffle_at = models.DateTimeField(null=True, blank=True) - fake_ids = models.JSONField(default=list, blank=True, verbose_name='当前批次假单主键列表') + last_shuffle_at = models.DateTimeField(null=True, blank=True, verbose_name='上次累加时间') + fake_ids = models.JSONField(default=list, blank=True, verbose_name='已展示假单主键列表') UpdateTime = models.DateTimeField(auto_now=True) class Meta: diff --git a/orders/services/today_orders.py b/orders/services/today_orders.py index 5e2ddc6..061b6f5 100644 --- a/orders/services/today_orders.py +++ b/orders/services/today_orders.py @@ -7,7 +7,7 @@ from datetime import datetime, time, timedelta from decimal import Decimal from django.conf import settings -from django.db import IntegrityError +from django.db import IntegrityError, transaction from django.db.models import Count, Q from django.utils import timezone @@ -120,7 +120,43 @@ def _real_qs(club_id: str): ) +def _parse_fake_ids(raw) -> list[int]: + if not isinstance(raw, list): + return [] + out = [] + for x in raw: + try: + if isinstance(x, bool): + continue + out.append(int(x)) + except (TypeError, ValueError): + continue + return out + + +def _ensure_batch_state(club_id: str) -> TodayOrderFakeBatchState: + state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first() + if state: + return state + state = TodayOrderFakeBatchState(club_id=club_id, fake_ids=[], last_shuffle_at=None) + try: + state.save() + except IntegrityError: + state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first() + if not state: + raise + return state + + def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrderFake]: + """ + 假单累加展示(惰性,仅有用户请求时推进): + - 当天首次:抽出 batch_size 条,写入状态 + - 之后每隔 interval_seconds,在原列表上再追加 batch_size(已展示的不变) + - 间隔未到:原样返回,刷新不重抽 + - 无用户刷新:时间到了也不自动变 + - 跨自然日:清空累加,重新从首批开始 + """ if not cfg.mix_enabled or int(cfg.batch_size or 0) <= 0: return [] pool = list( @@ -128,39 +164,83 @@ def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrder ) if not pool: return [] - batch_n = min(int(cfg.batch_size), len(pool)) + + batch_n = max(1, int(cfg.batch_size)) interval = max(10, int(cfg.interval_seconds or 120)) now = _now_naive() - state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first() - need_reshuffle = True - ids = [] - if state and state.last_shuffle_at and isinstance(state.fake_ids, list): - last = state.last_shuffle_at - if timezone.is_aware(last): - last = timezone.make_naive(last) - elapsed = (now - last).total_seconds() - if elapsed < interval and state.fake_ids: - ids = [int(x) for x in state.fake_ids if str(x).isdigit() or isinstance(x, int)] - need_reshuffle = False - if need_reshuffle: - chosen = random.sample(pool, batch_n) - ids = [int(x.id) for x in chosen] + today = now.date() + pool_by_id = {int(x.id): x for x in pool} + + with transaction.atomic(): + # 行锁,避免并发刷新重复累加 + state = ( + TodayOrderFakeBatchState.objects + .select_for_update() + .filter(club_id=club_id) + .first() + ) if not state: - state = TodayOrderFakeBatchState(club_id=club_id) - state.last_shuffle_at = now - state.fake_ids = ids - try: - state.save() - except IntegrityError: - # 并发首次创建同一 club 批次行:改更新已有行 - state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first() - if state: - state.last_shuffle_at = now - state.fake_ids = ids - state.save() - return chosen - id_set = set(ids) - return [x for x in pool if int(x.id) in id_set] + state = _ensure_batch_state(club_id) + state = ( + TodayOrderFakeBatchState.objects + .select_for_update() + .filter(club_id=club_id) + .first() + ) + if not state: + return [] + + last = state.last_shuffle_at + if last and timezone.is_aware(last): + last = timezone.make_naive(last) + + ids = _parse_fake_ids(state.fake_ids) + # 跨日重置(不记历史,只保留「当前时段」) + if last is None or last.date() != today: + ids = [] + last = None + + # 池子里已删的 id 丢掉,保持展示稳定且可继续追加 + ids = [i for i in ids if i in pool_by_id] + id_set = set(ids) + + add_batches = 0 + if not ids: + add_batches = 1 + elif last is not None: + elapsed = (now - last).total_seconds() + if elapsed >= interval: + # 离开期间跨过多档则一次补上(仍只在有人刷新时发生) + add_batches = int(elapsed // interval) + + if add_batches > 0: + available = [x for x in pool if int(x.id) not in id_set] + for _ in range(add_batches): + if not available: + break + n = min(batch_n, len(available)) + picked = random.sample(available, n) + for row in picked: + rid = int(row.id) + ids.append(rid) + id_set.add(rid) + available = [x for x in available if int(x.id) not in id_set] + + if last is None: + new_last = now + else: + new_last = last + timedelta(seconds=add_batches * interval) + if new_last > now: + new_last = now + state.fake_ids = ids + state.last_shuffle_at = new_last + state.save(update_fields=['fake_ids', 'last_shuffle_at', 'UpdateTime']) + elif ids != _parse_fake_ids(state.fake_ids): + # 仅清理失效 id + state.fake_ids = ids + state.save(update_fields=['fake_ids', 'UpdateTime']) + + return [pool_by_id[i] for i in ids if i in pool_by_id] def _serialize_real(o: Order, icon_map: dict[int, str]) -> dict: @@ -253,7 +333,7 @@ def build_summary(items: list[dict], source: str = 'all') -> dict: def today_order_count(club_id: str) -> dict: - """抢单池角标:当日总量(真单 + 当前假批次),不分平台。""" + """抢单池角标:当日总量(真单 + 当前已累加假单),不分平台。""" cid = (club_id or '').strip() if not cid: return {'total': 0}