391 lines
13 KiB
Python
391 lines
13 KiB
Python
"""后台假单管理(按俱乐部 + 商品类型)。"""
|
||
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
|
||
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 _scope_key(club_id, leixing_id):
|
||
"""与历史打乱缓存 key 一致:club|leixing。"""
|
||
lid = leixing_id if leixing_id is not None else 'all'
|
||
return f'{club_id}|{lid}'
|
||
|
||
|
||
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(),
|
||
'fake_grab_enabled': False,
|
||
'fake_yajin_hide_threshold': 10,
|
||
**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()
|
||
|
||
from jituan.services.fake_grab_switch import (
|
||
get_fake_yajin_hide_threshold,
|
||
is_club_fake_grab_enabled,
|
||
)
|
||
|
||
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(),
|
||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||
'fake_yajin_hide_threshold': float(get_fake_yajin_hide_threshold(club_id)),
|
||
**list_response_meta(request),
|
||
}
|
||
|
||
|
||
def set_fake_grab_enabled(request, enabled):
|
||
"""开启/关闭本俱乐部打手侧假单功能。"""
|
||
if club_write_blocked(request):
|
||
return None, CLUB_WRITE_SCOPE_MSG
|
||
|
||
from jituan.models import Club
|
||
from jituan.services.fake_grab_switch import (
|
||
get_fake_yajin_hide_threshold,
|
||
is_club_fake_grab_enabled,
|
||
set_club_fake_grab_enabled,
|
||
)
|
||
|
||
club_id = club_id_for_write(request)
|
||
club = Club.query.filter(club_id=club_id).first()
|
||
if not club:
|
||
return None, '俱乐部不存在'
|
||
|
||
set_club_fake_grab_enabled(club, bool(enabled))
|
||
club.save(update_fields=['config_json'])
|
||
return {
|
||
'club_id': club_id,
|
||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||
'fake_yajin_hide_threshold': float(get_fake_yajin_hide_threshold(club_id)),
|
||
}, None
|
||
|
||
|
||
def set_fake_yajin_threshold(request, threshold):
|
||
"""设置押金 ≥ 该值时过滤假单。"""
|
||
if club_write_blocked(request):
|
||
return None, CLUB_WRITE_SCOPE_MSG
|
||
|
||
from jituan.models import Club
|
||
from jituan.services.fake_grab_switch import (
|
||
get_fake_yajin_hide_threshold,
|
||
is_club_fake_grab_enabled,
|
||
set_fake_yajin_hide_threshold,
|
||
)
|
||
|
||
club_id = club_id_for_write(request)
|
||
club = Club.query.filter(club_id=club_id).first()
|
||
if not club:
|
||
return None, '俱乐部不存在'
|
||
|
||
val = set_fake_yajin_hide_threshold(club, threshold)
|
||
club.save(update_fields=['config_json'])
|
||
return {
|
||
'club_id': club_id,
|
||
'fake_grab_enabled': is_club_fake_grab_enabled(club_id),
|
||
'fake_yajin_hide_threshold': float(val),
|
||
}, None
|
||
|
||
|
||
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
|