331 lines
12 KiB
Python
331 lines
12 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 backend.utils import update_shangjia_daily
|
|
from config.models import ShangjiaLianjie, ShangjiaMoban
|
|
from merchant_ops.constants import ACTOR_STAFF, STAT_ACTION_DISPATCH
|
|
from merchant_ops.models import MerchantOrderDispatch, MerchantStaffMember
|
|
from merchant_ops.services.stats import update_staff_daily
|
|
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
|
|
if jiage > shangjia.yue:
|
|
return False, {'code': 400, 'msg': f'商家余额不足,当前{shangjia.yue}元', '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 = ''
|
|
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}",
|
|
)
|
|
|
|
if chenghao_id:
|
|
ch = Chenghao.query.filter(id=chenghao_id).first()
|
|
if ch:
|
|
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
|
|
|
|
UserShangjia.query.filter(id=shangjia.id).update(
|
|
fabu=F('fabu') + 1,
|
|
yue=F('yue') - jiage,
|
|
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)
|
|
update_shangjia_daily(merchant_id, Decimal(str(jiage)), STAT_ACTION_DISPATCH)
|
|
update_staff_daily(
|
|
merchant_id, member.staff_user_id, member.id, jiage, STAT_ACTION_DISPATCH,
|
|
)
|
|
MerchantStaffMember.query.filter(id=member.id).update(
|
|
today_dispatch_count=F('today_dispatch_count') + 1,
|
|
today_dispatch_amount=F('today_dispatch_amount') + Decimal(str(jiage)),
|
|
)
|
|
|
|
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',
|
|
},
|
|
}
|