feat: 商家退款申请支持证据图片
新增 tuikuantupian 表;申请退款必传图片;打手详情与客服订单详情返回退款图。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
31
orders/migrations/0018_refund_evidence_image.py
Normal file
31
orders/migrations/0018_refund_evidence_image.py
Normal 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'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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):
|
||||||
"""退款记录表"""
|
"""退款记录表"""
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '等会接'}
|
||||||
|
|||||||
Reference in New Issue
Block a user