"""orders.views.merchant_penalty - 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_PENALTY_APPLY, ACTION_PENALTY_CANCEL, ACTION_PENALTY_MODIFY, ACTION_PENALTY_RESUBMIT, ACTION_REFUND_CANCEL, ACTION_REFUND_MODIFY, ) 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__) # ===================== 视图:商家罚款申请 ===================== # 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 class ShangjiaFakuanApplyView(APIView): """ 商家罚款申请接口 路径:POST /dingdan/sjfksq 权限:JWT认证的商家用户 参数: dingdan_id - 订单ID (必填) chufa_liyou - 罚款原因 (必填) fakuanjine - 罚款金额,元 (必填) yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) 说明: 1. 自动从订单中获取被打手ID,防止商家篡改目标。 2. 同一订单同一打手不允许重复罚款。 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # ========== 1. 身份验证 ========== current_user = request.user try: shangjia_id = current_user.UserUID # 商家的用户ID except AttributeError: return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # ========== 2. 参数提取与校验 ========== dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() chufa_liyou = request.data.get('chufa_liyou', '').strip() fakuanjine = request.data.get('fakuanjine', 0) yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) evidence_urls = request.data.get('evidence_urls') or [] if not dingdan_id or not chufa_liyou or not fakuanjine: return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) if not isinstance(evidence_urls, list): return Response({'code': 2, 'msg': '证据图片格式错误'}, status=status.HTTP_400_BAD_REQUEST) evidence_urls = [str(u).strip() for u in evidence_urls if str(u).strip()] if len(evidence_urls) > 9: return Response({'code': 2, 'msg': '证据图片最多9张'}, status=status.HTTP_400_BAD_REQUEST) if len(evidence_urls) < 1: return Response({'code': 2, 'msg': '请至少上传1张证据图片'}, status=status.HTTP_400_BAD_REQUEST) try: fakuanjine = float(fakuanjine) if fakuanjine <= 0: raise ValueError except (ValueError, TypeError): return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) # ========== 3. 获取订单,校验归属商家 ========== try: # 关联查询商家扩展表,确保订单属于当前商家 shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get( Order__OrderID=dingdan_id, MerchantID=shangjia_id ) order = shangjia_kuozhan.Order except MerchantOrderExt.DoesNotExist: return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) # ========== 4. 获取被打手ID ========== dashou_id = order.PlayerID if not dashou_id: return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) # ========== 5. 重复罚款检查 ========== if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists(): return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) # ========== 6. 本地创建罚单 ========== staff_kefu_id = None staff_member = getattr(request, '_staff_member', None) if staff_member is not None: staff_kefu_id = getattr(staff_member, 'staff_user_id', None) or None from utils.penalty_status import PENALTY_PLATFORM_AUDIT with transaction.atomic(): fadan = Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=shangjia_id, # 申请人为当前商家 Identity=1, # 被打者身份:1=打手 Reason=chufa_liyou, FineAmount=fakuanjine, RelatedOrderID=dingdan_id, Status=PENALTY_PLATFORM_AUDIT, # 平台审核中,通过后变为待缴纳 AffectsGrabbing=yingxiang_qiangdan, ApplicantIdentity=5, # 申请人是商家 ClubID=resolve_penalty_club_id( order=order, penalized_user_id=dashou_id, request=request, applicant_id=shangjia_id, user=current_user, ), ShopStaffUserID=staff_kefu_id, ) PenaltyAppealImage.objects.bulk_create([ PenaltyAppealImage( Penalty=fadan, PenalizedUserID=dashou_id, ImageURL=url, Purpose=1, ) for url in evidence_urls ]) lock_penalty_bonus(fadan) logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}") record_order_action_from_request( request, ACTION_PENALTY_APPLY, dingdan_id, merchant_id=shangjia_id, amount=fakuanjine, leixing_id=leixing_id_from_order(order), remark=chufa_liyou[:200] if chufa_liyou else '', ) return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}}) class ShangjiaFakuaiXiugaiView(APIView): """ 商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由 URL: /dingdan/sjfkxgycx """ permission_classes = [IsAuthenticated] def post(self, request): user = request.user # 验证商家身份 try: shangjia = user.ShopProfile except UserShangjia.DoesNotExist: return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN) if shangjia.zhuangtai != 1: return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN) operation = request.data.get('operation') dingdan_id = request.data.get('dingdan_id') or request.data.get('OrderID') if not dingdan_id: return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST) # 验证订单属于该商家 try: dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.MerchantID != user.UserUID: return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN) # 处理订单相关操作 if operation == 'cancel_refund': resp = self._cancel_refund(dingdan, request.data.get('liyou', '')) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_REFUND_CANCEL, dingdan_id, merchant_id=user.UserUID, amount=getattr(dingdan, 'Amount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) return resp elif operation == 'modify_refund_reason': resp = self._modify_refund_reason(dingdan, request.data.get('liyou', '')) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_REFUND_MODIFY, dingdan_id, merchant_id=user.UserUID, amount=getattr(dingdan, 'Amount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) return resp # 处理罚款相关操作 fadan_id = request.data.get('fadan_id') if not fadan_id: return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) try: fadan = Penalty.query.get(id=fadan_id) except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) if fadan.RelatedOrderID != dingdan_id: return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) if operation == 'modify_penalty': resp = self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_PENALTY_MODIFY, dingdan_id, merchant_id=user.UserUID, amount=request.data.get('jine') or getattr(fadan, 'FineAmount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) return resp elif operation == 'cancel_penalty': resp = self._cancel_penalty(fadan, request.data.get('liyou', ''), user.UserUID) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_PENALTY_CANCEL, dingdan_id, merchant_id=user.UserUID, amount=getattr(fadan, 'FineAmount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) return resp elif operation == 'resubmit_penalty': resp = self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_PENALTY_RESUBMIT, dingdan_id, merchant_id=user.UserUID, amount=request.data.get('jine') or getattr(fadan, 'FineAmount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) return resp elif operation == 'manage_penalty_evidence': return self._manage_penalty_evidence(fadan, request.data) else: return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST) def _cancel_refund(self, dingdan, liyou): """撤销退款申请(状态4 → 状态8,清空退款理由)""" if dingdan.Status != 4: return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'}) with transaction.atomic(): dingdan.Status = 8 dingdan.RefundReason = '' # 清空退款理由 dingdan.save() # 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理) logger.info(f"商家撤销退款订单 {dingdan.OrderID},理由: {liyou}") return Response({'code': 0, 'msg': '已撤销退款申请'}) def _modify_refund_reason(self, dingdan, liyou): """修改退款理由(仅状态4时)""" if dingdan.Status != 4: return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'}) if not liyou.strip(): return Response({'code': 400, 'msg': '退款理由不能为空'}) dingdan.RefundReason = liyou[:500] dingdan.save() return Response({'code': 0, 'msg': '退款理由修改成功'}) def _modify_penalty(self, fadan, liyou, jine_str, processor_id=None): """修改罚款(待缴纳 / 申诉中 / 平台审核中)""" from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT): return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) try: jine = Decimal(jine_str) if jine <= 0: raise ValueError except: return Response({'code': 400, 'msg': '罚款金额必须为正数'}) with transaction.atomic(): fadan.Reason = liyou[:500] fadan.FineAmount = jine if processor_id: fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 # 小程序商家自行处理 fadan.save() lock_penalty_bonus(fadan) return Response({'code': 0, 'msg': '罚款修改成功'}) def _cancel_penalty(self, fadan, liyou, processor_id=None): """撤销罚款(待缴纳 / 申诉中 / 平台审核中 → 已驳回)""" from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT, PENALTY_REJECTED if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT): return Response({'code': 400, 'msg': '当前罚单状态不可撤销'}) if not liyou.strip(): return Response({'code': 400, 'msg': '驳回理由不能为空'}) with transaction.atomic(): fadan.Status = PENALTY_REJECTED # 已驳回 fadan.RejectReason = liyou[:500] if processor_id: fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 # 小程序商家撤销=商家作为处理人 fadan.save() return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) def _resubmit_penalty(self, fadan, liyou, jine_str, processor_id=None): """再次申请罚款(已驳回 → 平台审核中)""" from utils.penalty_status import PENALTY_REJECTED, PENALTY_PLATFORM_AUDIT if fadan.Status != PENALTY_REJECTED: return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) try: jine = Decimal(jine_str) if jine <= 0: raise ValueError except: return Response({'code': 400, 'msg': '罚款金额必须为正数'}) with transaction.atomic(): fadan.Status = PENALTY_PLATFORM_AUDIT fadan.Reason = liyou[:500] fadan.FineAmount = jine fadan.RejectReason = '' # 清空驳回理由 if processor_id: fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 fadan.save() lock_penalty_bonus(fadan) logger.info(f"商家重新申请罚单{fadan.id},金额={jine}") return Response({'code': 0, 'msg': '已重新申请罚款'}) def _manage_penalty_evidence(self, fadan, data): """管理罚单证据图片(Purpose=1),与申诉图片严格隔离""" from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT): return Response({'code': 400, 'msg': '当前罚单状态不可修改证据图片'}) add_urls = data.get('add_urls') or [] remove_ids = data.get('remove_ids') or [] if not add_urls and not remove_ids: return Response({'code': 400, 'msg': '未提供图片变更'}) with transaction.atomic(): if remove_ids: PenaltyAppealImage.query.filter( Penalty=fadan, Purpose=1, id__in=remove_ids ).delete() existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count() if existing_count + len(add_urls) > 9: return Response({'code': 400, 'msg': '证据图片最多9张'}) _valid_add_urls = [str(u).strip() for u in add_urls if str(u).strip()] if _valid_add_urls: PenaltyAppealImage.objects.bulk_create([ PenaltyAppealImage( Penalty=fadan, PenalizedUserID=fadan.PenalizedUserID, ImageURL=url, Purpose=1, ) for url in _valid_add_urls ]) logger.info( f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张" ) return Response({'code': 0, 'msg': '证据图片已更新'})