fix: 今日订单假单按间隔累加,刷新不再整批重抽
This commit is contained in:
1 parent
4c7f273cf7
commit
9f6618969b
2 files changed
+122
-38
No files matched your search
+10
-6
@@ -1291,13 +1291,13 @@ class TodayOrderFake(QModel):
|
|||||||
|
|
||||||
|
|
||||||
class TodayOrderShowConfig(QModel):
|
class TodayOrderShowConfig(QModel):
|
||||||
"""俱乐部「今日订单」真假混合与随机批规则。"""
|
"""俱乐部「今日订单」真假混合:按间隔累加假单数(不整批重抽)。"""
|
||||||
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
id = models.BigAutoField(primary_key=True)
|
||||||
club_id = models.CharField(max_length=16, unique=True, db_index=True)
|
club_id = models.CharField(max_length=16, unique=True, db_index=True)
|
||||||
mix_enabled = models.BooleanField(default=False, verbose_name='真假混合')
|
mix_enabled = models.BooleanField(default=False, verbose_name='真假混合')
|
||||||
batch_size = models.PositiveIntegerField(default=5, verbose_name='每批随机假单数')
|
batch_size = models.PositiveIntegerField(default=5, verbose_name='每间隔累加假单数')
|
||||||
interval_seconds = models.PositiveIntegerField(default=120, verbose_name='换批间隔秒')
|
interval_seconds = models.PositiveIntegerField(default=120, verbose_name='累加间隔秒')
|
||||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
UpdateTime = models.DateTimeField(auto_now=True)
|
UpdateTime = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
@@ -1306,12 +1306,16 @@ class TodayOrderShowConfig(QModel):
|
|||||||
|
|
||||||
|
|
||||||
class TodayOrderFakeBatchState(QModel):
|
class TodayOrderFakeBatchState(QModel):
|
||||||
"""当前展示中的假单批次(按俱乐部)。"""
|
"""
|
||||||
|
当日假单累加状态(按俱乐部一行,不记历史日):
|
||||||
|
- fake_ids:已展示假单主键(只增不换)
|
||||||
|
- last_shuffle_at:上次累加时间(跨日则重置)
|
||||||
|
"""
|
||||||
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
id = models.BigAutoField(primary_key=True)
|
||||||
club_id = models.CharField(max_length=16, unique=True, db_index=True)
|
club_id = models.CharField(max_length=16, unique=True, db_index=True)
|
||||||
last_shuffle_at = models.DateTimeField(null=True, blank=True)
|
last_shuffle_at = models.DateTimeField(null=True, blank=True, verbose_name='上次累加时间')
|
||||||
fake_ids = models.JSONField(default=list, blank=True, verbose_name='当前批次假单主键列表')
|
fake_ids = models.JSONField(default=list, blank=True, verbose_name='已展示假单主键列表')
|
||||||
UpdateTime = models.DateTimeField(auto_now=True)
|
UpdateTime = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
+112
-32
@@ -7,7 +7,7 @@ from datetime import datetime, time, timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
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.db.models import Count, Q
|
||||||
from django.utils import timezone
|
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]:
|
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:
|
if not cfg.mix_enabled or int(cfg.batch_size or 0) <= 0:
|
||||||
return []
|
return []
|
||||||
pool = list(
|
pool = list(
|
||||||
@@ -128,39 +164,83 @@ def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrder
|
|||||||
)
|
)
|
||||||
if not pool:
|
if not pool:
|
||||||
return []
|
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))
|
interval = max(10, int(cfg.interval_seconds or 120))
|
||||||
now = _now_naive()
|
now = _now_naive()
|
||||||
state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first()
|
today = now.date()
|
||||||
need_reshuffle = True
|
pool_by_id = {int(x.id): x for x in pool}
|
||||||
ids = []
|
|
||||||
if state and state.last_shuffle_at and isinstance(state.fake_ids, list):
|
with transaction.atomic():
|
||||||
last = state.last_shuffle_at
|
# 行锁,避免并发刷新重复累加
|
||||||
if timezone.is_aware(last):
|
state = (
|
||||||
last = timezone.make_naive(last)
|
TodayOrderFakeBatchState.objects
|
||||||
elapsed = (now - last).total_seconds()
|
.select_for_update()
|
||||||
if elapsed < interval and state.fake_ids:
|
.filter(club_id=club_id)
|
||||||
ids = [int(x) for x in state.fake_ids if str(x).isdigit() or isinstance(x, int)]
|
.first()
|
||||||
need_reshuffle = False
|
)
|
||||||
if need_reshuffle:
|
|
||||||
chosen = random.sample(pool, batch_n)
|
|
||||||
ids = [int(x.id) for x in chosen]
|
|
||||||
if not state:
|
if not state:
|
||||||
state = TodayOrderFakeBatchState(club_id=club_id)
|
state = _ensure_batch_state(club_id)
|
||||||
state.last_shuffle_at = now
|
state = (
|
||||||
state.fake_ids = ids
|
TodayOrderFakeBatchState.objects
|
||||||
try:
|
.select_for_update()
|
||||||
state.save()
|
.filter(club_id=club_id)
|
||||||
except IntegrityError:
|
.first()
|
||||||
# 并发首次创建同一 club 批次行:改更新已有行
|
)
|
||||||
state = TodayOrderFakeBatchState.query.filter(club_id=club_id).first()
|
if not state:
|
||||||
if state:
|
return []
|
||||||
state.last_shuffle_at = now
|
|
||||||
state.fake_ids = ids
|
last = state.last_shuffle_at
|
||||||
state.save()
|
if last and timezone.is_aware(last):
|
||||||
return chosen
|
last = timezone.make_naive(last)
|
||||||
id_set = set(ids)
|
|
||||||
return [x for x in pool if int(x.id) in id_set]
|
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:
|
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:
|
def today_order_count(club_id: str) -> dict:
|
||||||
"""抢单池角标:当日总量(真单 + 当前假批次),不分平台。"""
|
"""抢单池角标:当日总量(真单 + 当前已累加假单),不分平台。"""
|
||||||
cid = (club_id or '').strip()
|
cid = (club_id or '').strip()
|
||||||
if not cid:
|
if not cid:
|
||||||
return {'total': 0}
|
return {'total': 0}
|
||||||
|
|||||||
Reference in new issue
Block a user