feat: 商家退款申请支持证据图片

新增 tuikuantupian 表;申请退款必传图片;打手详情与客服订单详情返回退款图。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 07:12:43 +08:00
parent bf4fd468e8
commit 69b16848c4
5 changed files with 165 additions and 4 deletions

View File

@@ -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'),
],
},
),
]

View File

@@ -17,6 +17,7 @@
- **PenaltyRecord** — 处罚记录 - **PenaltyRecord** — 处罚记录
- **PenaltyEvidenceImage** — 处罚证据图片 - **PenaltyEvidenceImage** — 处罚证据图片
- **RefundRecord** — 退款记录 - **RefundRecord** — 退款记录
- **RefundEvidenceImage** — 退款申请证据图片
- **PlatformCashFlow** — 平台流水统计 - **PlatformCashFlow** — 平台流水统计
- **ChatSession** — 聊天会话 - **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): class RefundRecord(QModel):
"""退款记录表""" """退款记录表"""

View File

@@ -834,6 +834,33 @@ class DashouCOSZhengshuView(APIView):
resolve_penalty_evidence_upload_auth(request, dingdan_id, fadan_id=fadan_id) resolve_penalty_evidence_upload_auth(request, dingdan_id, fadan_id=fadan_id)
except PenaltyEvidenceAuthError as e: except PenaltyEvidenceAuthError as e:
return Response({'code': e.code, 'msg': e.msg}, status=403) 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: else:
# 默认为订单相关,需要验证订单且打手必须接单 # 默认为订单相关,需要验证订单且打手必须接单
try: try:
@@ -881,6 +908,10 @@ class DashouCOSZhengshuView(APIView):
resources.append( resources.append(
f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*" 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: else:
resources.append( resources.append(
f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*"
@@ -920,7 +951,8 @@ class DashouCOSZhengshuView(APIView):
# 5. 构建响应 # 5. 构建响应
current_time = int(time.time()) current_time = int(time.time())
upload_dir = ( 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 ( 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else (
'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/'
) )
@@ -1325,6 +1357,16 @@ class DashouDingdanXiangqingView(APIView):
'fuwudashou_id': dingdan.PlayerID or '', 'fuwudashou_id': dingdan.PlayerID or '',
'fadanpingtai': dingdan.Platform 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: try:
from jituan.services.order_deal import order_auto_settle_payload from jituan.services.order_deal import order_auto_settle_payload
dingdan_data.update(order_auto_settle_payload(dingdan)) dingdan_data.update(order_auto_settle_payload(dingdan))

View File

@@ -70,7 +70,7 @@ from orders.notice_tasks import dingdan_guangbo
from ..models import ( from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage, Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate, 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_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_context import resolve_club_id_from_request
@@ -735,6 +735,7 @@ class ShangjiaDingdanXiangqingView(APIView):
# 商家扩展字段(旧) # 商家扩展字段(旧)
'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '', 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
'tkliyou': order.RefundReason or '', 'tkliyou': order.RefundReason or '',
'tuikuan_liyou': order.RefundReason or '',
'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款
'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0, 'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
'bhliyou': shangjia_kuozhan.RejectReason or '', 'bhliyou': shangjia_kuozhan.RejectReason or '',
@@ -754,6 +755,16 @@ class ShangjiaDingdanXiangqingView(APIView):
'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '', '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: try:
from jituan.services.order_deal import order_auto_settle_payload from jituan.services.order_deal import order_auto_settle_payload
response_data.update(order_auto_settle_payload(order)) response_data.update(order_auto_settle_payload(order))
@@ -1189,6 +1200,11 @@ class ShangjiaTuikuanShenqingView(APIView):
data = request.data data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
tuikuan_liyou = data.get('tuikuan_liyou', '').strip() tuikuan_liyou = data.get('tuikuan_liyou', '').strip()
tuikuan_tupian_urls = (
data.get('tuikuan_tupian_urls')
or data.get('evidence_urls')
or []
)
except Exception: except Exception:
return Response({ return Response({
'code': 3, 'code': 3,
@@ -1196,13 +1212,29 @@ class ShangjiaTuikuanShenqingView(APIView):
'data': None 'data': None
}, status=status.HTTP_400_BAD_REQUEST) }, 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: if not dingdan_id or not tuikuan_liyou:
return Response({ return Response({
'code': 4, 'code': 4,
'msg': '订单ID和退款原因不能为空', 'msg': '订单ID和退款原因不能为空',
'data': None 'data': None
}, status=status.HTTP_400_BAD_REQUEST) }, 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: try:
# 5. 使用事务确保数据一致性 # 5. 使用事务确保数据一致性
@@ -1258,6 +1290,17 @@ class ShangjiaTuikuanShenqingView(APIView):
refund_record.save(update_fields=['Reason', 'UpdateTime']) refund_record.save(update_fields=['Reason', 'UpdateTime'])
logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") 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退款中 # 9. 更新订单状态为4退款中
dingdan.Status = 4 dingdan.Status = 4
dingdan.RefundReason = tuikuan_liyou dingdan.RefundReason = tuikuan_liyou
@@ -1294,6 +1337,7 @@ class ShangjiaTuikuanShenqingView(APIView):
'dingdan_id': dingdan_id, 'dingdan_id': dingdan_id,
'new_zhuangtai': 4, 'new_zhuangtai': 4,
'tuikuan_liyou': tuikuan_liyou, 'tuikuan_liyou': tuikuan_liyou,
'tuikuan_tupian': valid_urls,
'jine': float(dingdan.Amount) if dingdan.Amount else 0.0, 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0,
'dashou_id': dingdan.PlayerID 'dashou_id': dingdan.PlayerID
} }

View File

@@ -1641,13 +1641,24 @@ class KefuGetOrderDetailView(APIView):
logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}") logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
response_data['dashou_images'] = [] response_data['dashou_images'] = []
# 7. 退款理由 # 7. 退款理由 + 证据图
try: try:
tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first() 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 '' response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
except Exception as e: except Exception as e:
logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}") logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
response_data['tuikuan_liyou'] = '' 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. 指定打手响应 # 8. 指定打手响应
ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'} ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}