diff --git a/jituan/urls.py b/jituan/urls.py index 636796b..c0d6479 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -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'), diff --git a/jituan/views_today_order.py b/jituan/views_today_order.py new file mode 100644 index 0000000..fe4f481 --- /dev/null +++ b/jituan/views_today_order.py @@ -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'}) diff --git a/orders/migrations/0022_today_order_fake.py b/orders/migrations/0022_today_order_fake.py new file mode 100644 index 0000000..15e330e --- /dev/null +++ b/orders/migrations/0022_today_order_fake.py @@ -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'), + ), + ] diff --git a/orders/models.py b/orders/models.py index bb12c56..17c1101 100644 --- a/orders/models.py +++ b/orders/models.py @@ -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 无关 # =========================================================================== diff --git a/orders/services/today_orders.py b/orders/services/today_orders.py new file mode 100644 index 0000000..4ddb9a1 --- /dev/null +++ b/orders/services/today_orders.py @@ -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, '' diff --git a/orders/urls.py b/orders/urls.py index ee57fad..f55e1b9 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -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='今日订单明细'), diff --git a/orders/views/__init__.py b/orders/views/__init__.py index 468497d..9018354 100644 --- a/orders/views/__init__.py +++ b/orders/views/__init__.py @@ -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", diff --git a/orders/views/dashou.py b/orders/views/dashou.py index 8332203..158bb2c 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -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})