Files
XingQue eedd5fbd46 fix: 体验会员按正式会员返回抢单池数据 + 修复 MerchantStaffMember
clumber/dashouxinxi 有效期内体验会员与正式会员同格式返回;ddhq 保持原 yaoqiuleixing/huiyuan_id 供老端匹配;补 merchant-staff 缺 import。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:09:12 +08:00

322 lines
11 KiB
Python

"""子客服专用:极速派单链接生成 / 列表(独立实现,不修改商家原接口)。"""
import hashlib
import logging
import os
import random
import secrets
import time
from decimal import Decimal
from django.conf import settings
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import F
from django.utils import timezone
from config.models import ShangjiaLianjie, ShangjiaMoban
from merchant_ops.constants import ACTOR_STAFF
from merchant_ops.models import MerchantOrderDispatch, MerchantStaffMember
from merchant_ops.services.wallet import check_wallet_available, confirm_dispatch_deduct
from orders.models import MerchantOrderExt, Order, CommissionRate
from products.models import ShangpinLeixing
from rank.models import Chenghao, DingdanBiaoqian
from users.models import UserShangjia
from users.business_models import User
logger = logging.getLogger(__name__)
def _generate_dingdan_id():
ts = int(time.time() * 1000)
pid = os.getpid() % 1000
rnd = random.randint(1000, 9999)
return f"SJ{ts}{pid:03d}{rnd}"
def _generate_secure_token(dd_id, uid):
timestamp = int(time.time() * 1_000_000)
salt = secrets.token_hex(8)
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
return _generate_secure_token(dd_id, uid)
return token
def _generate_link_url(token):
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
return f"{base}/order/{token}"
def _staff_operator_payload(member):
role = member.role
return {
'operatorType': ACTOR_STAFF,
'staffMemberId': member.id,
'staffUserId': member.staff_user_id,
'roleCode': role.role_code if role else '',
'roleName': role.role_name if role else '',
'displayName': member.display_name or '',
}
def staff_generate_link(member, staff_user, data):
"""
子客服生成发单链接。
扣商家余额 + 扣客服额度,并写入 merchant_order_dispatch 标记操作人。
"""
shangpin_type_id = data.get('shangpinTypeId')
moban_id = data.get('mobanId')
if not all([shangpin_type_id, moban_id]):
return False, {'code': 400, 'msg': '缺少参数', 'data': {}}
try:
shangpin_type_id = int(shangpin_type_id)
moban_id = int(moban_id)
except (TypeError, ValueError):
return False, {'code': 400, 'msg': '参数格式错误', 'data': {}}
merchant_id = member.merchant_id
try:
shangjia = UserShangjia.query.get(user__UserUID=merchant_id)
except UserShangjia.DoesNotExist:
return False, {'code': 403, 'msg': '所属商家不存在', 'data': {}}
if shangjia.zhuangtai != 1:
return False, {'code': 403, 'msg': '商家状态异常', 'data': {}}
try:
shangpin_leixing = ShangpinLeixing.query.get(id=shangpin_type_id, shenhezhuangtai=1)
except ShangpinLeixing.DoesNotExist:
return False, {'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}
try:
moban = ShangjiaMoban.query.get(
id=moban_id, UserID=merchant_id, ProductTypeID=shangpin_type_id,
)
except ShangjiaMoban.DoesNotExist:
return False, {'code': 400, 'msg': '模板不存在', 'data': {}}
jiage = moban.Price
try:
check_wallet_available(member, jiage)
except ValueError as e:
return False, {'code': 400, 'msg': str(e), 'data': {}}
try:
lilu = float(CommissionRate.query.get(Platform='3').Rate) or 1.0
except CommissionRate.DoesNotExist:
lilu = 1.0
dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2)))
chenghao_id = moban.TitleID
if moban.CommissionGrabEnabled and moban.CommissionPrice is not None:
final_yaoqiuleixing = 2
final_yongjin = moban.CommissionPrice
final_huiyuan_id = ''
else:
final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1
final_yongjin = None
final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
label_name = ''
ch = None
if chenghao_id:
ch = Chenghao.query.filter(id=chenghao_id).first()
if ch:
label_name = ch.mingcheng
merchant_user = User.query.filter(UserUID=merchant_id).first()
operator = _staff_operator_payload(member)
try:
with transaction.atomic():
check_wallet_available(member, jiage)
dingdan_id = _generate_dingdan_id()
secure_token = _generate_secure_token(dingdan_id, merchant_id)
dingdan = Order.query.create(
OrderID=dingdan_id,
Status=13,
Platform=2,
Amount=jiage,
PlayerCommission=dashou_fencheng,
ProductTypeID=shangpin_type_id,
GrabRequirement=final_yaoqiuleixing,
MembershipID=final_huiyuan_id,
CommissionReq=final_yongjin,
Description=moban.TemplateDesc[:2000],
User1ID=f"Sj{merchant_id}",
ImageURL=merchant_user.Avatar if merchant_user else '',
)
MerchantOrderExt.query.create(
Order=dingdan,
MerchantID=merchant_id,
MerchantNickname=shangjia.nicheng or f"商家{merchant_id}",
)
# 复用已查询的 ch 对象,避免重复查询
if ch:
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
jinridingdan=F('jinridingdan') + 1,
jinriliushui=F('jinriliushui') + jiage,
jinyuedingdan=F('jinyuedingdan') + 1,
jinyueliushui=F('jinyueliushui') + jiage,
)
link_url = _generate_link_url(secure_token)
lianjie_obj = ShangjiaLianjie.query.create(
UserID=merchant_id,
ProductTypeID=shangpin_type_id,
TemplateID=moban_id,
LinkURL=link_url,
LinkToken=secure_token,
OrderID=dingdan_id,
is_used=False,
ExpireTime=timezone.now() + timezone.timedelta(days=1),
)
ShangjiaMoban.query.filter(id=moban_id).update(
PublishedCount=F('PublishedCount') + 1,
)
MerchantOrderDispatch.objects.create(
order_id=dingdan_id,
merchant_id=merchant_id,
dispatch_actor_type=ACTOR_STAFF,
dispatch_actor_id=member.staff_user_id,
staff_member_id=member.id,
)
confirm_dispatch_deduct(member, jiage, dingdan_id, member.staff_user_id)
shangjia.refresh_from_db()
from merchant_ops.services.wallet import get_or_create_wallet
wallet = get_or_create_wallet(member)
quota_available = str(wallet.quota_available)
except ValueError as e:
return False, {'code': 400, 'msg': str(e), 'data': {}}
except Exception as e:
logger.error(f'子客服生成链接失败: {e}', exc_info=True)
return False, {'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}}
logger.info(
f"子客服链接生成 - 订单:{dingdan_id} 客服:{member.staff_user_id} "
f"角色:{operator.get('roleName')}"
)
return True, {
'code': 200,
'msg': '链接生成成功',
'data': {
'linkUrl': link_url,
'newBalance': quota_available,
'quotaAvailable': quota_available,
'dingdanId': dingdan_id,
'dingdanJieshao': moban.TemplateDesc,
'jiage': str(jiage),
'labelId': chenghao_id,
'labelName': label_name,
'youxiaoShijian': lianjie_obj.ExpireTime.strftime('%Y-%m-%d %H:%M:%S'),
**operator,
},
}
def staff_list_links(member, data):
"""子客服链接列表;总管看全部,派单员只看自己生成的。"""
from merchant_ops.services.authz import member_has_perm
moban_id = data.get('mobanId')
used = int(data.get('used', 0))
page = max(1, int(data.get('page', 1)))
page_size = min(50, max(1, int(data.get('pageSize', 5))))
if not moban_id:
return False, {'code': 400, 'msg': '模板ID不能为空', 'data': {}}
merchant_id = member.merchant_id
try:
ShangjiaMoban.query.get(id=moban_id, UserID=merchant_id)
except ShangjiaMoban.DoesNotExist:
return False, {'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}
links = ShangjiaLianjie.query.filter(
UserID=merchant_id,
TemplateID=moban_id,
is_used=(used == 1),
).order_by('-CreateTime')
if not member_has_perm(member, 'order_view_all'):
own_order_ids = MerchantOrderDispatch.query.filter(
merchant_id=merchant_id,
staff_member_id=member.id,
).values_list('order_id', flat=True)
links = links.filter(OrderID__in=own_order_ids)
paginator = Paginator(links, page_size)
if page > paginator.num_pages and paginator.count > 0:
return False, {'code': 400, 'msg': '页码超出范围', 'data': {}}
current_page = paginator.page(page) if paginator.count else paginator.page(1)
order_ids = [lk.OrderID for lk in current_page.object_list if lk.OrderID]
dispatch_map = {}
if order_ids:
for d in MerchantOrderDispatch.query.filter(order_id__in=order_ids):
dispatch_map[d.order_id] = d
member_ids = {d.staff_member_id for d in dispatch_map.values() if d.staff_member_id}
members = {}
if member_ids:
for m in MerchantStaffMember.query.filter(id__in=member_ids).select_related('role'):
members[m.id] = m
data_list = []
for link in current_page.object_list:
item = {
'id': link.id,
'lianjie': link.LinkURL,
'LinkToken': link.LinkToken,
'is_used': link.is_used,
'dingdan_id': link.OrderID,
'ExpireTime': link.ExpireTime.strftime('%Y-%m-%d %H:%M:%S') if link.ExpireTime else '',
'CreateTime': link.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
'operatorType': 'MERCHANT',
'staffMemberId': None,
'staffUserId': '',
'roleCode': '',
'roleName': '',
'displayName': '商家老板',
}
disp = dispatch_map.get(link.OrderID)
if disp and disp.dispatch_actor_type == ACTOR_STAFF and disp.staff_member_id:
sm = members.get(disp.staff_member_id)
if sm:
item.update({
'operatorType': ACTOR_STAFF,
'staffMemberId': sm.id,
'staffUserId': sm.staff_user_id,
'roleCode': sm.role.role_code if sm.role else '',
'roleName': sm.role.role_name if sm.role else '',
'displayName': sm.display_name or sm.role.role_name if sm.role else '',
})
data_list.append(item)
return True, {
'code': 200,
'msg': '获取成功',
'data': {
'list': data_list,
'total': paginator.count,
'hasMore': current_page.has_next(),
'currentPage': page,
'pageSize': page_size,
'viewScope': 'all' if member_has_perm(member, 'order_view_all') else 'self',
},
}