小程序前端仍传 wechat;后台可切换 wechat/fubei,支持配置多套付呗并退款按原通道路由。 Co-authored-by: Cursor <cursoragent@cursor.com>
670 lines
29 KiB
Python
670 lines
29 KiB
Python
"""shop.views.shop_refund_views - 退款/强制结算/转移大厅/罚款申请视图."""
|
||
# ==================== 标准库 ====================
|
||
import io
|
||
import json
|
||
import time
|
||
import random
|
||
import string
|
||
import hmac
|
||
import hashlib
|
||
import requests
|
||
import logging
|
||
import traceback
|
||
import calendar
|
||
import threading
|
||
import xmltodict
|
||
from decimal import Decimal
|
||
from collections import defaultdict
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
from django.conf import settings
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.core.cache import cache
|
||
from django.core.paginator import Paginator, EmptyPage
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.utils import timezone
|
||
from django.http import HttpResponse
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework_simplejwt.tokens import RefreshToken
|
||
|
||
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
|
||
from utils.money import yuan_to_fen
|
||
|
||
from ..utils import verify_shop_permission
|
||
from shop.utils import verify_shop_permission, update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.utils import update_daily_payout
|
||
from backend.utils import update_dashou_daily_by_action
|
||
|
||
from ..models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji
|
||
|
||
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang
|
||
from users.business_models import User
|
||
|
||
from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan
|
||
|
||
from orders.models import (
|
||
Order, PlatformOrderExt, PlayerDeliveryImage, RefundRecord,
|
||
OrderPlayerHistory, Penalty
|
||
)
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class ShopRefundView(APIView):
|
||
"""
|
||
店铺订单退款接口
|
||
URL: POST /shangdian/tk
|
||
权限: JWT认证 + 店铺身份验证(只处理平台订单,我方派单)
|
||
|
||
请求参数:
|
||
userId (必填) : 店铺所属用户的ID
|
||
dingdan_id (必填): 订单ID
|
||
reason (可选) : 退款理由
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取参数
|
||
user_id = request.data.get('userId', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
reason = request.data.get('reason', '').strip() # 退款理由
|
||
|
||
if not user_id or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# 2. 店铺身份验证
|
||
dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
|
||
shop_id = dianpu.id
|
||
|
||
# 3. 查询订单,必须属于本店铺(通过扩展表关联)
|
||
try:
|
||
# 通过扩展表确认归属,并 select_related 获取订单主表
|
||
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
|
||
ShopID=shop_id,
|
||
Order__OrderID=dingdan_id
|
||
)
|
||
order = pingtai_ext.Order
|
||
except PlatformOrderExt.DoesNotExist:
|
||
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
|
||
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
|
||
|
||
# 4. 订单状态校验(只有这些状态可退款)
|
||
allowed_statuses = [1, 7, 2, 8, 4]
|
||
if order.Status not in allowed_statuses:
|
||
logger.warning(f"订单状态不允许退款: {order.Status}")
|
||
return Response({'code': 400, 'msg': '订单状态不允许退款'})
|
||
|
||
# 5. 全部视为平台订单(我方派单),调用微信退款
|
||
refund_result = self._call_wechat_refund(order)
|
||
if refund_result['code'] != 0:
|
||
return Response({'code': 400, 'msg': refund_result['msg']})
|
||
|
||
# 6. 事务内更新订单、打手、记录、统计
|
||
dashou_id = order.PlayerID
|
||
try:
|
||
with transaction.atomic():
|
||
# 更新订单状态为已退款
|
||
order.Status = 5
|
||
order.AssignedCS = str(shop_id) # 处理人记录为店铺ID
|
||
if reason:
|
||
order.RefundReason = reason
|
||
order.save()
|
||
|
||
# 更新打手统计(退款量+1,状态置为空闲)
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.tuikuanliang += 1
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新")
|
||
|
||
# 更新退款记录表
|
||
self._update_refund_record(order, dashou_id, str(shop_id), reason)
|
||
|
||
# 打手每日统计(退款动作)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=dashou_id,
|
||
amount=order.PlayerCommission or Decimal('0.00'),
|
||
action=3 # 3 表示退款
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
|
||
# 更新平台支出记录
|
||
try:
|
||
update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
|
||
except Exception as e:
|
||
logger.error(f"更新每日支出失败: {e}")
|
||
|
||
# ========== 新增:商品和店铺每日统计 ==========
|
||
try:
|
||
# 商品每日统计(只要有商品ID就执行)
|
||
if order.ProductID:
|
||
# 从扩展表获取已计算好的收益值(下单时已存储)
|
||
pingtai_shouyi = Decimal('0.00')
|
||
dianpu_shouyi = Decimal('0.00')
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
|
||
update_shangpin_daily_stat(
|
||
shangpin_id=order.ProductID,
|
||
caozuo_leixing=3, # 3 = 退款
|
||
dingdan_jiage=order.Amount,
|
||
pingtai_shouyi=pingtai_shouyi,
|
||
dianpu_zongshouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
|
||
|
||
# 店铺每日统计(仅当存在店铺ID时执行)
|
||
dianpu_id = None
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
dianpu_id = order.pingtai_kuozhan.dianpu_id
|
||
if dianpu_id:
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
update_dianpu_daily_stat(
|
||
dianpu_id=dianpu_id,
|
||
caozuo_leixing=3, # 3 = 退款
|
||
dingdan_jiage=order.Amount,
|
||
dianpu_shouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
|
||
except Exception as e:
|
||
logger.error(f"商品/店铺统计更新失败: {e}")
|
||
logger.error(traceback.format_exc())
|
||
# 统计失败不影响主流程
|
||
# =============================================
|
||
|
||
except Exception as e:
|
||
logger.exception("退款事务执行失败")
|
||
return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'})
|
||
|
||
return Response({'code': 200, 'msg': '退款成功'})
|
||
|
||
# ========== 微信退款 ==========
|
||
def _call_wechat_refund(self, order):
|
||
"""调用微信支付退款接口"""
|
||
from jituan.services.pay_refund_router import try_fubei_refund
|
||
fb = try_fubei_refund(
|
||
out_trade_no=order.OrderID,
|
||
amount_yuan=order.Amount or 0,
|
||
club_id=getattr(order, 'ClubID', None),
|
||
)
|
||
if fb is not None:
|
||
return fb
|
||
|
||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
|
||
key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
|
||
cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
|
||
key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
|
||
|
||
if not all([appid, mch_id, key, cert_path, key_path]):
|
||
return {'code': 500, 'msg': '微信支付配置不完整'}
|
||
|
||
out_refund_no = self._generate_refund_no(order.OrderID)
|
||
total_fee = yuan_to_fen(order.Amount or 0)
|
||
refund_fee = total_fee
|
||
|
||
params = {
|
||
'appid': appid,
|
||
'mch_id': mch_id,
|
||
'nonce_str': self._generate_nonce_str(),
|
||
'out_refund_no': out_refund_no,
|
||
'total_fee': total_fee,
|
||
'refund_fee': refund_fee,
|
||
'refund_desc': '店铺退款',
|
||
}
|
||
if order.WechatTransactionID:
|
||
params['transaction_id'] = order.WechatTransactionID
|
||
else:
|
||
params['out_trade_no'] = order.OrderID
|
||
|
||
# 签名
|
||
sorted_keys = sorted(params.keys())
|
||
stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
|
||
stringSignTemp = f"{stringA}&key={key}"
|
||
sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
|
||
params['sign'] = sign
|
||
|
||
xml_data = self._dict_to_xml(params)
|
||
url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
|
||
|
||
try:
|
||
response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10)
|
||
result = xmltodict.parse(response.content)['xml']
|
||
logger.info(f"微信退款响应: {result}")
|
||
except Exception as e:
|
||
logger.error(f"微信退款请求异常: {e}", exc_info=True)
|
||
return {'code': 500, 'msg': '微信退款请求异常'}
|
||
|
||
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
|
||
return {'code': 0, 'msg': '退款成功'}
|
||
else:
|
||
err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
|
||
return {'code': 400, 'msg': err_msg}
|
||
|
||
# ========== 退款记录 ==========
|
||
def _update_refund_record(self, order, dashou_id, chuli_id, reason):
|
||
"""更新退款记录表"""
|
||
try:
|
||
refund_record, created = RefundRecord.query.get_or_create(
|
||
OrderID=order.OrderID,
|
||
defaults={
|
||
'PlayerID': dashou_id or '',
|
||
'ApplicantID': '',
|
||
'ProcessorID': chuli_id,
|
||
'Reason': reason or '店铺退款',
|
||
'ApplyStatus': 1,
|
||
'Amount': order.Amount or 0,
|
||
'PlayerCommission': order.PlayerCommission or 0,
|
||
'Description': order.Description or '',
|
||
'Remark': '店铺退款',
|
||
'Nickname': order.Nickname or '',
|
||
}
|
||
)
|
||
if not created:
|
||
refund_record.ApplyStatus = 1
|
||
refund_record.ProcessorID = chuli_id
|
||
if reason:
|
||
refund_record.Reason = reason
|
||
refund_record.save()
|
||
except Exception as e:
|
||
logger.error(f"更新退款记录异常: {e}")
|
||
|
||
# ========== 工具方法 ==========
|
||
def _generate_nonce_str(self):
|
||
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||
|
||
def _generate_refund_no(self, dingdan_id):
|
||
timestamp = str(int(time.time()))
|
||
rand = ''.join(random.choices('0123456789', k=4))
|
||
return f"{dingdan_id}REF{timestamp}{rand}"
|
||
|
||
def _dict_to_xml(self, data):
|
||
xml = ['<xml>']
|
||
for k, v in data.items():
|
||
xml.append(f'<{k}>{v}</{k}>')
|
||
xml.append('</xml>')
|
||
return ''.join(xml)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class ShopForceCompleteView(APIView):
|
||
"""
|
||
店铺强制结单接口
|
||
URL: POST /shangdian/qzjd
|
||
权限: JWT认证 + 店铺身份验证
|
||
|
||
请求参数:
|
||
userId (必填) : 店铺所属用户的ID
|
||
dingdan_id(必填) : 订单ID
|
||
|
||
说明:
|
||
- 只处理平台订单(fadan_pingtai == 1)
|
||
- 订单必须属于当前店铺
|
||
- 订单状态必须为 8(结算中)才能强制结单
|
||
- 默认视为我方派单,根据是否有对方订单ID判断是否跨平台
|
||
- 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计
|
||
- 跨平台订单:仅通知对方平台,不处理打手
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ========== 1. 获取参数 ==========
|
||
user_id = request.data.get('userId', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not user_id or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# ========== 2. 店铺身份验证 ==========
|
||
dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
|
||
shop_id = dianpu.id
|
||
|
||
# ========== 3. 查询订单并验证归属 ==========
|
||
try:
|
||
# 通过平台扩展表确认订单属于本店铺
|
||
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
|
||
ShopID=shop_id,
|
||
Order__OrderID=dingdan_id
|
||
)
|
||
order = pingtai_ext.Order
|
||
except PlatformOrderExt.DoesNotExist:
|
||
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
|
||
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
|
||
|
||
# ========== 4. 校验订单状态必须为 8(结算中) ==========
|
||
if order.Status != 8:
|
||
logger.warning(f"订单 {dingdan_id} 当前状态为 {order.Status},不是结算中")
|
||
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
|
||
|
||
dashou_id = order.PlayerID
|
||
dashou_fencheng = order.PlayerCommission or Decimal('0.00')
|
||
jine = order.Amount or Decimal('0.00')
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 锁定订单行,防止并发
|
||
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单无接单打手,无法结算'})
|
||
if dashou_fencheng < 0:
|
||
return Response({'code': 400, 'msg': '打手分成金额无效'})
|
||
|
||
# ---------- 更新打手余额和统计 ----------
|
||
try:
|
||
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.chengjiaozongliang += 1
|
||
dashou_profile.yue += dashou_fencheng
|
||
dashou_profile.zonge += dashou_fencheng
|
||
dashou_profile.jinrishouyi += dashou_fencheng
|
||
dashou_profile.jinyueshouyi += dashou_fencheng
|
||
dashou_profile.zhuangtai = 1 # 空闲
|
||
dashou_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新")
|
||
|
||
# ---------- 更新订单状态 ----------
|
||
order.Status = 3
|
||
order.AssignedCS = str(shop_id)
|
||
order.save()
|
||
|
||
# ---------- 打手每日统计(成交) ----------
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=dashou_id,
|
||
amount=dashou_fencheng,
|
||
action=2 # 2 表示成交(结算)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
|
||
# ---------- 店铺收益统计(结算) ----------
|
||
if pingtai_ext.ShopIncome:
|
||
try:
|
||
# 商品每日统计(只要有商品ID就执行)
|
||
if order.ProductID:
|
||
# 从扩展表获取已计算好的收益值(下单时已存储)
|
||
pingtai_shouyi = Decimal('0.00')
|
||
dianpu_shouyi = Decimal('0.00')
|
||
if hasattr(order, 'pingtai_kuozhan'):
|
||
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
|
||
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
|
||
|
||
update_shangpin_daily_stat(
|
||
shangpin_id=order.ProductID,
|
||
caozuo_leixing=2, # 3 = 退款
|
||
dingdan_jiage=order.Amount,
|
||
pingtai_shouyi=pingtai_shouyi,
|
||
dianpu_zongshouyi=dianpu_shouyi
|
||
)
|
||
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
|
||
|
||
|
||
update_dianpu_daily_stat(
|
||
dianpu_id=shop_id,
|
||
caozuo_leixing=2, # 结算操作
|
||
dingdan_jiage=jine,
|
||
dianpu_shouyi=pingtai_ext.ShopIncome,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"店铺每日统计更新失败: {e}")
|
||
|
||
# ========== 5. 记录操作日志 ==========
|
||
logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}")
|
||
|
||
return Response({'code': 200, 'msg': '强制结单成功'})
|
||
|
||
except Order.DoesNotExist:
|
||
logger.warning(f"订单不存在: {dingdan_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
# ================================================================
|
||
# shangdian/views/transfer_hall_view.py
|
||
# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑
|
||
# ================================================================
|
||
# ================================================================
|
||
# shangdian/views/transfer_hall_view.py
|
||
# 店铺转移大厅接口 —— 添加本地打手退款统计
|
||
# ================================================================
|
||
|
||
class ShopTransferHallView(APIView):
|
||
"""
|
||
店铺转移大厅接口
|
||
URL: POST /shangdian/zydt
|
||
权限: JWT认证 + 店铺身份验证
|
||
|
||
请求参数:
|
||
userId (必填) : 店铺所属用户的ID
|
||
dingdan_id(必填) : 订单ID
|
||
|
||
说明:
|
||
- 只处理平台订单,我方派单
|
||
- 允许转移的状态:2,8,4
|
||
- 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方;
|
||
没有对方信息则处理本地打手(释放+退款统计)
|
||
- 转移后清空对方平台信息,订单重回可接单状态
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 1. 获取参数 ----------
|
||
user_id = request.data.get('userId', '').strip()
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
|
||
if not user_id or not dingdan_id:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# ---------- 2. 店铺身份验证 ----------
|
||
dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
|
||
shop_id = dianpu.id
|
||
|
||
# ---------- 3. 验证订单归属 ----------
|
||
try:
|
||
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
|
||
ShopID=shop_id,
|
||
Order__OrderID=dingdan_id
|
||
)
|
||
order = pingtai_ext.Order
|
||
except PlatformOrderExt.DoesNotExist:
|
||
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
|
||
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
|
||
|
||
# ---------- 4. 状态校验 ----------
|
||
if order.Status not in [2, 8, 4]:
|
||
return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.Status}'})
|
||
|
||
# ---------- 5. 提取关键字段 ----------
|
||
current_dashou_id = order.PlayerID
|
||
zhiding_id = order.zhiding_id
|
||
dashou_fencheng = order.PlayerCommission or 0 # 用于退款统计
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 锁定订单行
|
||
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||
|
||
# ---------- 6. 记录打手历史(原接口保留逻辑) ----------
|
||
if current_dashou_id:
|
||
last_history = OrderPlayerHistory.query.filter(
|
||
dingdan_id=dingdan_id
|
||
).order_by('-times').first()
|
||
next_times = (last_history.times + 1) if last_history else 1
|
||
OrderPlayerHistory.query.create(
|
||
dingdan_id=dingdan_id,
|
||
dashou_id=current_dashou_id,
|
||
times=next_times,
|
||
operator=str(shop_id)
|
||
)
|
||
|
||
# ---------- 7. 释放本地打手 + 退款统计 ----------
|
||
if current_dashou_id:
|
||
try:
|
||
dashou_profile = UserDashou.query.get(user__UserUID=current_dashou_id)
|
||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||
dashou_profile.save()
|
||
except ObjectDoesNotExist:
|
||
logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
|
||
else:
|
||
# 打手释放成功后,记录每日退款统计(转移视为退款)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=current_dashou_id,
|
||
amount=dashou_fencheng,
|
||
action=3 # 3 表示退款
|
||
)
|
||
logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计失败: {e}")
|
||
|
||
# ---------- 8. 重置订单 ----------
|
||
order.jiedan_dashou_id = None
|
||
order.clkf = str(shop_id)
|
||
|
||
# 根据是否有指定打手决定状态
|
||
order.zhuangtai = 7 if zhiding_id else 1
|
||
order.save()
|
||
|
||
logger.info(f"转移大厅成功,订单 {dingdan_id}")
|
||
return Response({'code': 200, 'msg': '转移大厅成功'})
|
||
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
except Exception as e:
|
||
logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ================================================================
|
||
# shangdian/views/fine_apply_view.py
|
||
# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款)
|
||
# ================================================================
|
||
class ShopFineApplyView(APIView):
|
||
"""
|
||
店铺罚款申请接口
|
||
URL: POST /shangdian/fksq
|
||
权限: JWT认证 + 店铺身份验证
|
||
|
||
请求参数:
|
||
userId (必填) : 店铺所属用户的ID(用于身份验证)
|
||
dingdan_id (必填) : 订单ID
|
||
reason (必填) : 罚款原因
|
||
amount (必填) : 罚款金额(元)
|
||
yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1
|
||
|
||
说明:
|
||
- 仅处理平台订单,需属于当前店铺
|
||
- 本地/对方派单处理逻辑与客服接口一致:
|
||
无对方订单ID 或 对方派单 → 本地生成罚单
|
||
有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID)
|
||
- 申请人:店铺ID,身份码固定为4(店铺管理员)
|
||
- 被处罚人:接单打手,身份码固定为1
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 1. 提取参数 ----------
|
||
user_id = request.data.get('userId', '').strip() # 店铺用户ID
|
||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||
chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason)
|
||
fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount)
|
||
yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单
|
||
|
||
if not all([user_id, dingdan_id, chufaliyou, fakuanjine]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
# ---------- 2. 店铺身份验证 ----------
|
||
dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
shop_id = dianpu.id # 店铺ID,作为罚款申请人
|
||
|
||
# ---------- 3. 验证订单归属 ----------
|
||
try:
|
||
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
|
||
ShopID=shop_id,
|
||
Order__OrderID=dingdan_id
|
||
)
|
||
order = pingtai_ext.Order
|
||
except PlatformOrderExt.DoesNotExist:
|
||
logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}")
|
||
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
|
||
|
||
# ---------- 4. 获取打手ID ----------
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||
|
||
# ---------- 5. 重复罚款检查 ----------
|
||
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists():
|
||
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||
|
||
# ---------- 6. 创建罚单 ----------
|
||
from jituan.services.club_penalty import resolve_penalty_club_id
|
||
with transaction.atomic():
|
||
penalty = Penalty.query.create(
|
||
PenalizedUserID=dashou_id,
|
||
ApplicantID=str(shop_id), # 申请人记为店铺ID
|
||
Identity=1, # 被处罚人身份:1 打手
|
||
Reason=chufaliyou,
|
||
FineAmount=fakuanjine,
|
||
RelatedOrderID=dingdan_id,
|
||
Status=1, # 待缴纳
|
||
AffectsGrabbing=yingxiang_qiangdan,
|
||
ApplicantIdentity=4, # 申请人身份:4 店铺管理员
|
||
ClubID=resolve_penalty_club_id(
|
||
order=order,
|
||
request=request,
|
||
user=request.user,
|
||
applicant_id=str(shop_id),
|
||
),
|
||
)
|
||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||
lock_penalty_bonus(penalty)
|
||
logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
|
||
return Response({'code': 200, 'msg': '罚款已生成'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"店铺罚款申请异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'})
|