"""后台假单管理(按俱乐部 + 商品类型)。""" import logging import os import random import time import uuid from decimal import Decimal from jituan.constants import DATA_SCOPE_ALL from jituan.services.club_context import ( club_id_for_write, resolve_club_id_from_request, resolve_club_scope, ) from jituan.services.club_user_access import list_response_meta from orders.models import FakeGrabOrder, FakeGrabOrderShuffleState from orders.services.fake_order_pool import DEFAULT_DISPATCHER_AVATAR, _scope_key from products.models import ShangpinLeixing logger = logging.getLogger(__name__) CLUB_WRITE_SCOPE_MSG = '当前为集团汇总视图,请先在顶栏切换到具体俱乐部后再操作' CLUB_READ_SCOPE_MSG = '请先在顶栏切换到具体小程序/俱乐部后再操作' PRODUCT_PERMS = ('2200a',) SHUFFLE_STATE_PK = 1 def club_read_blocked(request): return resolve_club_scope(request) == DATA_SCOPE_ALL def club_write_blocked(request): return resolve_club_scope(request) == DATA_SCOPE_ALL def _gen_order_id(): while True: oid = f'JD{int(time.time() * 1000)}{random.randint(1000, 9999)}' if not FakeGrabOrder.query.filter(OrderID=oid).exists(): return oid def invalidate_fake_order_shuffle(club_id, leixing_id=None): """假单增删改后清除对应打乱缓存,下次读取自动重排。""" try: state = FakeGrabOrderShuffleState.objects.get(pk=SHUFFLE_STATE_PK) except FakeGrabOrderShuffleState.DoesNotExist: return seq_map = dict(state.OrderSequence or {}) if leixing_id is not None: seq_map.pop(_scope_key(club_id, leixing_id), None) else: prefix = f'{club_id}|' for key in list(seq_map.keys()): if key.startswith(prefix): seq_map.pop(key, None) state.OrderSequence = seq_map state.save(update_fields=['OrderSequence', 'UpdateTime']) def _leixing_name_map(): return { lx.id: (lx.jieshao or f'类型{lx.id}') for lx in ShangpinLeixing.query.exclude(shenhezhuangtai=2) } def build_leixing_options(): """全部可用商品类型(非审核专用),假单可绑定任意类型。""" options = [] for lx in ShangpinLeixing.query.exclude(shenhezhuangtai=2).order_by('-paixu', 'id'): options.append({ 'id': lx.id, 'jieshao': lx.jieshao or '', 'tupian_url': lx.tupian_url or '', 'bankuai_id': lx.bankuai_id, }) return options def _row_to_dict(row, leixing_names=None): leixing_names = leixing_names or _leixing_name_map() return { 'order_id': row.OrderID, 'dingdan_id': row.OrderID, 'amount': str(row.Amount or 0), 'jine': str(row.Amount or 0), 'leixing_id': row.ProductTypeID, 'leixing_name': leixing_names.get(row.ProductTypeID, f'类型{row.ProductTypeID}'), 'jieshao': row.Description or '', 'beizhu': row.Remark or '', 'paidan_yonghuid': row.DispatcherUserID or '', 'paidan_touxiang': row.DispatcherAvatar or DEFAULT_DISPATCHER_AVATAR, 'shangjia_nicheng': row.MerchantNickname or '', 'is_youzhi_shangjia': bool(row.IsPremiumMerchant), 'fadan_pingtai': int(row.Platform or 1), 'platform_label': '平台发单' if int(row.Platform or 1) == 1 else '商家发单', 'sort_order': int(row.SortOrder or 0), 'club_id': row.ClubID, 'CreateTime': row.CreateTime.isoformat() if row.CreateTime else None, 'UpdateTime': row.UpdateTime.isoformat() if row.UpdateTime else None, } def build_fake_order_list_payload(request, leixing_id=None, page=1, page_size=20): if club_read_blocked(request): return { 'list': [], 'total': 0, 'page': page, 'page_size': page_size, 'club_id': resolve_club_id_from_request(request), 'scope': resolve_club_scope(request), 'scope_error': CLUB_READ_SCOPE_MSG, 'leixing_options': build_leixing_options(), **list_response_meta(request), } club_id = resolve_club_id_from_request(request) page = max(1, int(page or 1)) page_size = min(100, max(1, int(page_size or 20))) qs = FakeGrabOrder.query.filter(ClubID=club_id) if leixing_id not in (None, ''): qs = qs.filter(ProductTypeID=int(leixing_id)) qs = qs.order_by('-SortOrder', '-CreateTime') total = qs.count() offset = (page - 1) * page_size rows = list(qs[offset:offset + page_size]) names = _leixing_name_map() return { 'list': [_row_to_dict(r, names) for r in rows], 'total': total, 'page': page, 'page_size': page_size, 'club_id': club_id, 'scope': resolve_club_scope(request), 'leixing_options': build_leixing_options(), **list_response_meta(request), } def _parse_decimal(value, field_name='金额'): try: val = Decimal(str(value)) if val < 0: raise ValueError return val except Exception: raise ValueError(f'{field_name}格式错误') def _validate_leixing_id(leixing_id): try: lid = int(leixing_id) except (TypeError, ValueError): return None, '请选择商品类型' if not ShangpinLeixing.query.filter(id=lid).exclude(shenhezhuangtai=2).exists(): return None, '商品类型不存在或为审核专用' return lid, None def create_fake_order(request, data): if club_write_blocked(request): return None, CLUB_WRITE_SCOPE_MSG club_id = club_id_for_write(request) leixing_id, err = _validate_leixing_id(data.get('leixing_id')) if err: return None, err try: amount = _parse_decimal(data.get('amount') or data.get('jine'), '价格') except ValueError as e: return None, str(e) platform = int(data.get('fadan_pingtai') or data.get('platform') or 1) if platform not in (1, 2): return None, '发单平台无效' row = FakeGrabOrder.query.create( OrderID=_gen_order_id(), Amount=amount, ProductTypeID=leixing_id, Description=(data.get('jieshao') or '').strip(), Remark=(data.get('beizhu') or '').strip(), DispatcherUserID=(data.get('paidan_yonghuid') or '').strip(), DispatcherAvatar=(data.get('paidan_touxiang') or '').strip() or DEFAULT_DISPATCHER_AVATAR, MerchantNickname=(data.get('shangjia_nicheng') or '').strip() if platform == 2 else '', IsPremiumMerchant=bool(data.get('is_youzhi_shangjia')) if platform == 2 else False, Platform=platform, ClubID=club_id, SortOrder=int(data.get('sort_order') or 0), ) invalidate_fake_order_shuffle(club_id, leixing_id) return _row_to_dict(row), None def update_fake_order(request, order_id, data): if club_write_blocked(request): return None, CLUB_WRITE_SCOPE_MSG club_id = club_id_for_write(request) order_id = (order_id or '').strip() if not order_id: return None, '缺少订单ID' try: row = FakeGrabOrder.objects.get(OrderID=order_id, ClubID=club_id) except FakeGrabOrder.DoesNotExist: return None, '假单不存在或不属于当前俱乐部' old_leixing = row.ProductTypeID update_fields = ['UpdateTime'] if 'leixing_id' in data and data.get('leixing_id') not in (None, ''): leixing_id, err = _validate_leixing_id(data.get('leixing_id')) if err: return None, err row.ProductTypeID = leixing_id update_fields.append('ProductTypeID') if 'amount' in data or 'jine' in data: try: row.Amount = _parse_decimal(data.get('amount') or data.get('jine'), '价格') except ValueError as e: return None, str(e) update_fields.append('Amount') if 'jieshao' in data: row.Description = (data.get('jieshao') or '').strip() update_fields.append('Description') if 'beizhu' in data: row.Remark = (data.get('beizhu') or '').strip() update_fields.append('Remark') if 'paidan_yonghuid' in data: row.DispatcherUserID = (data.get('paidan_yonghuid') or '').strip() update_fields.append('DispatcherUserID') if 'paidan_touxiang' in data: row.DispatcherAvatar = (data.get('paidan_touxiang') or '').strip() or DEFAULT_DISPATCHER_AVATAR update_fields.append('DispatcherAvatar') if 'sort_order' in data: row.SortOrder = int(data.get('sort_order') or 0) update_fields.append('SortOrder') if 'fadan_pingtai' in data or 'platform' in data: platform = int(data.get('fadan_pingtai') or data.get('platform') or row.Platform or 1) if platform not in (1, 2): return None, '发单平台无效' row.Platform = platform update_fields.append('Platform') if platform == 1: row.MerchantNickname = '' row.IsPremiumMerchant = False update_fields.extend(['MerchantNickname', 'IsPremiumMerchant']) if row.Platform == 2: if 'shangjia_nicheng' in data: row.MerchantNickname = (data.get('shangjia_nicheng') or '').strip() update_fields.append('MerchantNickname') if 'is_youzhi_shangjia' in data: row.IsPremiumMerchant = bool(data.get('is_youzhi_shangjia')) update_fields.append('IsPremiumMerchant') row.save(update_fields=list(set(update_fields))) invalidate_fake_order_shuffle(club_id, row.ProductTypeID) if old_leixing != row.ProductTypeID: invalidate_fake_order_shuffle(club_id, old_leixing) return _row_to_dict(row), None def delete_fake_order(request, order_id): if club_write_blocked(request): return None, CLUB_WRITE_SCOPE_MSG club_id = club_id_for_write(request) order_id = (order_id or '').strip() if not order_id: return None, '缺少订单ID' try: row = FakeGrabOrder.objects.get(OrderID=order_id, ClubID=club_id) except FakeGrabOrder.DoesNotExist: return None, '假单不存在或不属于当前俱乐部' leixing_id = row.ProductTypeID row.delete() invalidate_fake_order_shuffle(club_id, leixing_id) return {'order_id': order_id}, None def upload_fake_order_avatar(request, order_id, file_obj): if club_write_blocked(request): return None, CLUB_WRITE_SCOPE_MSG from utils.oss_utils import upload_to_oss, validate_image club_id = club_id_for_write(request) order_id = (order_id or '').strip() if not order_id or not file_obj: return None, '参数不完整' ok, msg = validate_image(file_obj) if not ok: return None, msg try: row = FakeGrabOrder.objects.get(OrderID=order_id, ClubID=club_id) except FakeGrabOrder.DoesNotExist: return None, '假单不存在或不属于当前俱乐部' ext = os.path.splitext(getattr(file_obj, 'name', '') or '')[1].lower() if ext not in ('.png', '.jpg', '.jpeg', '.webp', '.gif'): ext = '.png' rel_path = f'club/{club_id}/fake_grab_order/{order_id}_{uuid.uuid4().hex}{ext}' if not upload_to_oss(file_obj, rel_path): return None, '上传 OSS 失败' row.DispatcherAvatar = rel_path row.save(update_fields=['DispatcherAvatar', 'UpdateTime']) invalidate_fake_order_shuffle(club_id, row.ProductTypeID) return {'order_id': order_id, 'paidan_touxiang': rel_path}, None