From 69b16848c4e14e74463955a03a26d08d242b1b15 Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 27 Jul 2026 07:12:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=AE=B6=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E6=94=AF=E6=8C=81=E8=AF=81=E6=8D=AE=E5=9B=BE?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 tuikuantupian 表;申请退款必传图片;打手详情与客服订单详情返回退款图。 Co-authored-by: Cursor --- .../migrations/0018_refund_evidence_image.py | 31 ++++++++++++ orders/models.py | 33 +++++++++++++ orders/views/dashou.py | 44 ++++++++++++++++- orders/views/merchant.py | 48 ++++++++++++++++++- users/views/kefu_orders.py | 13 ++++- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 orders/migrations/0018_refund_evidence_image.py diff --git a/orders/migrations/0018_refund_evidence_image.py b/orders/migrations/0018_refund_evidence_image.py new file mode 100644 index 0000000..d1e3faf --- /dev/null +++ b/orders/migrations/0018_refund_evidence_image.py @@ -0,0 +1,31 @@ +# Generated manually for RefundEvidenceImage + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0017_team_recruit'), + ] + + operations = [ + migrations.CreateModel( + name='RefundEvidenceImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('OrderID', models.CharField(db_column='dingdan_id', db_index=True, max_length=32, null=True, verbose_name='订单ID')), + ('UserID', models.CharField(blank=True, db_column='yonghuid', db_index=True, max_length=32, null=True, verbose_name='用户ID')), + ('ImageURL', models.CharField(blank=True, db_column='tupian', max_length=500, null=True, verbose_name='证据图片URL')), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'tuikuantupian', + 'indexes': [ + models.Index(fields=['UserID'], name='tuikuantupi_yonghui_idx'), + models.Index(fields=['OrderID'], name='tuikuantupi_dingdan_idx'), + ], + }, + ), + ] diff --git a/orders/models.py b/orders/models.py index 4417126..da341f1 100644 --- a/orders/models.py +++ b/orders/models.py @@ -17,6 +17,7 @@ - **PenaltyRecord** — 处罚记录 - **PenaltyEvidenceImage** — 处罚证据图片 - **RefundRecord** — 退款记录 +- **RefundEvidenceImage** — 退款申请证据图片 - **PlatformCashFlow** — 平台流水统计 - **ChatSession** — 聊天会话 """ @@ -866,6 +867,38 @@ class PenaltyEvidenceImage(QModel): ] +class RefundEvidenceImage(QModel): + """退款申请证据图片表""" + + OrderID = models.CharField( + max_length=32, db_index=True, null=True, + db_column='dingdan_id', verbose_name='订单ID', + ) + UserID = models.CharField( + max_length=32, null=True, blank=True, db_index=True, + db_column='yonghuid', verbose_name='用户ID', + ) + ImageURL = models.CharField( + max_length=500, null=True, blank=True, + db_column='tupian', verbose_name='证据图片URL', + ) + CreateTime = models.DateTimeField( + auto_now_add=True, + verbose_name='创建时间', + ) + UpdateTime = models.DateTimeField( + auto_now=True, + verbose_name='更新时间', + ) + + class Meta: + db_table = 'tuikuantupian' + indexes = [ + models.Index(fields=['UserID']), + models.Index(fields=['OrderID']), + ] + + class RefundRecord(QModel): """退款记录表""" diff --git a/orders/views/dashou.py b/orders/views/dashou.py index d9d807e..6e391d5 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -834,6 +834,33 @@ class DashouCOSZhengshuView(APIView): resolve_penalty_evidence_upload_auth(request, dingdan_id, fadan_id=fadan_id) except PenaltyEvidenceAuthError as e: return Response({'code': e.code, 'msg': e.msg}, status=403) + elif yongtu == 'tuikuan_evidence': + # 商家老板或能操作该订单的子客服上传退款证据图 + if not dingdan_id: + return Response({'code': 400, 'msg': '缺少订单ID'}, status=400) + uid = getattr(request.user, 'UserUID', None) + allowed = False + try: + from orders.models import MerchantOrderExt + sj = getattr(request.user, 'ShopProfile', None) + if sj and getattr(sj, 'zhuangtai', None) == 1: + if MerchantOrderExt.query.filter( + Order__OrderID=dingdan_id, MerchantID=uid + ).exists(): + allowed = True + except Exception: + pass + if not allowed: + try: + from merchant_ops.services.authz import get_active_staff_member, staff_can_access_order + member = get_active_staff_member(request.user) + if member: + ok, _ = staff_can_access_order(member, dingdan_id) + allowed = bool(ok) + except Exception: + pass + if not allowed: + return Response({'code': 403, 'msg': '无权上传退款图片'}, status=403) else: # 默认为订单相关,需要验证订单且打手必须接单 try: @@ -881,6 +908,10 @@ class DashouCOSZhengshuView(APIView): resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*" ) + elif yongtu == 'tuikuan_evidence': + resources.append( + f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/tuikuan/shangjiatuikuan/zhengju/*" + ) else: resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" @@ -920,7 +951,8 @@ class DashouCOSZhengshuView(APIView): # 5. 构建响应 current_time = int(time.time()) upload_dir = ( - 'fakuan/shangjiafakuan/zhengju/' if yongtu == 'fakuan_evidence' else + 'tuikuan/shangjiatuikuan/zhengju/' if yongtu == 'tuikuan_evidence' else + 'fakuan/shangjiafakuan/zhengju/' if yongtu in ('fakuan_evidence', 'fakuan_evidence_draft') else 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' ) @@ -1325,6 +1357,16 @@ class DashouDingdanXiangqingView(APIView): 'fuwudashou_id': dingdan.PlayerID or '', 'fadanpingtai': dingdan.Platform or '' } + try: + from orders.models import RefundEvidenceImage + dingdan_data['tuikuan_tupian'] = list( + RefundEvidenceImage.query.filter(OrderID=dingdan.OrderID) + .exclude(ImageURL__isnull=True) + .exclude(ImageURL='') + .values_list('ImageURL', flat=True) + ) + except Exception: + dingdan_data['tuikuan_tupian'] = [] try: from jituan.services.order_deal import order_auto_settle_payload dingdan_data.update(order_auto_settle_payload(dingdan)) diff --git a/orders/views/merchant.py b/orders/views/merchant.py index bb32b12..6d166e0 100644 --- a/orders/views/merchant.py +++ b/orders/views/merchant.py @@ -70,7 +70,7 @@ from orders.notice_tasks import dingdan_guangbo from ..models import ( Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage, PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate, - CommissionRate, PlayerRating, RefundRecord, + CommissionRate, PlayerRating, RefundRecord, RefundEvidenceImage, ) from jituan.services.club_config import get_commission_rate, get_commission_rate_object from jituan.services.club_context import resolve_club_id_from_request @@ -735,6 +735,7 @@ class ShangjiaDingdanXiangqingView(APIView): # 商家扩展字段(旧) 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '', 'tkliyou': order.RefundReason or '', + 'tuikuan_liyou': order.RefundReason or '', 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款 'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0, 'bhliyou': shangjia_kuozhan.RejectReason or '', @@ -754,6 +755,16 @@ class ShangjiaDingdanXiangqingView(APIView): 'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '', } + try: + response_data['tuikuan_tupian'] = list( + RefundEvidenceImage.query.filter(OrderID=dingdan_id) + .exclude(ImageURL__isnull=True) + .exclude(ImageURL='') + .values_list('ImageURL', flat=True) + ) + except Exception: + response_data['tuikuan_tupian'] = [] + try: from jituan.services.order_deal import order_auto_settle_payload response_data.update(order_auto_settle_payload(order)) @@ -1189,6 +1200,11 @@ class ShangjiaTuikuanShenqingView(APIView): data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() tuikuan_liyou = data.get('tuikuan_liyou', '').strip() + tuikuan_tupian_urls = ( + data.get('tuikuan_tupian_urls') + or data.get('evidence_urls') + or [] + ) except Exception: return Response({ 'code': 3, @@ -1196,13 +1212,29 @@ class ShangjiaTuikuanShenqingView(APIView): 'data': None }, status=status.HTTP_400_BAD_REQUEST) - # 4. 参数验证 + if not isinstance(tuikuan_tupian_urls, list): + tuikuan_tupian_urls = [] + valid_urls = [str(u).strip() for u in tuikuan_tupian_urls if u and str(u).strip()] + + # 4. 参数验证(文字 + 至少1张证据图) if not dingdan_id or not tuikuan_liyou: return Response({ 'code': 4, 'msg': '订单ID和退款原因不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) + if not valid_urls: + return Response({ + 'code': 4, + 'msg': '请至少上传1张退款相关图片', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + if len(valid_urls) > 9: + return Response({ + 'code': 4, + 'msg': '退款图片最多9张', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) try: # 5. 使用事务确保数据一致性 @@ -1258,6 +1290,17 @@ class ShangjiaTuikuanShenqingView(APIView): refund_record.save(update_fields=['Reason', 'UpdateTime']) logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") + # 8b. 退款证据图:覆盖写入(重新申请时替换旧图) + RefundEvidenceImage.query.filter(OrderID=dingdan_id).delete() + RefundEvidenceImage.objects.bulk_create([ + RefundEvidenceImage( + OrderID=dingdan_id, + UserID=current_user.UserUID, + ImageURL=url, + ) + for url in valid_urls + ]) + # 9. 更新订单状态为4(退款中) dingdan.Status = 4 dingdan.RefundReason = tuikuan_liyou @@ -1294,6 +1337,7 @@ class ShangjiaTuikuanShenqingView(APIView): 'dingdan_id': dingdan_id, 'new_zhuangtai': 4, 'tuikuan_liyou': tuikuan_liyou, + 'tuikuan_tupian': valid_urls, 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0, 'dashou_id': dingdan.PlayerID } diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index 6bde545..e203470 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -1641,13 +1641,24 @@ class KefuGetOrderDetailView(APIView): logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}") response_data['dashou_images'] = [] - # 7. 退款理由 + # 7. 退款理由 + 证据图 try: tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first() response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else '' except Exception as e: logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}") response_data['tuikuan_liyou'] = '' + try: + from orders.models import RefundEvidenceImage + response_data['tuikuan_tupian'] = list( + RefundEvidenceImage.query.filter(OrderID=dingdan_id) + .exclude(ImageURL__isnull=True) + .exclude(ImageURL='') + .values_list('ImageURL', flat=True) + ) + except Exception as e: + logger.exception(f"[kefuhqddxq] 退款图片查询异常 dingdan_id={dingdan_id}") + response_data['tuikuan_tupian'] = [] # 8. 指定打手响应 ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}