feat: 今日订单真假混合统计与假数据接口
This commit is contained in:
1 parent
7ff18fc827
commit
6b028738f1
8 files changed
+643
-2
No files matched your search
@@ -79,6 +79,11 @@ from jituan.views_fake_grab_order import (
|
||||
FakeGrabOrderListView,
|
||||
FakeGrabOrderManageView,
|
||||
)
|
||||
from jituan.views_today_order import (
|
||||
TodayOrderConfigView,
|
||||
TodayOrderFakeListView,
|
||||
TodayOrderFakeManageView,
|
||||
)
|
||||
from jituan.views_club_catalog import (
|
||||
ClubKaoheChenghaoCatalogView,
|
||||
ClubKaoheChenghaoDisableView,
|
||||
@@ -218,6 +223,9 @@ urlpatterns = [
|
||||
path('houtai/fake-grab-order-list', FakeGrabOrderListView.as_view(), name='jituan_fake_grab_order_list'),
|
||||
path('houtai/fake-grab-order-manage', FakeGrabOrderManageView.as_view(), name='jituan_fake_grab_order_manage'),
|
||||
path('houtai/fake-grab-order-avatar', FakeGrabOrderAvatarUploadView.as_view(), name='jituan_fake_grab_order_avatar'),
|
||||
path('houtai/today-order-config', TodayOrderConfigView.as_view(), name='jituan_today_order_config'),
|
||||
path('houtai/today-order-fake-list', TodayOrderFakeListView.as_view(), name='jituan_today_order_fake_list'),
|
||||
path('houtai/today-order-fake-manage', TodayOrderFakeManageView.as_view(), name='jituan_today_order_fake_manage'),
|
||||
path('houtai/club-leixing-list', ClubShangpinLeixingListView.as_view(), name='jituan_club_leixing_list'),
|
||||
path('houtai/club-leixing-catalog', ClubShangpinLeixingCatalogView.as_view(), name='jituan_club_leixing_catalog'),
|
||||
path('houtai/club-leixing-enable', ClubShangpinLeixingEnableView.as_view(), name='jituan_club_leixing_enable'),
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""今日订单假数据 / 真假混合配置(客服后台)。"""
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from backend.view import verify_kefu_permission
|
||||
from jituan.services.club_context import club_id_for_write, resolve_club_scope
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
|
||||
PRODUCT_PERMS = ('2200a', '8080a')
|
||||
|
||||
|
||||
def _auth(request):
|
||||
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||||
if not phone:
|
||||
return None, Response({'code': 401, 'msg': '缺少客服账号'})
|
||||
kefu, permissions = verify_kefu_permission(request, phone)
|
||||
if kefu is None:
|
||||
return None, permissions
|
||||
if not any(p in permissions for p in PRODUCT_PERMS):
|
||||
return None, Response({'code': 403, 'msg': '无权限'})
|
||||
return kefu, None
|
||||
|
||||
|
||||
class TodayOrderConfigView(APIView):
|
||||
"""POST /jituan/houtai/today-order-config action=get|save"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
_, err = _auth(request)
|
||||
if err:
|
||||
return err
|
||||
if resolve_club_scope(request) == DATA_SCOPE_ALL:
|
||||
return Response({'code': 400, 'msg': '请切换到具体俱乐部'})
|
||||
club_id = club_id_for_write(request)
|
||||
from orders.services import today_orders as svc
|
||||
|
||||
action = (request.data.get('action') or 'get').strip().lower()
|
||||
if action == 'get':
|
||||
row = svc.get_or_create_config(club_id)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': svc.config_to_dict(row)})
|
||||
if action == 'save':
|
||||
data = svc.save_config(club_id, request.data)
|
||||
return Response({'code': 0, 'msg': '已保存', 'data': data})
|
||||
return Response({'code': 400, 'msg': '未知 action'})
|
||||
|
||||
|
||||
class TodayOrderFakeListView(APIView):
|
||||
"""POST /jituan/houtai/today-order-fake-list"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
_, err = _auth(request)
|
||||
if err:
|
||||
return err
|
||||
if resolve_club_scope(request) == DATA_SCOPE_ALL:
|
||||
return Response({'code': 400, 'msg': '请切换到具体俱乐部'})
|
||||
club_id = club_id_for_write(request)
|
||||
from orders.services import today_orders as svc
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': {'list': svc.list_fakes(club_id)}})
|
||||
|
||||
|
||||
class TodayOrderFakeManageView(APIView):
|
||||
"""POST /jituan/houtai/today-order-fake-manage action=create|update|delete"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser, MultiPartParser, FormParser]
|
||||
|
||||
def post(self, request):
|
||||
_, err = _auth(request)
|
||||
if err:
|
||||
return err
|
||||
if resolve_club_scope(request) == DATA_SCOPE_ALL:
|
||||
return Response({'code': 400, 'msg': '请切换到具体俱乐部'})
|
||||
club_id = club_id_for_write(request)
|
||||
from orders.services import today_orders as svc
|
||||
|
||||
action = (request.data.get('action') or '').strip().lower()
|
||||
if action == 'delete':
|
||||
ok, msg = svc.delete_fake(club_id, int(request.data.get('id') or 0))
|
||||
if not ok:
|
||||
return Response({'code': 400, 'msg': msg})
|
||||
return Response({'code': 0, 'msg': '已删除'})
|
||||
if action in ('create', 'update', 'add', 'save'):
|
||||
fake_id = request.data.get('id')
|
||||
if action == 'update' or fake_id:
|
||||
try:
|
||||
fake_id = int(fake_id)
|
||||
except (TypeError, ValueError):
|
||||
fake_id = None
|
||||
else:
|
||||
fake_id = None
|
||||
# 可选上传图片
|
||||
data = dict(request.data)
|
||||
file_obj = request.FILES.get('file') or request.FILES.get('image')
|
||||
if file_obj:
|
||||
try:
|
||||
import uuid
|
||||
from utils.oss_utils import upload_to_oss
|
||||
ext = '.' + (file_obj.name.rsplit('.', 1)[-1].lower() if '.' in file_obj.name else 'jpg')
|
||||
if ext not in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||
ext = '.jpg'
|
||||
rel = f'club/{club_id}/today_order_fake/{uuid.uuid4().hex}{ext}'
|
||||
if not upload_to_oss(file_obj, rel):
|
||||
return Response({'code': 400, 'msg': '上传失败'})
|
||||
data['image_url'] = rel
|
||||
except Exception as e:
|
||||
return Response({'code': 400, 'msg': f'上传失败: {e}'})
|
||||
row, msg = svc.upsert_fake(club_id, data, fake_id=fake_id)
|
||||
if not row:
|
||||
return Response({'code': 400, 'msg': msg or '保存失败'})
|
||||
return Response({'code': 0, 'msg': '已保存', 'data': row})
|
||||
return Response({'code': 400, 'msg': '未知 action'})
|
||||
@@ -0,0 +1,69 @@
|
||||
# Generated manually for today order fake / mix feature
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0021_partial_merchant_refund'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TodayOrderFake',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部')),
|
||||
('platform', models.PositiveSmallIntegerField(db_index=True, default=1, verbose_name='1平台2商家')),
|
||||
('leixing_id', models.IntegerField(blank=True, db_index=True, null=True, verbose_name='商品类型ID(图标回退)')),
|
||||
('jieshao', models.TextField(blank=True, default='', verbose_name='介绍')),
|
||||
('price', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='价格')),
|
||||
('image_url', models.CharField(blank=True, default='', max_length=500, verbose_name='图片相对URL')),
|
||||
('status', models.PositiveSmallIntegerField(db_index=True, default=1, verbose_name='1进行中2已完成')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True, verbose_name='是否启用')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'today_order_fake',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TodayOrderShowConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('club_id', models.CharField(db_index=True, max_length=16, unique=True)),
|
||||
('mix_enabled', models.BooleanField(default=False, verbose_name='真假混合')),
|
||||
('batch_size', models.PositiveIntegerField(default=5, verbose_name='每批随机假单数')),
|
||||
('interval_seconds', models.PositiveIntegerField(default=120, verbose_name='换批间隔秒')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'today_order_show_config',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TodayOrderFakeBatchState',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('club_id', models.CharField(db_index=True, max_length=16, unique=True)),
|
||||
('last_shuffle_at', models.DateTimeField(blank=True, null=True)),
|
||||
('fake_ids', models.JSONField(blank=True, default=list, verbose_name='当前批次假单主键列表')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'today_order_fake_batch',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='todayorderfake',
|
||||
index=models.Index(fields=['club_id', 'is_active', 'platform'], name='today_order_club_id_7a1b2c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='todayorderfake',
|
||||
index=models.Index(fields=['club_id', 'status'], name='today_order_club_id_8d3e4f_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1253,6 +1253,71 @@ class FakeGrabOrderShuffleState(QModel):
|
||||
db_table = 'fake_grab_order_shuffle'
|
||||
|
||||
|
||||
class TodayOrderFake(QModel):
|
||||
"""今日订单展示用假数据(与抢单池 FakeGrabOrder 独立,不影响老功能)。"""
|
||||
|
||||
STATUS_DOING = 1
|
||||
STATUS_DONE = 2
|
||||
PLATFORM_PINGTAI = 1
|
||||
PLATFORM_SHANGJIA = 2
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部')
|
||||
platform = models.PositiveSmallIntegerField(
|
||||
default=1, db_index=True, verbose_name='1平台2商家',
|
||||
)
|
||||
leixing_id = models.IntegerField(
|
||||
null=True, blank=True, db_index=True, verbose_name='商品类型ID(图标回退)',
|
||||
)
|
||||
jieshao = models.TextField(blank=True, default='', verbose_name='介绍')
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='价格')
|
||||
image_url = models.CharField(
|
||||
max_length=500, blank=True, default='', verbose_name='图片相对URL',
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
default=1, db_index=True, verbose_name='1进行中2已完成',
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True, verbose_name='是否启用')
|
||||
sort_order = models.IntegerField(default=0, verbose_name='排序')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'today_order_fake'
|
||||
indexes = [
|
||||
models.Index(fields=['club_id', 'is_active', 'platform']),
|
||||
models.Index(fields=['club_id', 'status']),
|
||||
]
|
||||
|
||||
|
||||
class TodayOrderShowConfig(QModel):
|
||||
"""俱乐部「今日订单」真假混合与随机批规则。"""
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
club_id = models.CharField(max_length=16, unique=True, db_index=True)
|
||||
mix_enabled = models.BooleanField(default=False, verbose_name='真假混合')
|
||||
batch_size = models.PositiveIntegerField(default=5, verbose_name='每批随机假单数')
|
||||
interval_seconds = models.PositiveIntegerField(default=120, verbose_name='换批间隔秒')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'today_order_show_config'
|
||||
|
||||
|
||||
class TodayOrderFakeBatchState(QModel):
|
||||
"""当前展示中的假单批次(按俱乐部)。"""
|
||||
|
||||
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)
|
||||
fake_ids = models.JSONField(default=list, blank=True, verbose_name='当前批次假单主键列表')
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'today_order_fake_batch'
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 组队(Team)— 新域,与 EscortOrder 无关
|
||||
# ===========================================================================
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
"""今日订单:真单统计 + 可选假数据混合(独立于抢单池假单)。"""
|
||||
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, ''
|
||||
+3
-1
@@ -11,7 +11,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al
|
||||
ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \
|
||||
ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaFakuanSpecListView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \
|
||||
LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \
|
||||
ZhiDingChaxunView, ZhiDingHuiFuView, H5ChatSendImageView
|
||||
ZhiDingChaxunView, ZhiDingHuiFuView, H5ChatSendImageView, TodayOrderCountView, TodayOrderFeedView
|
||||
from jituan.services.fubei_notify import FubeiPayNotifyView
|
||||
from jituan.views_order_deal import MiniappMyDealStatView
|
||||
|
||||
@@ -70,6 +70,8 @@ urlpatterns = [
|
||||
path('adddgh', AdGengHuanDaShou.as_view(), name='管理更换打手'),
|
||||
path('sjscpz', ShangjiaCOSZhengshuView.as_view(), name='商家上传证据图片获取密钥'),
|
||||
path('dsqdhqddlx', DashouHuoquLeixingView.as_view(), name='打手抢单页面获取订单类型'),
|
||||
path('today-order-count', TodayOrderCountView.as_view(), name='今日订单总量角标'),
|
||||
path('today-order-feed', TodayOrderFeedView.as_view(), name='今日订单明细'),
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ from .dashou import (
|
||||
DashouDingdanXiangqingView,
|
||||
DashouHuoquLeixingView,
|
||||
DashouXiugaiView,
|
||||
TodayOrderCountView,
|
||||
TodayOrderFeedView,
|
||||
)
|
||||
from .admin import (
|
||||
AdGetDingdanXiangQing,
|
||||
@@ -90,7 +92,7 @@ __all__ = [
|
||||
"DashouDingdanHuoquView", "ShangpinLeixingHuoquView", "QiangdanView",
|
||||
"DashouDingdanHuoquView1", "DashouCOSZhengshuView", "ShangjiaCOSZhengshuView",
|
||||
"DashouTijiaoView", "DashouDingdanXiangqingView", "DashouHuoquLeixingView",
|
||||
"DashouXiugaiView",
|
||||
"DashouXiugaiView", "TodayOrderCountView", "TodayOrderFeedView",
|
||||
"AdGetDingdanXiangQing", "AdGengHuanDaShou", "AdQiangZhiJieDan",
|
||||
"AdJuJueTuiKuan", "AdTongYiTuiKuanShangJia", "AdTongYiTuiKuanPingTai",
|
||||
"AdJuJueJieSuan", "AdZhuanYiDaTing", "AdQuXiaoZhiDing",
|
||||
|
||||
@@ -1921,5 +1921,46 @@ class DashouXiugaiView(APIView):
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class TodayOrderCountView(APIView):
|
||||
"""POST /dingdan/today-order-count — 抢单池当日订单总量角标。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from orders.services.today_orders import today_order_count
|
||||
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
data = today_order_count(club_id)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception as e:
|
||||
logger.error('today-order-count failed: %s', e, exc_info=True)
|
||||
return Response({'code': 7, 'msg': '服务器内部错误', 'data': {'total': 0}})
|
||||
|
||||
|
||||
class TodayOrderFeedView(APIView):
|
||||
"""POST /dingdan/today-order-feed — 今日订单页汇总+列表。"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from orders.services.today_orders import today_order_feed
|
||||
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
body = request.data if isinstance(request.data, dict) else {}
|
||||
data = today_order_feed(
|
||||
club_id,
|
||||
source=body.get('source') or 'all',
|
||||
tab=body.get('tab') or 'doing',
|
||||
page=body.get('page') or 1,
|
||||
limit=body.get('limit') or 15,
|
||||
)
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception as e:
|
||||
logger.error('today-order-feed failed: %s', e, exc_info=True)
|
||||
return Response({'code': 7, 'msg': '服务器内部错误', 'data': None})
|
||||
|
||||
|
||||
Reference in new issue
Block a user