Files
Django/orders/services/today_orders.py
T

340 lines
11 KiB
Python

"""今日订单:真单统计 + 可选假数据混合(独立于抢单池假单)。"""
from __future__ import annotations
import logging
import random
from datetime import datetime, time, timedelta
from decimal import Decimal
from zoneinfo import ZoneInfo
from django.conf import settings
from django.db.models import Count, Q
from django.utils import timezone
from orders.models import Order, TodayOrderFake, TodayOrderFakeBatchState, TodayOrderShowConfig
logger = logging.getLogger(__name__)
TZ = ZoneInfo('Asia/Shanghai')
# 进行中:已下单/待抢、进行中、指定中
DOING_STATUSES = {1, 2, 7}
# 不计入当日有效单:未支付、支付失败、已废弃
EXCLUDE_STATUSES = {9, 14, 25}
def _today_range():
now = timezone.now().astimezone(TZ)
start = datetime.combine(now.date(), time.min, tzinfo=TZ)
end = start + timedelta(days=1)
return start, end
def _abs_media(path: str) -> str:
p = (path or '').strip()
if not p:
return ''
if p.startswith('http://') or p.startswith('https://'):
return p
base = (getattr(settings, 'COS_DOMAIN', '') or '').rstrip('/')
return f'{base}/{p.lstrip("/")}' if base else p
def get_or_create_config(club_id: str) -> TodayOrderShowConfig:
cid = (club_id or '').strip()
row = TodayOrderShowConfig.query.filter(club_id=cid).first()
if row:
return row
row = TodayOrderShowConfig(club_id=cid, mix_enabled=False, batch_size=5, interval_seconds=120)
row.save()
return row
def config_to_dict(row: TodayOrderShowConfig | None) -> dict:
if not row:
return {
'club_id': '',
'mix_enabled': False,
'batch_size': 5,
'interval_seconds': 120,
}
return {
'club_id': row.club_id,
'mix_enabled': bool(row.mix_enabled),
'batch_size': int(row.batch_size or 5),
'interval_seconds': int(row.interval_seconds or 120),
}
def save_config(club_id: str, data: dict) -> dict:
row = get_or_create_config(club_id)
if 'mix_enabled' in data:
row.mix_enabled = bool(data.get('mix_enabled'))
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.save()
return config_to_dict(row)
def _leixing_icon_map(club_id: str, leixing_ids: set[int]) -> dict[int, str]:
if not leixing_ids:
return {}
out = {}
try:
from jituan.services.club_shangpin_leixing import build_club_leixing_list_for_api
for row in build_club_leixing_list_for_api(club_id):
lid = int(row.get('id') or 0)
if lid in leixing_ids:
out[lid] = _abs_media(row.get('tupian_url') or '')
except Exception as e:
logger.warning('leixing icon map failed: %s', e)
if len(out) < len(leixing_ids):
try:
from products.models import ShangpinLeixing
for lx in ShangpinLeixing.query.filter(id__in=list(leixing_ids)):
if lx.id not in out:
out[int(lx.id)] = _abs_media(lx.tupian_url or '')
except Exception as e:
logger.warning('fallback leixing icons failed: %s', e)
return out
def _real_qs(club_id: str):
start, end = _today_range()
return (
Order.query.filter(ClubID=club_id, CreateTime__gte=start, CreateTime__lt=end)
.exclude(Status__in=list(EXCLUDE_STATUSES))
)
def _pick_fake_batch(club_id: str, cfg: TodayOrderShowConfig) -> list[TodayOrderFake]:
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 []
batch_n = min(int(cfg.batch_size), len(pool))
interval = max(10, int(cfg.interval_seconds or 120))
now = timezone.now()
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):
elapsed = (now - state.last_shuffle_at).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]
if not state:
state = TodayOrderFakeBatchState(club_id=club_id)
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]
def _serialize_real(o: Order, icon_map: dict[int, str]) -> dict:
lid = int(o.ProductTypeID or 0) if o.ProductTypeID else 0
img = _abs_media(o.ImageURL or '')
if not img and lid:
img = icon_map.get(lid) or ''
st = int(o.Status or 0)
tab = 'doing' if st in DOING_STATUSES else 'done'
platform = int(o.Platform or 0)
source = 'pingtai' if platform == 1 else ('shangjia' if platform == 2 else 'pingtai')
return {
'id': f'r:{o.OrderID}',
'is_fake': False,
'order_id': o.OrderID,
'source': source,
'platform': platform,
'tab': tab,
'status': st,
'price': str(o.Amount if o.Amount is not None else '0.00'),
'jieshao': (o.Description or o.Remark or '').strip() or '订单',
'image_url': img,
'leixing_id': lid or None,
}
def _serialize_fake(f: TodayOrderFake, icon_map: dict[int, str]) -> 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:
img = icon_map.get(lid) or ''
platform = int(f.platform or 1)
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}',
'is_fake': True,
'order_id': f'FAKE-{f.id}',
'source': source,
'platform': platform,
'tab': tab,
'status': int(f.status or 1),
'price': str(f.price if f.price is not None else '0.00'),
'jieshao': (f.jieshao or '').strip() or '订单',
'image_url': img,
'leixing_id': lid or None,
}
def _merged_items(club_id: str) -> list[dict]:
cfg = get_or_create_config(club_id)
reals = list(_real_qs(club_id).order_by('-CreateTime')[:800])
fakes = _pick_fake_batch(club_id, cfg)
lids = set()
for o in reals:
if o.ProductTypeID:
lids.add(int(o.ProductTypeID))
for f in fakes:
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
def _filter_items(items: list[dict], source: str, tab: str) -> list[dict]:
src = (source or 'all').strip().lower()
tb = (tab or 'doing').strip().lower()
out = items
if src in ('pingtai', 'shangjia'):
out = [x for x in out if x.get('source') == src]
if tb in ('doing', 'done'):
out = [x for x in out if x.get('tab') == tb]
return out
def build_summary(items: list[dict], source: str = 'all') -> 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')
return {
'total': len(scoped),
'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)}
def today_order_feed(
club_id: str,
source: str = 'all',
tab: str = 'doing',
page: int = 1,
limit: int = 15,
) -> dict:
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)
filtered = _filter_items(items, source=source, tab=tab)
start = (page - 1) * limit
end = start + limit
chunk = filtered[start:end]
return {
'summary': summary,
'list': chunk,
'more': end < len(filtered),
'page': page,
'limit': limit,
}
def fake_to_admin_dict(row: TodayOrderFake) -> dict:
return {
'id': row.id,
'club_id': row.club_id,
'platform': int(row.platform or 1),
'leixing_id': row.leixing_id,
'jieshao': row.jieshao or '',
'price': str(row.price or '0.00'),
'image_url': row.image_url or '',
'image_full_url': _abs_media(row.image_url or ''),
'status': int(row.status or 1),
'is_active': bool(row.is_active),
'sort_order': int(row.sort_order or 0),
}
def list_fakes(club_id: str) -> list[dict]:
rows = TodayOrderFake.query.filter(club_id=club_id).order_by('-sort_order', '-id')
return [fake_to_admin_dict(r) for r in rows]
def upsert_fake(club_id: str, data: dict, fake_id=None) -> tuple[dict | None, str]:
cid = (club_id or '').strip()
if not cid:
return None, '缺少俱乐部'
platform = int(data.get('platform') or 1)
if platform not in (1, 2):
return None, 'platform 须为 1 或 2'
status = int(data.get('status') or TodayOrderFake.STATUS_DOING)
if status not in (TodayOrderFake.STATUS_DOING, TodayOrderFake.STATUS_DONE):
return None, 'status 仅支持进行中/已完成'
try:
price = Decimal(str(data.get('price') if data.get('price') is not None else '0'))
except Exception:
return None, '价格无效'
leixing_id = data.get('leixing_id')
try:
leixing_id = int(leixing_id) if leixing_id not in (None, '') else None
except (TypeError, ValueError):
return None, '商品类型无效'
if fake_id:
row = TodayOrderFake.query.filter(id=fake_id, club_id=cid).first()
if not row:
return None, '假单不存在'
else:
row = TodayOrderFake(club_id=cid)
row.platform = platform
row.leixing_id = leixing_id
row.jieshao = str(data.get('jieshao') or '')[:2000]
row.price = price
if 'image_url' in data:
row.image_url = str(data.get('image_url') or '').strip()[:500]
row.status = status
if 'is_active' in data:
raw = data.get('is_active')
if isinstance(raw, str):
row.is_active = raw.strip().lower() in ('1', 'true', 'yes', 'on')
else:
row.is_active = bool(raw)
if 'sort_order' in data and data.get('sort_order') is not None:
row.sort_order = int(data.get('sort_order') or 0)
row.save()
return fake_to_admin_dict(row), ''
def delete_fake(club_id: str, fake_id: int) -> tuple[bool, str]:
row = TodayOrderFake.query.filter(id=fake_id, club_id=club_id).first()
if not row:
return False, '假单不存在'
row.delete()
return True, ''