fix: 今日订单假单按 show_count 间隔累加,角标只增不卡死

This commit is contained in:
XingQue committed 2026-08-11 21:33:22 +08:00
1 parent 9f6618969b
commit 98c7b0886b
3 files changed
+112 -67

No files matched your search

@@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0022_today_order_fake'),
]
operations = [
migrations.AddField(
model_name='todayorderfakebatchstate',
name='show_count',
field=models.PositiveIntegerField(default=0, verbose_name='当前展示假单数量'),
),
migrations.AlterField(
model_name='todayorderfakebatchstate',
name='fake_ids',
field=models.JSONField(blank=True, default=list, verbose_name='已展示假单主键列表(兼容)'),
),
migrations.AlterField(
model_name='todayorderfakebatchstate',
name='last_shuffle_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='上次累加时间'),
),
]
+4 -2
View File
@@ -1308,14 +1308,16 @@ class TodayOrderShowConfig(QModel):
class TodayOrderFakeBatchState(QModel):
"""
当日假单累加状态(按俱乐部一行,不记历史日):
- fake_ids:已展示假单主键(只增不换)
- show_count:当前对外展示的假单数量(只增不减,按间隔累加)
- last_shuffle_at:上次累加时间(跨日则重置)
- fake_ids:兼容旧字段,可空
"""
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, verbose_name='上次累加时间')
fake_ids = models.JSONField(default=list, blank=True, verbose_name='已展示假单主键列表')
show_count = models.PositiveIntegerField(default=0, verbose_name='当前展示假单数量')
fake_ids = models.JSONField(default=list, blank=True, verbose_name='已展示假单主键列表(兼容)')
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
+82 -65
View File
@@ -2,13 +2,12 @@
from __future__ import annotations
import logging
import random
from datetime import datetime, time, timedelta
from decimal import Decimal
from django.conf import settings
from django.db import IntegrityError, transaction
from django.db.models import Count, Q
from django.db.models import Q
from django.utils import timezone
from orders.models import Order, TodayOrderFake, TodayOrderFakeBatchState, TodayOrderShowConfig
@@ -20,6 +19,10 @@ DOING_STATUSES = {1, 2, 7}
# 不计入当日有效单:未支付、支付失败、已废弃
EXCLUDE_STATUSES = {9, 14, 25}
# 明细列表假单上限(角标仍用完整 show_count)
_FAKE_LIST_CAP = 3000
_FAKE_COUNT_CAP = 20000
def _now_naive():
"""项目 USE_TZ=False,MySQL 只接受 naive datetime。"""
@@ -84,7 +87,7 @@ def save_config(club_id: str, data: dict) -> dict:
if 'batch_size' in data and data.get('batch_size') is not None:
row.batch_size = max(0, min(500, int(data.get('batch_size') or 0)))
if 'interval_seconds' in data and data.get('interval_seconds') is not None:
row.interval_seconds = max(10, min(86400, int(data.get('interval_seconds') or 120)))
row.interval_seconds = max(1, min(86400, int(data.get('interval_seconds') or 120)))
row.save()
return config_to_dict(row)
@@ -138,7 +141,9 @@ 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)
state = TodayOrderFakeBatchState(
club_id=club_id, fake_ids=[], show_count=0, last_shuffle_at=None,
)
try:
state.save()
except IntegrityError:
@@ -148,31 +153,27 @@ def _ensure_batch_state(club_id: str) -> TodayOrderFakeBatchState:
return state
def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrderFake]:
def _tick_fake_show_count(club_id: str, cfg: TodayOrderShowConfig) -> int:
"""
假单累加展示(惰性,仅有用户请求时推进):
- 当天首次:抽出 batch_size 条,写入状态
- 之后每隔 interval_seconds,在原列表上再追加 batch_size(已展示的不变)
- 间隔未到:原样返回,刷新不重抽
- 无用户刷新:时间到了也不自动变
- 跨自然日:清空累加,重新从首批开始
惰性累加「假单展示数量」(抢单池「今日」角标用这个):
- 当天首次有人请求:show_count = batch_size(如 35)
- 之后每过 interval 秒再有人请求:show_count += batch_size(526→561→596)
- 未到间隔:数字不变
- 无人请求:时间到了也不涨
- 跨自然日:清零重计
数量与假单池「行数」脱钩;明细从池子循环填充。
"""
if not cfg.mix_enabled or int(cfg.batch_size or 0) <= 0:
return []
pool = list(
TodayOrderFake.query.filter(club_id=club_id, is_active=True).order_by('-sort_order', '-id')
)
if not pool:
return []
return 0
if not TodayOrderFake.query.filter(club_id=club_id, is_active=True).exists():
return 0
batch_n = max(1, int(cfg.batch_size))
interval = max(10, int(cfg.interval_seconds or 120))
interval = max(1, int(cfg.interval_seconds or 120))
now = _now_naive()
today = now.date()
pool_by_id = {int(x.id): x for x in pool}
with transaction.atomic():
# 行锁,避免并发刷新重复累加
state = (
TodayOrderFakeBatchState.objects
.select_for_update()
@@ -180,7 +181,7 @@ def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrder
.first()
)
if not state:
state = _ensure_batch_state(club_id)
_ensure_batch_state(club_id)
state = (
TodayOrderFakeBatchState.objects
.select_for_update()
@@ -188,59 +189,60 @@ def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrder
.first()
)
if not state:
return []
return 0
last = state.last_shuffle_at
if last and timezone.is_aware(last):
last = timezone.make_naive(last)
ids = _parse_fake_ids(state.fake_ids)
# 跨日重置(不记历史,只保留「当前时段」)
show_count = int(getattr(state, 'show_count', 0) or 0)
# 兼容旧逻辑:只有 fake_ids
if show_count <= 0:
legacy_ids = _parse_fake_ids(state.fake_ids)
if legacy_ids:
show_count = len(legacy_ids)
if last is None or last.date() != today:
ids = []
show_count = 0
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:
if show_count <= 0:
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]
show_count = min(show_count + batch_n * add_batches, _FAKE_COUNT_CAP)
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.show_count = show_count
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'])
state.save(update_fields=['show_count', 'last_shuffle_at', 'UpdateTime'])
elif int(getattr(state, 'show_count', 0) or 0) != show_count:
state.show_count = show_count
state.save(update_fields=['show_count', 'UpdateTime'])
return [pool_by_id[i] for i in ids if i in pool_by_id]
return int(state.show_count or 0)
def _load_fake_display_rows(club_id: str, show_count: int) -> list[tuple[TodayOrderFake, int]]:
"""按 show_count 从假单池循环取行。"""
if show_count <= 0:
return []
pool = list(
TodayOrderFake.query.filter(club_id=club_id, is_active=True).order_by('-sort_order', '-id')
)
if not pool:
return []
n = min(int(show_count), _FAKE_LIST_CAP)
return [(pool[i % len(pool)], i) for i in range(n)]
def _serialize_real(o: Order, icon_map: dict[int, str]) -> dict:
@@ -267,7 +269,7 @@ def _serialize_real(o: Order, icon_map: dict[int, str]) -> dict:
}
def _serialize_fake(f: TodayOrderFake, icon_map: dict[int, str]) -> dict:
def _serialize_fake(f: TodayOrderFake, icon_map: dict[int, str], seq: int = 0) -> dict:
lid = int(f.leixing_id or 0) if f.leixing_id else 0
img = _abs_media(f.image_url or '')
if not img and lid:
@@ -276,9 +278,9 @@ def _serialize_fake(f: TodayOrderFake, icon_map: dict[int, str]) -> dict:
source = 'pingtai' if platform == 1 else 'shangjia'
tab = 'doing' if int(f.status or 1) == TodayOrderFake.STATUS_DOING else 'done'
return {
'id': f'f:{f.id}',
'id': f'f:{f.id}:{seq}',
'is_fake': True,
'order_id': f'FAKE-{f.id}',
'order_id': f'FAKE-{f.id}-{seq}',
'source': source,
'platform': platform,
'tab': tab,
@@ -290,21 +292,23 @@ def _serialize_fake(f: TodayOrderFake, icon_map: dict[int, str]) -> dict:
}
def _merged_items(club_id: str) -> list[dict]:
def _merged_items(club_id: str) -> tuple[list[dict], int, int]:
"""返回 (明细items, 真单数, 假单展示数 show_count)。"""
cfg = get_or_create_config(club_id)
reals = list(_real_qs(club_id).order_by('-CreateTime')[:800])
fakes = _pick_fake_batch(club_id, cfg)
show_count = _tick_fake_show_count(club_id, cfg)
fake_rows = _load_fake_display_rows(club_id, show_count)
lids = set()
for o in reals:
if o.ProductTypeID:
lids.add(int(o.ProductTypeID))
for f in fakes:
for f, _seq in fake_rows:
if f.leixing_id:
lids.add(int(f.leixing_id))
icon_map = _leixing_icon_map(club_id, lids)
items = [_serialize_real(o, icon_map) for o in reals]
items.extend(_serialize_fake(f, icon_map) for f in fakes)
return items
items.extend(_serialize_fake(f, icon_map, seq=seq) for f, seq in fake_rows)
return items, len(reals), int(show_count)
def _filter_items(items: list[dict], source: str, tab: str) -> list[dict]:
@@ -318,27 +322,38 @@ def _filter_items(items: list[dict], source: str, tab: str) -> list[dict]:
return out
def build_summary(items: list[dict], source: str = 'all') -> dict:
def build_summary(items: list[dict], source: str = 'all', *, total_override: int | None = None) -> dict:
scoped = items
src = (source or 'all').strip().lower()
if src in ('pingtai', 'shangjia'):
scoped = [x for x in items if x.get('source') == src]
doing = sum(1 for x in scoped if x.get('tab') == 'doing')
done = sum(1 for x in scoped if x.get('tab') == 'done')
total = int(total_override) if total_override is not None and src == 'all' else len(scoped)
# source=all 且明细截断时,用 override 保证角标一致;doing/done 按已生成明细比例不够精确时仍用明细
if total_override is not None and src == 'all' and total > len(scoped):
# 把差额算进 done,避免 total != doing+done
done = max(done, total - doing)
return {
'total': len(scoped),
'total': total,
'doing': doing,
'done': done,
}
def today_order_count(club_id: str) -> dict:
"""抢单池角标:当日总量(真单 + 当前已累加假单),不分平台。"""
"""抢单池角标:真单数 + 累加假单数(按间隔只增)。"""
cid = (club_id or '').strip()
if not cid:
return {'total': 0}
items = _merged_items(cid)
return {'total': len(items)}
cfg = get_or_create_config(cid)
real_n = _real_qs(cid).count()
fake_n = _tick_fake_show_count(cid, cfg)
return {
'total': int(real_n) + int(fake_n),
'real': int(real_n),
'fake': int(fake_n),
}
def today_order_feed(
@@ -351,8 +366,10 @@ def today_order_feed(
cid = (club_id or '').strip()
page = max(1, int(page or 1))
limit = max(1, min(50, int(limit or 15)))
items = _merged_items(cid)
summary = build_summary(items, source=source)
items, real_n, fake_n = _merged_items(cid)
summary = build_summary(
items, source=source, total_override=int(real_n) + int(fake_n),
)
filtered = _filter_items(items, source=source, tab=tab)
start = (page - 1) * limit
end = start + limit