fix: 今日订单假单按间隔累加,刷新不再整批重抽

This commit is contained in:
XingQue committed 2026-08-11 21:27:38 +08:00
1 parent 4c7f273cf7
commit 9f6618969b
2 files changed
+122 -38

No files matched your search

+10 -6
View File
@@ -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:
+112 -32
View File
@@ -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}