fix: 今日订单假单按 show_count 间隔累加,角标只增不卡死
This commit is contained in:
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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user