From 2445b39f3b08e30b92047adf23c6249ef020ec8e Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 27 Jun 2026 04:51:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=AE=B6=E7=BD=9A=E5=8D=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=BB=9F=E8=AE=A1=E4=B8=8E=E8=AF=81=E6=8D=AE?= =?UTF-8?q?=E5=9B=BE=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orders/views.py | 72 ++++++++++++++++- users/urls.py | 6 +- users/views.py | 202 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 5 deletions(-) diff --git a/orders/views.py b/orders/views.py index 24092d4..d370792 100644 --- a/orders/views.py +++ b/orders/views.py @@ -57,7 +57,7 @@ from orders.notice_tasks import dingdan_guangbo from .models import ( Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage, - PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyBonus, + PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, CommissionRate, PlayerRating, RefundRecord, ) from jituan.services.club_config import get_commission_rate, get_commission_rate_object @@ -3871,6 +3871,27 @@ class DashouCOSZhengshuView(APIView): elif yongtu == 'fakuan': # 罚款申诉不需要订单验证 pass + elif yongtu == 'fakuan_evidence': + # 商家上传罚单证据图(Purpose=1) + fadan_id = request.data.get('fadan_id') + if not dingdan_id or not fadan_id: + return Response( + {'code': 400, 'msg': '缺少订单ID或罚单ID'}, + status=status.HTTP_400_BAD_REQUEST, + ) + try: + shangjia_profile = request.user.ShopProfile + if shangjia_profile.zhuangtai != 1: + return Response({'code': 403, 'msg': '商家状态异常'}, status=403) + except Exception: + return Response({'code': 403, 'msg': '非商家无权上传'}, status=403) + fadan = Penalty.query.filter( + id=fadan_id, + RelatedOrderID=dingdan_id, + ApplicantID=request.user.UserUID, + ).first() + if not fadan: + return Response({'code': 403, 'msg': '罚单不存在或无权操作'}, status=403) else: # 默认为订单相关,需要验证订单且打手必须接单 try: @@ -3910,6 +3931,10 @@ class DashouCOSZhengshuView(APIView): resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*" ) + elif yongtu == 'fakuan_evidence': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*" + ) else: resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" @@ -3948,8 +3973,11 @@ class DashouCOSZhengshuView(APIView): # 5. 构建响应 current_time = int(time.time()) - upload_dir = 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( - 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' + upload_dir = ( + 'fakuan/shangjiafakuan/zhengju/' if yongtu == 'fakuan_evidence' else + 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( + 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' + ) ) response_data = { 'credentials': { @@ -6499,6 +6527,8 @@ class ShangjiaFakuaiXiugaiView(APIView): return self._cancel_penalty(fadan, request.data.get('liyou', '')) elif operation == 'resubmit_penalty': return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) + 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) @@ -6541,6 +6571,10 @@ class ShangjiaFakuaiXiugaiView(APIView): fadan.FineAmount = jine # 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态 fadan.save() + logger.info( + f"商家修改罚单{fadan.id}:理由={liyou[:50]},金额={jine}," + f"操作人={fadan.ApplicantID}" + ) return Response({'code': 0, 'msg': '罚款修改成功'}) def _cancel_penalty(self, fadan, liyou): @@ -6553,6 +6587,7 @@ class ShangjiaFakuaiXiugaiView(APIView): fadan.Status = 4 # 已驳回 fadan.RejectReason = liyou[:500] fadan.save() + logger.info(f"商家撤销罚单{fadan.id},驳回理由={liyou[:50]}") return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) def _resubmit_penalty(self, fadan, liyou, jine_str): @@ -6573,4 +6608,33 @@ class ShangjiaFakuaiXiugaiView(APIView): fadan.FineAmount = jine fadan.RejectReason = '' # 清空驳回理由 fadan.save() - return Response({'code': 0, 'msg': '已重新申请罚款'}) \ No newline at end of file + logger.info(f"商家重新申请罚单{fadan.id},金额={jine}") + return Response({'code': 0, 'msg': '已重新申请罚款'}) + + def _manage_penalty_evidence(self, fadan, data): + """管理罚单证据图片(Purpose=1),与申诉图片严格隔离""" + if fadan.Status not in (1, 3): + 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() + for url in add_urls: + url = str(url).strip() + if not url: + continue + PenaltyAppealImage.query.create( + Penalty=fadan, + PenalizedUserID=fadan.PenalizedUserID, + ImageURL=url, + Purpose=1, + ) + logger.info( + f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张" + ) + return Response({'code': 0, 'msg': '证据图片已更新'}) \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index de9b5da..84db893 100644 --- a/users/urls.py +++ b/users/urls.py @@ -15,7 +15,7 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP KefuUpdateDashouView, KefuPunishmentListView, KefuPunishmentActionView, KefuPunishmentDetailView, \ KefuWithdrawListView, KefuWithdrawDetailView, KefuWithdrawActionView, KefuPunishView, AdKfglView, AdKftjView, \ AdKfxgView,GuanshiRegisterView, WechatLoginAndGuanshiRegisterView,ZuzhangZhuceView, ZuzhangXinxiView,KefuForceCompleteView,HuoQuYaoQingRenView, DashouJianquanView, LaobanJianquanView,\ - FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView,\ + FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView, ShangjiaCufaTongjiView, ShangjiaFakuanLieBiaoView,\ FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View from .tixian_shenhe_views import TixianZddkshApplyView from .paihang_views import PhbHqsjView @@ -135,6 +135,10 @@ urlpatterns = [ # 获取打手自己的罚款列表 path('dsfklbhq', DaShouFaKuanLieBiaoView.as_view(), name='获取打手自己的罚款列表'), + # 商家端处罚统计 / 金额罚单列表 + path('sjcftjhq', ShangjiaCufaTongjiView.as_view(), name='商家处罚统计'), + path('sjfklbhq', ShangjiaFakuanLieBiaoView.as_view(), name='商家罚款列表'), + # 提交罚款申诉 path('fkss', FaKuanShenSuView.as_view(), name='提交罚款申诉'), diff --git a/users/views.py b/users/views.py index de44fcc..dc4436a 100644 --- a/users/views.py +++ b/users/views.py @@ -11696,6 +11696,208 @@ class DaShouFaKuanLieBiaoView(APIView): return Response({'code': 99, 'msg': '系统繁忙'}, status=500) +def _merchant_penalty_auth(user): + """校验商家身份,返回 (yonghuid, None) 或 (None, error_response)。""" + try: + shangjia_profile = user.ShopProfile + if shangjia_profile.zhuangtai != 1: + return None, Response({ + 'code': 3, 'msg': '商家账号状态异常', 'data': None + }, status=status.HTTP_403_FORBIDDEN) + except UserShangjia.DoesNotExist: + return None, Response({ + 'code': 2, 'msg': '用户不是商家身份', 'data': None + }, status=status.HTTP_403_FORBIDDEN) + return user.UserUID, None + + +def _money_str(val): + if val is None: + return '0.00' + return str(val) + + +class ShangjiaCufaTongjiView(APIView): + """ + 商家端罚款+积分处罚综合统计(含金额汇总) + 路径: POST /yonghu/sjcftjhq + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghuid, err = _merchant_penalty_auth(request.user) + if err: + return err + + fadan_agg = Penalty.query.filter(ApplicantID=yonghuid).aggregate( + total=Count('id'), + total_jine=Sum('FineAmount'), + daijiaona=Count('id', filter=Q(Status=1)), + daijiaona_jine=Sum('FineAmount', filter=Q(Status=1)), + yijiaona=Count('id', filter=Q(Status=2)), + yijiaona_jine=Sum('FineAmount', filter=Q(Status=2)), + shensuzhong=Count('id', filter=Q(Status=3)), + shensuzhong_jine=Sum('FineAmount', filter=Q(Status=3)), + yibohui=Count('id', filter=Q(Status=4)), + yibohui_jine=Sum('FineAmount', filter=Q(Status=4)), + shijidaozhang_jine=Sum('ApplicantBonusAmount', filter=Q(Status=2)), + ) + + jifen_agg = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate( + total=Count('id'), + total_jifen=Sum('DeductedPoints'), + daichuli=Count('id', filter=Q(ApplyStatus=0) | Q(ApplyStatus=3)), + daichuli_jifen=Sum( + 'DeductedPoints', + filter=Q(ApplyStatus=0) | Q(ApplyStatus=3), + ), + yichuli=Count('id', filter=Q(ApplyStatus=1) | Q(ApplyStatus=2)), + yichuli_jifen=Sum( + 'DeductedPoints', + filter=Q(ApplyStatus=1) | Q(ApplyStatus=2), + ), + daichuli_0=Count('id', filter=Q(ApplyStatus=0)), + shensuzhong_3=Count('id', filter=Q(ApplyStatus=3)), + yichufa_1=Count('id', filter=Q(ApplyStatus=1)), + yibohui_2=Count('id', filter=Q(ApplyStatus=2)), + ) + + data = { + 'fakuan': { + 'total': fadan_agg['total'] or 0, + 'total_jine': _money_str(fadan_agg['total_jine']), + 'daijiaona': fadan_agg['daijiaona'] or 0, + 'daijiaona_jine': _money_str(fadan_agg['daijiaona_jine']), + 'yijiaona': fadan_agg['yijiaona'] or 0, + 'yijiaona_jine': _money_str(fadan_agg['yijiaona_jine']), + 'shensuzhong': fadan_agg['shensuzhong'] or 0, + 'shensuzhong_jine': _money_str(fadan_agg['shensuzhong_jine']), + 'yibohui': fadan_agg['yibohui'] or 0, + 'yibohui_jine': _money_str(fadan_agg['yibohui_jine']), + 'shijidaozhang_jine': _money_str(fadan_agg['shijidaozhang_jine']), + }, + 'jifen': { + 'total': jifen_agg['total'] or 0, + 'total_jifen': jifen_agg['total_jifen'] or 0, + 'daichuli': jifen_agg['daichuli'] or 0, + 'daichuli_jifen': jifen_agg['daichuli_jifen'] or 0, + 'yichuli': jifen_agg['yichuli'] or 0, + 'yichuli_jifen': jifen_agg['yichuli_jifen'] or 0, + 'daichuli_0': jifen_agg['daichuli_0'] or 0, + 'shensuzhong_3': jifen_agg['shensuzhong_3'] or 0, + 'yichufa_1': jifen_agg['yichufa_1'] or 0, + 'yibohui_2': jifen_agg['yibohui_2'] or 0, + }, + } + return Response({'code': 0, 'msg': '获取统计成功', 'data': data}) + except Exception as e: + logger.error(f"商家处罚统计异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统繁忙'}, status=500) + + +class ShangjiaFakuanLieBiaoView(APIView): + """ + 商家端金额罚单列表 + 路径: POST /yonghu/sjfklbhq + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghuid, err = _merchant_penalty_auth(request.user) + if err: + return err + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + if page_size > 50: + page_size = 50 + if page_size < 1: + page_size = 10 + + zhuangtai = request.data.get('zhuangtai') + sousuo_dingdan_id = str(request.data.get('sousuo_dingdan_id', '')).strip() + + qs = Penalty.query.filter(ApplicantID=yonghuid) + if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: + qs = qs.filter(Status=int(zhuangtai)) + if sousuo_dingdan_id: + qs = qs.filter(RelatedOrderID__icontains=sousuo_dingdan_id) + qs = qs.order_by('-CreateTime') + + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except Exception: + return Response({ + 'code': 0, 'msg': '获取成功', + 'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size}, + }) + + records = list(page_obj) + fadan_ids = [r.id for r in records] + staff_ids = list({r.ShopStaffUserID for r in records if r.ShopStaffUserID}) + + staff_name_map = {} + if staff_ids: + from merchant_ops.models import MerchantStaffMember + for m in MerchantStaffMember.query.filter( + merchant_id=yonghuid, staff_user_id__in=staff_ids + ): + staff_name_map[m.staff_user_id] = m.display_name or m.staff_user_id + + zhengju_map = {} + shensu_map = {} + if fadan_ids: + tupians = PenaltyAppealImage.query.filter(Penalty_id__in=fadan_ids).values( + 'id', 'Penalty_id', 'ImageURL', 'Purpose' + ) + for t in tupians: + fid = t['Penalty_id'] + img = {'id': t['id'], 'url': t['ImageURL']} + if t['Purpose'] == 1: + zhengju_map.setdefault(fid, []).append(img) + else: + shensu_map.setdefault(fid, []).append(img) + + chufa_list = [] + for r in records: + rid = r.id + chufa_list.append({ + 'id': rid, + 'beichufa_id': r.PenalizedUserID, + 'guanliandingdan_id': r.RelatedOrderID or '', + 'chufaliyou': r.Reason or '', + 'fakuanjine': str(r.FineAmount), + 'zhuangtai': r.Status, + 'yingxiang_qiangdan': r.AffectsGrabbing, + 'shensuliyou': r.AppealReason or '', + 'bohuiliyou': r.RejectReason or '', + 'applicant_bonus_amount': str(r.ApplicantBonusAmount or 0), + 'shop_staff_user_id': r.ShopStaffUserID or '', + 'staff_display_name': staff_name_map.get(r.ShopStaffUserID or '', ''), + 'zhengju_tupian': zhengju_map.get(rid, []), + 'shensu_tupian': shensu_map.get(rid, []), + 'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', + 'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '', + }) + + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': chufa_list, + 'has_more': page_obj.has_next(), + 'page': page, + 'page_size': page_size, + }, + }) + except Exception as e: + logger.error(f"商家罚款列表异常: {e}", exc_info=True) + return Response({'code': 99, 'msg': '系统繁忙'}, status=500) + + class FaKuanShenSuView(APIView): """ 罚款申诉提交/修改接口