"""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): """调用微信支付退款接口""" 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 = [''] for k, v in data.items(): xml.append(f'<{k}>{v}') xml.append('') 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': '服务器内部错误'})