新增 merchant_order_action_log,在结算/撤单/退款/罚单/派单/换打手成功路径写入真实操作人;提供 action-stats / action-operators 接口供经营数据页筛选统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
1469 lines
61 KiB
Python
1469 lines
61 KiB
Python
"""orders.views.merchant - auto-generated by split script."""
|
||
# ==================== 标准库 ====================
|
||
import hmac
|
||
import os
|
||
import json
|
||
import time
|
||
import random
|
||
import string
|
||
import traceback
|
||
import threading
|
||
import requests
|
||
import hashlib
|
||
import xml.etree.ElementTree as ET
|
||
import xmltodict
|
||
import logging
|
||
from decimal import Decimal
|
||
|
||
from django.conf import settings
|
||
from django.db import models, transaction
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models import F, Q, OuterRef, Subquery
|
||
from django.core.cache import cache
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.http import HttpResponse
|
||
from django.utils import timezone
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import permissions, status
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework.parsers import JSONParser
|
||
from rest_framework.throttling import AnonRateThrottle
|
||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||
|
||
from tencentcloud.common import credential
|
||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||
from tencentcloud.sts.v20180813 import sts_client, models
|
||
|
||
from utils.weixin_broadcast import WeixinBroadcastSender
|
||
from utils.money import yuan_to_fen
|
||
from utils.chat_utils import (
|
||
_send_group_message, _subscribe_users_to_group, establish_order_chat,
|
||
prepare_order_group_chat, resolve_pair_group_id,
|
||
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
|
||
query_pair_orders, order_to_chat_dict,
|
||
)
|
||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||
|
||
from orders.utils import (
|
||
calc_shangjia_order_fencheng,
|
||
update_daily_payout,
|
||
settle_shangjia_order_guanshi_fenhong
|
||
)
|
||
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
|
||
from merchant_ops.constants import (
|
||
ACTION_SETTLE, ACTION_CANCEL, ACTION_REFUND_APPLY, ACTION_DISPATCH, ACTION_PENALTY_APPLY,
|
||
)
|
||
from merchant_ops.services.action_log import (
|
||
record_order_action_from_request, leixing_id_from_order,
|
||
)
|
||
from rank.services import record_dashou_biaoxian
|
||
from rank.utils import check_dashou_biaoqian_required
|
||
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
|
||
from ..models import (
|
||
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
|
||
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
|
||
CommissionRate, PlayerRating, RefundRecord,
|
||
)
|
||
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
|
||
from jituan.services.club_context import resolve_club_id_from_request
|
||
from jituan.services.club_user import get_payment_openid
|
||
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
|
||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
|
||
from users.models import UserDashou, UserShangjia, UserBoss
|
||
from users.business_models import User
|
||
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
|
||
from config.models import (
|
||
ShangjiaLianjie
|
||
)
|
||
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class ShangpinLeixingView(APIView):
|
||
"""
|
||
获取商家可用的商品类型列表(含关联板块的称号)
|
||
POST /dingdan/sjspleixing
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@staticmethod
|
||
def _can_access(request):
|
||
"""商家老板、有效子客服(含代理调用)方可获取"""
|
||
if getattr(request, '_staff_member', None) is not None:
|
||
return True
|
||
from merchant_ops.services.authz import (
|
||
get_active_staff_member, is_merchant_owner, member_has_perm, staff_order_read_perm,
|
||
)
|
||
if is_merchant_owner(request.user):
|
||
return True
|
||
member = get_active_staff_member(request.user)
|
||
return bool(member and staff_order_read_perm(member))
|
||
|
||
def post(self, request):
|
||
|
||
if not self._can_access(request):
|
||
return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []})
|
||
|
||
try:
|
||
# 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序
|
||
shangpin_queryset = ShangpinLeixing.query.filter(
|
||
yaoqiuleixing=1,
|
||
shenhezhuangtai=1
|
||
).select_related('bankuai').order_by('paixu')
|
||
|
||
# 2. 收集所有涉及到的板块ID
|
||
bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct()
|
||
|
||
# 3. 查询这些板块下的所有称号(每个称号绑定到一个板块)
|
||
chenghao_list = Chenghao.query.filter(
|
||
bankuai_id__in=bankuai_ids
|
||
).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id')
|
||
|
||
# 4. 构建 板块ID -> 称号列表 的映射(确保准确关联)
|
||
bankuai_chenghao_map = {}
|
||
for ch in chenghao_list:
|
||
bid = ch['bankuai_id']
|
||
if bid not in bankuai_chenghao_map:
|
||
bankuai_chenghao_map[bid] = []
|
||
bankuai_chenghao_map[bid].append({
|
||
'id': ch['id'],
|
||
'mingcheng': ch['mingcheng'],
|
||
'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要
|
||
})
|
||
|
||
# 5. 组装最终数据
|
||
data_list = []
|
||
for sp in shangpin_queryset:
|
||
item = {
|
||
'id': sp.id,
|
||
'yaoqiuleixing': sp.yaoqiuleixing,
|
||
'huiyuan_id': sp.huiyuan_id,
|
||
'jieshao': sp.jieshao,
|
||
# 当前类型所属板块的称号列表(无板块则为空数组)
|
||
'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, [])
|
||
}
|
||
data_list.append(item)
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': data_list
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取商品类型失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||
|
||
|
||
|
||
class ShangjiaPaifaView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
logger.info(f"商家派单请求: user_id={request.user.UserUID}, data={request.data}")
|
||
|
||
try:
|
||
data = request.data
|
||
|
||
# ----- 1. 获取原始参数(保持原类型)-----
|
||
shangpin_type_id = data.get('shangpinTypeId')
|
||
huiyuan_id = data.get('huiyuanId')
|
||
yaoqiuleixing = data.get('GrabRequirement')
|
||
dingdan_jieshao_raw = data.get('dingdanJieshao', '')
|
||
dingdan_beizhu_raw = data.get('dingdanBeizhu', '')
|
||
laoban_name_raw = data.get('laobanName', '')
|
||
zhiding_uid_raw = data.get('zhidingUid', '')
|
||
jiage_str = data.get('jiage')
|
||
|
||
label_id = data.get('labelId') # 整数或 None
|
||
commission_enabled = data.get('commissionEnabled', False)
|
||
commission_value_raw = data.get('commissionValue')
|
||
waibu_dingdan_id_raw = data.get('waibuDingdanId', '')
|
||
|
||
# ----- 2. 字符串字段安全处理 -----
|
||
dingdan_jieshao = str(dingdan_jieshao_raw).strip()
|
||
dingdan_beizhu = str(dingdan_beizhu_raw).strip()
|
||
laoban_name = str(laoban_name_raw).strip()
|
||
zhiding_uid = str(zhiding_uid_raw).strip()
|
||
commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
|
||
waibu_dingdan_id = str(waibu_dingdan_id_raw).strip()[:64] if waibu_dingdan_id_raw else ''
|
||
|
||
# ----- 3. 必填校验 -----
|
||
if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]):
|
||
logger.warning(f"参数缺失: {request.data}")
|
||
return Response({'code': 400, 'msg': '缺少必填参数'})
|
||
|
||
# ----- 4. 价格验证 -----
|
||
try:
|
||
jiage = float(jiage_str)
|
||
if jiage < 1 or jiage > 10000:
|
||
return Response({'code': 400, 'msg': '价格需在1-10000元之间'})
|
||
if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2:
|
||
return Response({'code': 400, 'msg': '最多支持两位小数'})
|
||
except ValueError:
|
||
return Response({'code': 400, 'msg': '价格格式错误'})
|
||
|
||
# ----- 5. 商品类型 -----
|
||
try:
|
||
shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
logger.error(f"商品类型不存在: {shangpin_type_id}")
|
||
return Response({'code': 400, 'msg': '商品类型不存在'})
|
||
|
||
# ----- 6. 押金 / 会员模式决定 -----
|
||
final_yongjin = None
|
||
if commission_enabled and commission_value_str:
|
||
try:
|
||
cv = float(commission_value_str)
|
||
if cv > 0:
|
||
final_yaoqiu_type = 2
|
||
final_yongjin = Decimal(str(cv))
|
||
final_huiyuan_id = ''
|
||
logger.info(f"押金模式: 佣金={cv}")
|
||
else:
|
||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||
logger.warning("佣金为0,降级为会员模式")
|
||
except ValueError:
|
||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||
logger.warning("佣金格式错误,降级为会员模式")
|
||
else:
|
||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||
logger.info(f"会员模式: 类型={final_yaoqiu_type}")
|
||
|
||
# ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)-----
|
||
chenghao_obj = None
|
||
label_id_str = str(label_id).strip() if label_id is not None else ''
|
||
if label_id_str and label_id_str not in ('0', 'null', 'undefined'):
|
||
try:
|
||
lid = int(label_id_str)
|
||
if lid > 0:
|
||
chenghao_obj = Chenghao.query.filter(id=lid).first()
|
||
if chenghao_obj:
|
||
logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}")
|
||
else:
|
||
logger.info(f"标签不存在,按无标签派单: {label_id}")
|
||
except (ValueError, TypeError):
|
||
logger.info(f"标签ID无效,按无标签派单: {label_id}")
|
||
|
||
# ----- 8. 商家信息 -----
|
||
try:
|
||
shangjia = request.user.ShopProfile
|
||
except UserShangjia.DoesNotExist:
|
||
logger.warning(f"用户不是商家: {request.user.UserUID}")
|
||
return Response({'code': 403, 'msg': '您不是商家'})
|
||
|
||
if shangjia.zhuangtai != 1:
|
||
logger.warning(f"商家状态异常: {shangjia.zhuangtai}")
|
||
return Response({'code': 403, 'msg': '商家状态异常'})
|
||
|
||
if jiage > shangjia.yue:
|
||
logger.warning(f"余额不足: {shangjia.yue} < {jiage}")
|
||
return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'})
|
||
|
||
# ----- 9. 指定打手 -----
|
||
zhiding_dashou = None
|
||
if zhiding_uid:
|
||
try:
|
||
zhiding_user = User.query.get(UserUID=zhiding_uid)
|
||
zhiding_dashou = zhiding_user.DashouProfile
|
||
if zhiding_dashou.zhanghaozhuangtai != 1:
|
||
return Response({'code': 400, 'msg': '指定打手状态异常'})
|
||
except User.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '指定用户不存在'})
|
||
except UserDashou.DoesNotExist:
|
||
return Response({'code': 400, 'msg': '指定用户不是打手'})
|
||
|
||
# ----- 10. 利率(打手+管事分成,打手优先) -----
|
||
club_id = resolve_club_id_from_request(request)
|
||
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
|
||
|
||
# ----- 11. 生成订单ID -----
|
||
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}"
|
||
|
||
# ----- 12. 事务:创建订单、更新商家统计 -----
|
||
dingdan = None
|
||
try:
|
||
with transaction.atomic():
|
||
dingdan = Order.query.create(
|
||
OrderID=generate_dingdan_id(),
|
||
Status=7 if zhiding_dashou else 1,
|
||
Platform=2,
|
||
Amount=jiage,
|
||
PlayerCommission=dashou_fencheng,
|
||
ManagerCommission=guanshi_fencheng,
|
||
AssignedID=zhiding_uid if zhiding_dashou else '',
|
||
ProductID=0,
|
||
ProductTypeID=int(shangpin_type_id),
|
||
GrabRequirement=final_yaoqiu_type,
|
||
CommissionReq=final_yongjin,
|
||
MembershipID=final_huiyuan_id,
|
||
Description=dingdan_jieshao[:2000],
|
||
Remark=dingdan_beizhu[:1000],
|
||
Nickname=laoban_name[:50],
|
||
ImageURL=request.user.Avatar,
|
||
User1ID=f"Sj{request.user.UserUID}",
|
||
ClubID=club_id,
|
||
ExternalOrderID=waibu_dingdan_id or None,
|
||
)
|
||
logger.info(f"订单主表创建: {dingdan.OrderID}")
|
||
|
||
MerchantOrderExt.query.create(
|
||
Order=dingdan,
|
||
MerchantID=request.user.UserUID,
|
||
MerchantNickname=shangjia.nicheng,
|
||
)
|
||
|
||
if chenghao_obj:
|
||
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj)
|
||
logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
|
||
|
||
# 更新商家统计
|
||
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,
|
||
)
|
||
|
||
update_shangjia_daily(
|
||
yonghuid=request.user.UserUID,
|
||
amount=Decimal(str(jiage)),
|
||
action=1,
|
||
order_id=dingdan.OrderID,
|
||
)
|
||
# 事务提交,订单已持久化
|
||
except Exception as e:
|
||
logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
|
||
|
||
# ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
|
||
# Celery 广播任务(异步)
|
||
try:
|
||
order_info = {
|
||
'OrderID': dingdan.OrderID,
|
||
'game_type': self._get_game_type_name(dingdan.ProductTypeID),
|
||
'amount': str(dingdan.Amount),
|
||
'order_desc': (dingdan.Description or '')[:50],
|
||
}
|
||
dingdan_guangbo.delay(order_info)
|
||
logger.info(f"已提交广播任务: {dingdan.OrderID}")
|
||
except Exception as e:
|
||
logger.error(f"广播任务提交失败: {e}", exc_info=True)
|
||
|
||
logger.info(f"商家派单成功: {dingdan.OrderID}, 金额={jiage}")
|
||
record_order_action_from_request(
|
||
request,
|
||
ACTION_DISPATCH,
|
||
dingdan.OrderID,
|
||
merchant_id=request.user.UserUID,
|
||
amount=jiage,
|
||
leixing_id=getattr(dingdan, 'ProductTypeID', None),
|
||
)
|
||
return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.Amount)}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '系统内部错误'})
|
||
|
||
def _get_game_type_name(self, leixing_id):
|
||
try:
|
||
gt = ShangpinLeixing.query.filter(id=leixing_id).first()
|
||
return gt.jieshao if gt else '游戏订单'
|
||
except Exception:
|
||
return '游戏订单'
|
||
|
||
# views.py 中 ShangjiaDingdanHuoquView 完整改造
|
||
class ShangjiaDingdanHuoquView(APIView):
|
||
"""
|
||
商家订单获取接口
|
||
POST /dingdan/sjdingdanhq
|
||
需要JWT认证
|
||
支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID)
|
||
返回待结算订单数量(pending_count)
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户的yonghuid
|
||
try:
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
except AttributeError:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '用户认证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 解析参数
|
||
try:
|
||
data = request.data
|
||
zhuangtai_list = data.get('zhuangtai_list', [])
|
||
page = max(1, int(data.get('page', 1)))
|
||
page_size = min(100, max(1, int(data.get('page_size', 10))))
|
||
|
||
# 商品类型 / 关键字 / 时间段等见 apply_merchant_order_list_filters
|
||
except (ValueError, TypeError):
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 验证状态列表
|
||
if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '状态列表不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
|
||
|
||
# 4. 构建基础查询(该商家的所有订单)
|
||
base_qs = MerchantOrderExt.query.filter(
|
||
MerchantID=yonghuid
|
||
).select_related('Order')
|
||
|
||
filter_payload = dict(data)
|
||
filter_payload['_merchant_id_filter'] = yonghuid
|
||
|
||
# 5. 状态过滤
|
||
base_qs = base_qs.filter(Order__Status__in=zhuangtai_list)
|
||
|
||
# 6–7. 类型 / 关键字 / 时间段 / 客服筛选
|
||
base_qs = apply_merchant_order_list_filters(base_qs, filter_payload)
|
||
|
||
# 8. 待结算统计(全量聚合,非分页求和)
|
||
from orders.services.merchant_order_stats import (
|
||
build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member,
|
||
)
|
||
stats_qs = merchant_ext_qs(yonghuid)
|
||
stats_qs = apply_merchant_order_list_filters(stats_qs, filter_payload)
|
||
order_stats = build_merchant_order_stats(stats_qs)
|
||
|
||
# 9. 排序、分页
|
||
total_count = base_qs.count()
|
||
start_index = (page - 1) * page_size
|
||
end_index = start_index + page_size
|
||
current_page_orders = list(base_qs.order_by('-Order__CreateTime')[start_index:end_index])
|
||
|
||
from jituan.services.club_context import resolve_club_id_from_request
|
||
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
|
||
|
||
club_id = resolve_club_id_from_request(request)
|
||
order_list = enrich_merchant_order_list_items(current_page_orders, club_id=club_id)
|
||
|
||
# 11. 判断是否还有更多
|
||
current_page_count = len(current_page_orders)
|
||
has_more = start_index + current_page_count < total_count
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '成功',
|
||
'data': {
|
||
'list': order_list,
|
||
'has_more': has_more,
|
||
'total': total_count,
|
||
'pending_count': order_stats['pending_count'],
|
||
'pending_amount': order_stats['pending_amount'],
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家订单获取失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '服务器内部错误',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class ShangjiaShujuTongjiView(APIView):
|
||
"""
|
||
商家经营数据统计(订单表聚合)
|
||
POST /dingdan/sjshujutj
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
except AttributeError:
|
||
return Response({'code': 1, 'msg': '用户认证失败', 'data': None}, status=401)
|
||
|
||
try:
|
||
shangjia = current_user.ShopProfile
|
||
if shangjia.zhuangtai != 1:
|
||
return Response({'code': 403, 'msg': '商家状态异常', 'data': None}, status=403)
|
||
except Exception:
|
||
return Response({'code': 403, 'msg': '非商家用户', 'data': None}, status=403)
|
||
|
||
data = request.data or {}
|
||
from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
|
||
from orders.services.merchant_order_stats import merchant_ext_qs, build_merchant_order_stats
|
||
|
||
filter_payload = dict(data)
|
||
filter_payload['_merchant_id_filter'] = yonghuid
|
||
qs = merchant_ext_qs(yonghuid)
|
||
qs = apply_merchant_order_list_filters(qs, filter_payload)
|
||
order_stats = build_merchant_order_stats(qs)
|
||
|
||
penalty_stats = None
|
||
try:
|
||
from orders.models import Penalty, PenaltyRecord
|
||
from django.db.models import Q, Sum, Count
|
||
_fakuan_agg = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
|
||
count=Count('id'),
|
||
amt=Sum('FineAmount'),
|
||
)
|
||
fakuan_pending = _fakuan_agg['count']
|
||
fakuan_pending_amt = _fakuan_agg['amt']
|
||
jifen_pending = PenaltyRecord.query.filter(
|
||
ApplicantID=yonghuid,
|
||
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
|
||
penalty_stats = {
|
||
'fakuan_pending': fakuan_pending,
|
||
'fakuan_pending_amount': str(fakuan_pending_amt or '0.00'),
|
||
'jifen_pending': jifen_pending,
|
||
}
|
||
except Exception:
|
||
penalty_stats = {
|
||
'fakuan_pending': 0,
|
||
'fakuan_pending_amount': '0.00',
|
||
'jifen_pending': 0,
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '成功',
|
||
'data': {
|
||
'order': order_stats,
|
||
'penalty': penalty_stats,
|
||
'can_view_finance': True,
|
||
},
|
||
})
|
||
|
||
|
||
|
||
|
||
class ShangjiaDingdanXiangqingView(APIView):
|
||
"""
|
||
商家订单详情接口
|
||
返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户的 yonghuid
|
||
try:
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
except AttributeError:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '用户认证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 获取前端参数
|
||
try:
|
||
data = request.data
|
||
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||
except Exception:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not dingdan_id:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '订单ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
# 3. 查询商家订单扩展表,验证权限并获取基础订单数据
|
||
shangjia_kuozhan = MerchantOrderExt.query.filter(
|
||
MerchantID=yonghuid,
|
||
Order__OrderID=dingdan_id
|
||
).select_related('Order').first()
|
||
|
||
if not shangjia_kuozhan:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '订单不存在或您无权限查看',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
order = shangjia_kuozhan.Order
|
||
|
||
# 4. 交付图片列表
|
||
dashou_tupian_list = PlayerDeliveryImage.query.filter(
|
||
OrderID=dingdan_id
|
||
).order_by('CreateTime').values_list('ImageURL', flat=True)
|
||
dashou_jiaofu = list(dashou_tupian_list)
|
||
|
||
# 5. 打手详细信息(头像、昵称、用户ID)
|
||
dashou_yonghuid = order.PlayerID or ''
|
||
dashou_nicheng = ''
|
||
dashou_avatar = ''
|
||
if dashou_yonghuid:
|
||
dashou_user = User.query.filter(
|
||
UserUID=dashou_yonghuid
|
||
).select_related('DashouProfile').first()
|
||
if dashou_user:
|
||
dashou_avatar = dashou_user.Avatar or ''
|
||
if hasattr(dashou_user, 'DashouProfile') and dashou_user.DashouProfile:
|
||
dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
|
||
|
||
# 6. 罚款数据(如果存在)
|
||
fadan_obj = Penalty.query.filter(
|
||
RelatedOrderID=dingdan_id,
|
||
PenalizedUserID=dashou_yonghuid
|
||
).first()
|
||
fadan_dict = None
|
||
if fadan_obj:
|
||
ct = fmt_datetime(fadan_obj.CreateTime)
|
||
ut = fmt_datetime(fadan_obj.UpdateTime)
|
||
zhengju_urls = list(
|
||
PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1).values_list('ImageURL', flat=True)
|
||
)
|
||
shensu_urls = list(
|
||
PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2).values_list('ImageURL', flat=True)
|
||
)
|
||
zhengju_imgs = [
|
||
{'id': t.id, 'url': t.ImageURL}
|
||
for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1)
|
||
]
|
||
shensu_imgs = [
|
||
{'id': t.id, 'url': t.ImageURL}
|
||
for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2)
|
||
]
|
||
fadan_dict = {
|
||
'id': fadan_obj.id,
|
||
'chufaliyou': fadan_obj.Reason or '',
|
||
'fakuanjine': float(fadan_obj.FineAmount) if fadan_obj.FineAmount else 0.0,
|
||
'yingxiang_qiangdan': fadan_obj.AffectsGrabbing,
|
||
'zhuangtai': fadan_obj.Status,
|
||
'shensuliyou': fadan_obj.AppealReason or '',
|
||
'tongyi_chufaliyou': fadan_obj.ApproveReason or '',
|
||
'bohuiliyou': fadan_obj.RejectReason or '',
|
||
'yingde_fenhong': float(fadan_obj.ApplicantBonusAmount or 0),
|
||
'fenhong_lilv': float(fadan_obj.ApplicantBonusRate or 0),
|
||
'fenhong_yiruzhang': bool(fadan_obj.BonusCredited),
|
||
'zhengju_tupian': zhengju_imgs,
|
||
'shensu_tupian': shensu_imgs,
|
||
'CreateTime': ct,
|
||
'create_time': ct,
|
||
'UpdateTime': ut,
|
||
'update_time': ut,
|
||
}
|
||
|
||
# 7. 商家分红利率:已有罚单用锁定值;否则按商家所在俱乐部预览
|
||
fenhong_lilv = 0.0
|
||
try:
|
||
if fadan_obj and fadan_obj.ApplicantBonusRate:
|
||
fenhong_lilv = float(fadan_obj.ApplicantBonusRate)
|
||
else:
|
||
from users.fadan_fenhong_utils import get_merchant_fenhong_lilv
|
||
from jituan.services.club_write import resolve_club_id_for_write
|
||
merchant_club_id = resolve_club_id_for_write(request, current_user)
|
||
fenhong_lilv = get_merchant_fenhong_lilv(yonghuid, club_id=merchant_club_id)
|
||
except Exception as e:
|
||
logger.warning(f'获取分红利率失败: {e}')
|
||
|
||
# 8. 更换打手后产生的新订单ID(如果当前订单被更换过)
|
||
xin_dingdan_id = ''
|
||
try:
|
||
# 查找以当前订单作为“上一个订单ID”的新订单扩展记录
|
||
new_shangjia_order = MerchantOrderExt.query.filter(
|
||
PrevOrderID=dingdan_id,
|
||
MerchantID=yonghuid
|
||
).select_related('Order').first()
|
||
if new_shangjia_order:
|
||
xin_dingdan_id = new_shangjia_order.Order.OrderID
|
||
except Exception as e:
|
||
logger.warning(f'查找新订单ID失败: {e}')
|
||
|
||
# 9. 构建返回数据
|
||
response_data = {
|
||
# 基础订单字段
|
||
'dingdan_id': order.OrderID,
|
||
'zhuangtai': order.Status or 0,
|
||
'jine': float(order.Amount) if order.Amount else 0.0,
|
||
'jieshao': order.Description or '',
|
||
'beizhu': order.Remark or '',
|
||
'nicheng': order.Nickname or '',
|
||
**datetime_aliases(order.CreateTime),
|
||
|
||
# 打手信息
|
||
'dashou_yonghuid': dashou_yonghuid,
|
||
'dashou_nicheng': dashou_nicheng,
|
||
'dashou_avatar': dashou_avatar,
|
||
|
||
# 服务详情
|
||
'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容
|
||
'dashou_liuyan': order.PlayerRemark or '',
|
||
'dashou_jiaofu': dashou_jiaofu,
|
||
|
||
# 商家扩展字段(旧)
|
||
'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
|
||
'tkliyou': order.RefundReason or '',
|
||
'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款
|
||
'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
|
||
'bhliyou': shangjia_kuozhan.RejectReason or '',
|
||
'chufa_jieguo': '', # 已弃用
|
||
|
||
# 新增:罚款数据
|
||
'fadan': fadan_dict,
|
||
|
||
# 新增:分红利率
|
||
'fenhong_lilv': fenhong_lilv,
|
||
|
||
# 新增:更换打手后的新订单ID
|
||
'xin_dingdan_id': xin_dingdan_id,
|
||
|
||
# 拼多多订单号(商家/客服可见)
|
||
'waibu_dingdan_id': order.ExternalOrderID or '',
|
||
'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
|
||
}
|
||
|
||
try:
|
||
from merchant_ops.services.order_enrich import enrich_order_detail_data
|
||
enrich_order_detail_data(response_data, dingdan_id)
|
||
except Exception as e:
|
||
logger.warning(f'派单客服信息附加失败: {e}')
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '成功',
|
||
'data': response_data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '服务器内部错误',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
class ShangjiaJiesuanView(APIView):
|
||
"""商家结单接口"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户
|
||
try:
|
||
current_user = request.user
|
||
yonghuid = current_user.UserUID
|
||
|
||
try:
|
||
shangjia = current_user.ShopProfile
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '您不是商家或商家信息未完善',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
except AttributeError:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '用户认证失败',
|
||
'data': None
|
||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 获取前端参数
|
||
try:
|
||
data = request.data
|
||
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||
pingfen = int(data.get('pingfen', 0))
|
||
liuyan = data.get('liuyan', '').strip()
|
||
except (ValueError, TypeError):
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 参数验证
|
||
if not dingdan_id:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '订单ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if pingfen < 0 or pingfen > 5:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '评分必须在0-5分之间',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 4. 查询订单并加锁(修复并发问题)
|
||
order = Order.query.select_for_update().get(OrderID=dingdan_id)
|
||
|
||
# 验证订单是否属于当前商家
|
||
shangjia_kuozhan = MerchantOrderExt.query.filter(
|
||
Order=order,
|
||
MerchantID=yonghuid
|
||
).first()
|
||
|
||
if not shangjia_kuozhan:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '订单不存在或您无权限操作',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 5. 验证订单状态
|
||
if order.Status != 8:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '订单不在结算状态,无法结单',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 获取订单字段
|
||
jine = order.Amount or Decimal('0')
|
||
dashou_fencheng = order.PlayerCommission or Decimal('0')
|
||
jiedan_dashou_id = order.PlayerID
|
||
|
||
if not jiedan_dashou_id:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '订单没有接单打手,无法结单',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# ========== 本地订单正常结算给打手 ==========
|
||
# 查询打手信息
|
||
try:
|
||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
||
dashou = dashou_user.DashouProfile
|
||
except (User.DoesNotExist, UserDashou.DoesNotExist):
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '接单打手不存在或信息不完整',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 原子更新打手数据(使用 F 表达式)
|
||
UserDashou.query.filter(id=dashou.id).update(
|
||
chengjiaozongliang=F('chengjiaozongliang') + 1,
|
||
yue=F('yue') + dashou_fencheng,
|
||
zonge=F('zonge') + dashou_fencheng,
|
||
jinrishouyi=F('jinrishouyi') + dashou_fencheng,
|
||
jinyueshouyi=F('jinyueshouyi') + dashou_fencheng,
|
||
)
|
||
|
||
# 更新商家成交单量
|
||
UserShangjia.query.filter(id=shangjia.id).update(
|
||
chengjiao=F('chengjiao') + 1,
|
||
)
|
||
|
||
# 更新订单状态
|
||
order.Status = 3
|
||
order.save()
|
||
|
||
# 处理评分(所有情况都处理)
|
||
if pingfen > 0:
|
||
pingfen_obj, created = PlayerRating.query.get_or_create(
|
||
PlayerID=jiedan_dashou_id,
|
||
defaults={'TotalScore': pingfen, 'RatingCount': 1}
|
||
)
|
||
if not created:
|
||
PlayerRating.query.filter(id=pingfen_obj.id).update(
|
||
TotalScore=F('TotalScore') + pingfen,
|
||
RatingCount=F('RatingCount') + 1,
|
||
)
|
||
|
||
# 更新商家订单扩展表的评价
|
||
MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update(
|
||
MerchantReview=liuyan,
|
||
)
|
||
|
||
# 新增:调用打手每日统计(成交)
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=jiedan_dashou_id,
|
||
amount=dashou_fencheng,
|
||
action=2 # 2 表示成交(结算)
|
||
)
|
||
logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功")
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
# 不影响主流程
|
||
|
||
# ✅ 新方法(行为驱动,更安全)
|
||
update_shangjia_daily(
|
||
yonghuid=request.user.UserUID,
|
||
amount=Decimal(str(jine)),
|
||
action=2, # 2 = 结算
|
||
order_id=dingdan_id,
|
||
)
|
||
|
||
# 商家订单管事分红(仅 Platform=2)
|
||
if order.Platform == 2:
|
||
settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id)
|
||
|
||
record_order_action_from_request(
|
||
request,
|
||
ACTION_SETTLE,
|
||
dingdan_id,
|
||
merchant_id=yonghuid,
|
||
amount=jine,
|
||
leixing_id=leixing_id_from_order(order),
|
||
)
|
||
|
||
# 返回成功
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '结单成功',
|
||
'data': {
|
||
'dingdan_id': dingdan_id,
|
||
'new_zhuangtai': 3,
|
||
'jine': float(jine),
|
||
'dashou_fencheng': float(dashou_fencheng),
|
||
'jiedan_dashou_id': jiedan_dashou_id,
|
||
'pingfen': pingfen,
|
||
'liuyan': liuyan
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Order.DoesNotExist:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '订单不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"商家结单失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 9,
|
||
'msg': '服务器内部错误,结单失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
class ShangjiaChexiaoView(APIView):
|
||
"""商家撤销订单申请接口"""
|
||
permission_classes = [IsAuthenticated] # JWT认证
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户并验证商家身份
|
||
try:
|
||
current_user = request.user
|
||
# 使用反向查询获取商家扩展信息(性能最佳)
|
||
shangjia = current_user.ShopProfile
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '您不是商家或商家信息未完善',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 2. 检查商家账号状态
|
||
if shangjia.zhuangtai != 1:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '商家账号状态异常,无法操作',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 获取前端参数
|
||
try:
|
||
data = request.data
|
||
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||
chexiao_liyou = data.get('chexiao_liyou', '').strip()
|
||
except Exception:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 参数验证
|
||
if not dingdan_id or not chexiao_liyou:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '订单ID和撤销原因不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
# 5. 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 查询订单主表
|
||
dingdan = Order.query.filter(OrderID=dingdan_id).first()
|
||
if not dingdan:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '订单不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 6. 验证订单状态(13=链接待填单,可撤销释放额度)
|
||
if dingdan.Status not in [1, 7, 13]:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '当前订单状态不可撤销',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 7. 验证订单所属商家(使用反向查询)
|
||
shangjia_kuozhan = MerchantOrderExt.query.filter(
|
||
Order__OrderID=dingdan_id,
|
||
MerchantID=current_user.UserUID
|
||
).first()
|
||
|
||
if not shangjia_kuozhan:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '您无权撤销此订单',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 8. 创建退款记录(注意:申请状态直接设为1-成功)
|
||
RefundRecord.query.create(
|
||
OrderID=dingdan_id,
|
||
ApplicantID=current_user.UserUID,
|
||
Reason=chexiao_liyou,
|
||
ApplyStatus=1, # 直接成功
|
||
Amount=dingdan.Amount,
|
||
PlayerCommission=dingdan.PlayerCommission,
|
||
Description=dingdan.Description,
|
||
Remark=dingdan.Remark,
|
||
Nickname=dingdan.Nickname,
|
||
|
||
)
|
||
|
||
# 9. 更新商家扩展表(客服额度单不退商家余额,只释放额度)
|
||
from merchant_ops.services.wallet import is_staff_quota_order
|
||
staff_quota_order = is_staff_quota_order(dingdan_id)
|
||
shangjia_updates = {
|
||
'tuikuan': F('tuikuan') + 1,
|
||
'jinridingdan': F('jinridingdan') - 1,
|
||
'jinyuedingdan': F('jinyuedingdan') - 1,
|
||
'jinriliushui': F('jinriliushui') - dingdan.Amount,
|
||
'jinyueliushui': F('jinyueliushui') - dingdan.Amount,
|
||
}
|
||
if not staff_quota_order:
|
||
shangjia_updates['yue'] = F('yue') + dingdan.Amount
|
||
UserShangjia.query.filter(id=shangjia.id).update(**shangjia_updates)
|
||
|
||
# 10. 更新订单状态为5(已退款)
|
||
dingdan.Status = 5
|
||
|
||
dingdan.save()
|
||
|
||
# 行为驱动
|
||
update_shangjia_daily(
|
||
yonghuid=request.user.UserUID,
|
||
amount=Decimal(str(dingdan.Amount)),
|
||
action=3, # 3 = 退款
|
||
order_id=dingdan_id,
|
||
)
|
||
|
||
record_order_action_from_request(
|
||
request,
|
||
ACTION_CANCEL,
|
||
dingdan_id,
|
||
merchant_id=request.user.UserUID,
|
||
amount=dingdan.Amount,
|
||
leixing_id=leixing_id_from_order(dingdan),
|
||
remark=chexiao_liyou[:200] if chexiao_liyou else '',
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '撤销成功',
|
||
'data': {
|
||
'dingdan_id': dingdan_id,
|
||
'new_zhuangtai': 5,
|
||
'tuikuan_liyou': chexiao_liyou,
|
||
'jine': float(dingdan.Amount) if dingdan.Amount else 0.0
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
# 记录异常日志
|
||
logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True)
|
||
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '服务器内部错误,撤销失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class ShangjiaTuikuanShenqingView(APIView):
|
||
"""商家退款申请接口"""
|
||
permission_classes = [IsAuthenticated] # JWT认证
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户并验证商家身份
|
||
try:
|
||
current_user = request.user
|
||
shangjia = current_user.ShopProfile
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '您不是商家或商家信息未完善',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 2. 检查商家账号状态
|
||
if shangjia.zhuangtai != 1:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '商家账号状态异常,无法操作',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 获取前端参数
|
||
try:
|
||
data = request.data
|
||
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||
tuikuan_liyou = data.get('tuikuan_liyou', '').strip()
|
||
except Exception:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 参数验证
|
||
if not dingdan_id or not tuikuan_liyou:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '订单ID和退款原因不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
# 5. 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 查询订单主表
|
||
dingdan = Order.query.filter(OrderID=dingdan_id).first()
|
||
if not dingdan:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '订单不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 6. 验证订单状态
|
||
if dingdan.Status not in [2, 8]:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '当前订单状态不可申请退款',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 7. 验证订单所属商家
|
||
shangjia_kuozhan = MerchantOrderExt.query.filter(
|
||
Order__OrderID=dingdan_id,
|
||
MerchantID=current_user.UserUID
|
||
).first()
|
||
|
||
if not shangjia_kuozhan:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '您无权为此订单申请退款',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# ========== 8. 处理退款记录(修改点:先查询是否存在) ==========
|
||
refund_record, created = RefundRecord.query.get_or_create(
|
||
OrderID=dingdan_id,
|
||
defaults={
|
||
'PlayerID': dingdan.PlayerID,
|
||
'ApplicantID': current_user.UserUID,
|
||
'Reason': tuikuan_liyou,
|
||
'ApplyStatus': 0, # 待处理
|
||
'Amount': dingdan.Amount,
|
||
'PlayerCommission': dingdan.PlayerCommission,
|
||
'Description': dingdan.Description,
|
||
'Remark': dingdan.Remark,
|
||
'Nickname': dingdan.Nickname,
|
||
}
|
||
)
|
||
if not created:
|
||
# 记录已存在,更新退款理由和申请时间
|
||
refund_record.Reason = tuikuan_liyou
|
||
refund_record.save(update_fields=['Reason', 'UpdateTime'])
|
||
logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由")
|
||
|
||
# 9. 更新订单状态为4(退款中)
|
||
dingdan.Status = 4
|
||
dingdan.RefundReason = tuikuan_liyou
|
||
dingdan.save()
|
||
|
||
# ====== 原有功能:更新打手状态 ======
|
||
if dingdan.PlayerID:
|
||
try:
|
||
dashou_main = User.query.filter(
|
||
UserUID=dingdan.PlayerID
|
||
).select_related('DashouProfile').first()
|
||
if dashou_main and hasattr(dashou_main, 'DashouProfile'):
|
||
dashou_profile = dashou_main.DashouProfile
|
||
dashou_profile.zhuangtai = 1
|
||
dashou_profile.save()
|
||
logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.PlayerID}状态变更为1")
|
||
except Exception as e:
|
||
logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.PlayerID}")
|
||
|
||
record_order_action_from_request(
|
||
request,
|
||
ACTION_REFUND_APPLY,
|
||
dingdan_id,
|
||
merchant_id=current_user.UserUID,
|
||
amount=dingdan.Amount,
|
||
leixing_id=leixing_id_from_order(dingdan),
|
||
remark=tuikuan_liyou[:200] if tuikuan_liyou else '',
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '退款申请提交成功',
|
||
'data': {
|
||
'dingdan_id': dingdan_id,
|
||
'new_zhuangtai': 4,
|
||
'tuikuan_liyou': tuikuan_liyou,
|
||
'jine': float(dingdan.Amount) if dingdan.Amount else 0.0,
|
||
'dashou_id': dingdan.PlayerID
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家退款申请失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '服务器内部错误,申请失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class ShangjiaChufaShenqingView(APIView):
|
||
"""商家处罚申请接口(已添加图片上传功能)"""
|
||
permission_classes = [IsAuthenticated] # JWT认证
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户并验证商家身份
|
||
try:
|
||
current_user = request.user
|
||
shangjia = current_user.ShopProfile
|
||
except UserShangjia.DoesNotExist:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '您不是商家或商家信息未完善',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 2. 检查商家账号状态
|
||
if shangjia.zhuangtai != 1:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '商家账号状态异常,无法操作',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 3. 获取前端参数
|
||
try:
|
||
data = request.data
|
||
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
|
||
dashou_id = data.get('dashou_id', '').strip()
|
||
chufa_liyou = data.get('chufa_liyou', '').strip()
|
||
chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组
|
||
except Exception:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '参数格式错误',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 参数验证
|
||
if not dingdan_id or not dashou_id or not chufa_liyou:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '订单ID、打手ID和处罚原因不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
# 5. 使用事务确保数据一致性
|
||
with transaction.atomic():
|
||
# 验证打手身份
|
||
try:
|
||
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||
dashou = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '打手用户不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
except UserDashou.DoesNotExist:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '该用户不是打手',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 查询订单主表
|
||
dingdan = Order.query.filter(OrderID=dingdan_id).first()
|
||
if not dingdan:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '订单不存在',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 验证订单状态
|
||
if dingdan.Status not in [2, 8, 4, 5, 6, 3]:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '当前订单状态不可申请处罚',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证订单所属商家
|
||
shangjia_kuozhan = MerchantOrderExt.query.filter(
|
||
Order__OrderID=dingdan_id,
|
||
MerchantID=current_user.UserUID
|
||
).first()
|
||
|
||
if not shangjia_kuozhan:
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '您无权为此订单申请处罚',
|
||
'data': None
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 验证订单的打手是否与提供的打手ID一致
|
||
if dingdan.PlayerID != dashou_id:
|
||
return Response({
|
||
'code': 9,
|
||
'msg': '该打手并非此订单的服务打手',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 创建处罚记录
|
||
chufajilu = PenaltyRecord.query.create(
|
||
PlayerID=dashou_id,
|
||
ApplicantID=current_user.UserUID,
|
||
PenaltyReason=chufa_liyou,
|
||
ApplyStatus=0, # 待处理
|
||
DeductedPoints=5, # 扣除5积分
|
||
OrderID=dingdan_id,
|
||
ClubID=resolve_penalty_record_club_id(
|
||
order_id=dingdan_id,
|
||
player_id=dashou_id,
|
||
request=request,
|
||
applicant_id=current_user.UserUID,
|
||
user=current_user,
|
||
),
|
||
)
|
||
# 🔴【新增】在创建处罚记录后立即扣除打手积分
|
||
# 注意:这里使用前面已经获取的dashou对象(打手扩展表)
|
||
jifen_kouchu = chufajilu.DeductedPoints # 扣除的积分,这里是5
|
||
if jifen_kouchu > 0:
|
||
# 检查打手积分是否足够(UserDashou 字段为 jifen)
|
||
if dashou.jifen < jifen_kouchu:
|
||
dashou.jifen = 0
|
||
else:
|
||
dashou.jifen -= jifen_kouchu
|
||
dashou.save()
|
||
|
||
# 🔴【新增】保存处罚图片到数据库
|
||
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
|
||
_valid_urls = [u.strip() for u in chufa_tupian_urls if u and u.strip()]
|
||
if _valid_urls:
|
||
PenaltyEvidenceImage.objects.bulk_create([
|
||
PenaltyEvidenceImage(
|
||
OrderID=dingdan_id,
|
||
UserID=current_user.UserUID,
|
||
ImageURL=url,
|
||
)
|
||
for url in _valid_urls
|
||
])
|
||
|
||
# 更新商家订单扩展表的处罚相关字段
|
||
shangjia_kuozhan.PenaltyReason = chufa_liyou
|
||
shangjia_kuozhan.PenaltyApplyStatus = 0
|
||
shangjia_kuozhan.save()
|
||
|
||
record_order_action_from_request(
|
||
request,
|
||
ACTION_PENALTY_APPLY,
|
||
dingdan_id,
|
||
merchant_id=current_user.UserUID,
|
||
amount=None,
|
||
leixing_id=leixing_id_from_order(
|
||
Order.query.filter(OrderID=dingdan_id).first()
|
||
),
|
||
remark=(chufa_liyou or '')[:200],
|
||
)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '处罚申请提交成功',
|
||
'data': {
|
||
'dingdan_id': dingdan_id,
|
||
'dashou_id': dashou_id,
|
||
'chufa_liyou': chufa_liyou,
|
||
'sqzhuangtai': 0,
|
||
'jifen': 5,
|
||
'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 返回图片数量
|
||
}
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
|
||
|
||
return Response({
|
||
'code': 10,
|
||
'msg': '服务器内部错误,申请失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
|
||
|
||
|