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** — 处罚记录
|
||||
- **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):
|
||||
"""退款记录表"""
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: '等会接'}
|
||||
|
||||
Reference in New Issue
Block a user