Files
Django/orders/services/today_orders.py
T

348 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""今日订单:真单统计 + 可选假数据混合(独立于抢单池假单)。"""
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.models import Count, Q
from django.utils import timezone
from orders.models import Order, TodayOrderFake, TodayOrderFakeBatchState, TodayOrderShowConfig
logger = logging.getLogger(__name__)
# 进行中:已下单/待抢、进行中、指定中
DOING_STATUSES = {1, 2, 7}
# 不计入当日有效单:未支付、支付失败、已废弃
EXCLUDE_STATUSES = {9, 14, 25}
def _now_naive():
"""项目 USE_TZ=False,MySQL 只接受 naive datetime。"""
now = timezone.now()
if timezone.is_aware(now):
return timezone.make_naive(now)
return now
def _today_range():
now = _now_naive()
start = datetime.combine(now.date(), time.min)
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 = _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]
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, ''