734 lines
30 KiB
Python
734 lines
30 KiB
Python
import hmac
|
||
import threading
|
||
import traceback
|
||
import uuid
|
||
import hashlib
|
||
import xmltodict
|
||
import time
|
||
import logging
|
||
import requests
|
||
import os
|
||
import secrets
|
||
import random
|
||
import string
|
||
from calendar import monthrange
|
||
from decimal import Decimal
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.core.paginator import Paginator
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
# 工具类导入
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action,
|
||
update_shangjia_daily
|
||
)
|
||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||
from jituan.services.club_penalty import (
|
||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||
)
|
||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from ..utils import (
|
||
verify_kefu_permission,
|
||
PERMISSION_TO_SHENFEN,
|
||
SHENFEN_PROFILE_MAP,
|
||
has_fadan_view_permission,
|
||
has_merchant_order_permission,
|
||
check_fadan_permission,
|
||
pick_order_id,
|
||
pick_penalty_create_params,
|
||
write_xiugai_log,
|
||
XIUGAI_LEIXING_DASHOU,
|
||
XIUGAI_LEIXING_GUANSHI,
|
||
XIUGAI_LEIXING_SHANGJIA,
|
||
XIUGAI_LEIXING_ZUZHANG,
|
||
XIUGAI_LEIXING_LABEL,
|
||
)
|
||
|
||
# models 集中导入
|
||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||
## backend
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
## users
|
||
from users.models import (
|
||
UserGuanshi, UserBoss, UserZuzhang,
|
||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||
)
|
||
from users.business_models import User
|
||
|
||
## orders
|
||
from orders.models import (
|
||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||
OrderPlayerHistory, Order, RefundRecord,
|
||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||
)
|
||
|
||
## shop
|
||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
## products
|
||
from products.models import (
|
||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||
)
|
||
|
||
## config
|
||
from config.models import (
|
||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||
TixianQuotaDefault,
|
||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||
)
|
||
|
||
## rank
|
||
from rank.models import (
|
||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||
)
|
||
|
||
# 序列化器
|
||
from ..serializers import PopupPageSerializer
|
||
|
||
# 全局常量、日志对象
|
||
logger = logging.getLogger('houtai')
|
||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||
|
||
class FaKuanTongJiView(APIView):
|
||
"""
|
||
罚款统计接口
|
||
POST /houtai/htglyhqcfsltj
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
if not has_fadan_view_permission(permissions, username):
|
||
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
|
||
|
||
from utils.penalty_status import penalty_platform_audit_pending_q
|
||
pending_q = penalty_platform_audit_pending_q()
|
||
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
|
||
total=Count('id'),
|
||
daijiaona=Count('id', filter=Q(Status=1)),
|
||
shensuzhong=Count('id', filter=Q(Status=3)),
|
||
yijiaona=Count('id', filter=Q(Status=2)),
|
||
yibohui=Count('id', filter=Q(Status=4)),
|
||
pingtai_shenhe=Count('id', filter=pending_q),
|
||
)
|
||
data = {k: v or 0 for k, v in stats.items()}
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
|
||
except Exception as e:
|
||
logger.error(f"罚款统计异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanLieBiaoView(APIView):
|
||
"""
|
||
罚款列表接口
|
||
POST /houtai/hthqfklb
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if not has_fadan_view_permission(permissions, username):
|
||
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
|
||
|
||
page = int(request.data.get('page', 1))
|
||
page_size = int(request.data.get('page_size', 20))
|
||
if page_size > 100:
|
||
page_size = 100
|
||
|
||
qs = filter_penalty_qs(Penalty.query.all(), request)
|
||
|
||
# 状态筛选(5=平台审核中,仅 zhuangtai=5)
|
||
from utils.penalty_status import penalty_platform_audit_pending_q
|
||
zhuangtai_raw = request.data.get('zhuangtai')
|
||
if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '':
|
||
try:
|
||
zt = int(zhuangtai_raw)
|
||
if zt == 5:
|
||
qs = qs.filter(penalty_platform_audit_pending_q())
|
||
elif zt in (1, 2, 3, 4):
|
||
qs = qs.filter(Status=zt)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 被处罚人ID
|
||
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
|
||
if PenalizedUserID:
|
||
qs = qs.filter(PenalizedUserID=PenalizedUserID)
|
||
|
||
# 被处罚人身份
|
||
beichufa_shenfen = request.data.get('beichufa_shenfen')
|
||
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
|
||
qs = qs.filter(Identity=int(beichufa_shenfen))
|
||
|
||
# 模糊搜索(订单ID、被罚人ID、申请人ID)
|
||
sousuo = request.data.get('sousuo', '').strip()
|
||
if sousuo:
|
||
qs = qs.filter(
|
||
Q(RelatedOrderID__icontains=sousuo) |
|
||
Q(PenalizedUserID__icontains=sousuo) |
|
||
Q(ApplicantID__icontains=sousuo)
|
||
)
|
||
|
||
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': [], 'total': 0, 'page': page, 'page_size': page_size}
|
||
})
|
||
|
||
records = list(page_obj)
|
||
penalty_ids = [r.id for r in records]
|
||
|
||
# 批量获取图片并分组
|
||
zhengju_map = {}
|
||
shensu_map = {}
|
||
if penalty_ids:
|
||
tupians = PenaltyAppealImage.query.filter(
|
||
Penalty_id__in=penalty_ids
|
||
).values('Penalty_id', 'ImageURL', 'Purpose')
|
||
for t in tupians:
|
||
fid = t['Penalty_id']
|
||
url = t['ImageURL']
|
||
if t['Purpose'] == 1:
|
||
zhengju_map.setdefault(fid, []).append(url)
|
||
else:
|
||
shensu_map.setdefault(fid, []).append(url)
|
||
|
||
from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending
|
||
|
||
# 组装列表数据
|
||
results = []
|
||
for r in records:
|
||
results.append({
|
||
'id': r.id,
|
||
'beichufa_id': r.PenalizedUserID,
|
||
'shenfen': r.Identity,
|
||
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
|
||
'shenqing_chufa': r.ApplicantID or '',
|
||
'shenqingren_shenfen': r.ApplicantIdentity,
|
||
'chufaliyou': r.Reason or '',
|
||
'fakuanjine': str(r.FineAmount),
|
||
'guanliandingdan_id': r.RelatedOrderID or '',
|
||
'zhuangtai': r.Status,
|
||
'zhuangtai_text': penalty_display_status(r),
|
||
'platform_audit_pending': is_penalty_platform_audit_pending(r),
|
||
'yingxiang_qiangdan': r.AffectsGrabbing,
|
||
'shensuliyou': r.AppealReason or '',
|
||
'bohuiliyou': r.RejectReason or '',
|
||
'chulizhe': r.ProcessorID or '',
|
||
'chulizhe_shenfen': r.ProcessorIdentity,
|
||
'zhengju_tupian': zhengju_map.get(r.id, []),
|
||
'shensu_tupian': shensu_map.get(r.id, []),
|
||
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
'create_time': 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 '',
|
||
})
|
||
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({
|
||
'code': 0, 'msg': '成功',
|
||
'data': {
|
||
'list': results,
|
||
'total': paginator.count,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
**list_response_meta(request),
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"罚款列表异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanChuLiView(APIView):
|
||
"""
|
||
处理罚款申诉(同意/拒绝)
|
||
POST /houtai/glyclfk
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||
action = request.data.get('action', '').strip()
|
||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||
|
||
if not all([username, penalty_id, action]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||
except Penalty.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||
|
||
deny = forbid_penalty_out_of_scope(request, penalty)
|
||
if deny:
|
||
return deny
|
||
|
||
if penalty.Status != 3:
|
||
return Response({'code': 400, 'msg': '当前状态不可处理'})
|
||
|
||
if not check_fadan_permission(permissions, penalty.Identity):
|
||
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||
|
||
# 处理上传的图片
|
||
uploaded_urls = []
|
||
files = request.FILES.getlist('tupian') or []
|
||
if not files:
|
||
single = request.FILES.get('tupian')
|
||
if single:
|
||
files = [single]
|
||
for file in files:
|
||
valid, msg = validate_image(file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||
file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}"
|
||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||
url = upload_to_oss(file, file_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
uploaded_urls.append(file_path)
|
||
|
||
# 更新状态
|
||
if action == 'agree': # 同意申诉 → 驳回处罚
|
||
penalty.Status = 4
|
||
penalty.RejectReason = chuli_liyou
|
||
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
|
||
penalty.Status = 1
|
||
penalty.RejectReason = chuli_liyou
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||
|
||
# 记录处理者(使用 request.user 的 phone)
|
||
penalty.ProcessorID = request.user.Phone
|
||
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
|
||
penalty.save()
|
||
|
||
# 保存图片(证据图片,用途1)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty_id=penalty_id,
|
||
PenalizedUserID=penalty.PenalizedUserID,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
|
||
logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}")
|
||
return Response({'code': 0, 'msg': '处理成功'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款处理异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanPingTaiShenHeView(APIView):
|
||
"""
|
||
平台审核商家提交的罚款(同意/驳回)
|
||
POST /houtai/glyptshfk
|
||
action: platform_approve | platform_reject
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
from utils.penalty_status import (
|
||
PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED,
|
||
)
|
||
username = request.data.get('username', '').strip()
|
||
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||
action = request.data.get('action', '').strip()
|
||
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||
|
||
if not all([username, penalty_id, action]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||
except Penalty.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '罚单不存在'})
|
||
|
||
deny = forbid_penalty_out_of_scope(request, penalty)
|
||
if deny:
|
||
return deny
|
||
|
||
if penalty.Status != PENALTY_PLATFORM_AUDIT:
|
||
return Response({'code': 400, 'msg': '当前状态不可平台审核'})
|
||
|
||
if not check_fadan_permission(permissions, penalty.Identity):
|
||
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||
|
||
if action == 'platform_approve':
|
||
if penalty.AppealReason:
|
||
penalty.Status = PENALTY_APPEALING
|
||
else:
|
||
penalty.Status = PENALTY_PENDING_PAY
|
||
penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or ''
|
||
elif action == 'platform_reject':
|
||
penalty.Status = PENALTY_REJECTED
|
||
penalty.RejectReason = chuli_liyou or '平台审核驳回'
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||
|
||
penalty.ProcessorID = request.user.Phone
|
||
penalty.ProcessorIdentity = 3
|
||
penalty.save()
|
||
|
||
logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}")
|
||
return Response({'code': 0, 'msg': '平台审核完成'})
|
||
|
||
except Exception as e:
|
||
logger.error(f"罚款平台审核异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FaKuanChuangJianView(APIView):
|
||
"""
|
||
管理员创建罚款
|
||
POST /houtai/htfksc
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||
shenfen = request.data.get('shenfen')
|
||
|
||
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
shenfen = int(shenfen)
|
||
FineAmount = float(FineAmount)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '参数格式错误'})
|
||
|
||
if shenfen not in [1, 2, 3, 4]:
|
||
return Response({'code': 400, 'msg': '无效的被罚人身份'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
if not check_fadan_permission(permissions, shenfen):
|
||
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
|
||
|
||
# 验证用户是否存在对应的扩展表
|
||
try:
|
||
user = User.query.get(UserUID=PenalizedUserID)
|
||
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
|
||
if not profile_attr or not hasattr(user, profile_attr):
|
||
return Response({'code': 400, 'msg': '该用户不是所选身份'})
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '用户不存在'})
|
||
|
||
# 处理图片上传
|
||
uploaded_urls = []
|
||
files = request.FILES.getlist('tupian') or []
|
||
if not files:
|
||
single = request.FILES.get('tupian')
|
||
if single:
|
||
files = [single]
|
||
for file in files:
|
||
valid, msg = validate_image(file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||
url = upload_to_oss(file, file_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
uploaded_urls.append(file_path)
|
||
|
||
with transaction.atomic():
|
||
penalty = Penalty.query.create(
|
||
PenalizedUserID=PenalizedUserID,
|
||
Identity=shenfen,
|
||
ApplicantID=request.user.Phone, # 申请人(客服)手机号
|
||
ApplicantIdentity=2, # 售后身份
|
||
Reason=Reason,
|
||
FineAmount=FineAmount,
|
||
AffectsGrabbing=AffectsGrabbing,
|
||
Status=1, # 待缴纳
|
||
ClubID=resolve_penalty_club_id(
|
||
penalized_user_id=PenalizedUserID,
|
||
request=request,
|
||
user=request.user,
|
||
),
|
||
)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty=penalty,
|
||
PenalizedUserID=PenalizedUserID,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||
lock_penalty_bonus(penalty)
|
||
|
||
logger.info(f"罚款创建成功: {penalty.id}")
|
||
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建罚款异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class FineApplyView(APIView):
|
||
"""
|
||
客服罚款申请接口(基于订单对打手发起罚款)
|
||
POST /houtai/kffkdssq
|
||
权限: JWT认证 + 客服权限验证
|
||
|
||
请求参数:
|
||
username (必填) : 客服用户名(手机号)
|
||
OrderID (必填) : 订单ID
|
||
Reason (必填) : 罚款原因
|
||
FineAmount (必填) : 罚款金额(元)
|
||
AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
OrderID = pick_order_id(request.data)
|
||
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||
|
||
if not all([username, OrderID, Reason, FineAmount is not None]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
try:
|
||
FineAmount = float(FineAmount)
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '罚款金额格式错误'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 验证订单存在
|
||
try:
|
||
order = Order.query.get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 获取打手ID
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||
|
||
# 重复罚款检查
|
||
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
|
||
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||
|
||
# 检查权限(打手身份 shenfen=1)
|
||
if not check_fadan_permission(permissions, 1):
|
||
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||
|
||
# 处理图片上传
|
||
uploaded_urls = []
|
||
files = request.FILES.getlist('tupian') or []
|
||
if not files:
|
||
single = request.FILES.get('tupian')
|
||
if single:
|
||
files = [single]
|
||
for file in files:
|
||
valid, msg = validate_image(file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||
url = upload_to_oss(file, file_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
uploaded_urls.append(file_path)
|
||
|
||
# 创建罚单
|
||
with transaction.atomic():
|
||
penalty = Penalty.query.create(
|
||
PenalizedUserID=dashou_id,
|
||
ApplicantID=request.user.Phone,
|
||
Identity=1, # 被处罚人身份:1 打手
|
||
Reason=Reason,
|
||
FineAmount=FineAmount,
|
||
RelatedOrderID=OrderID,
|
||
Status=1, # 待缴纳
|
||
AffectsGrabbing=AffectsGrabbing,
|
||
ApplicantIdentity=1, # 申请人身份:1 客服
|
||
ClubID=resolve_penalty_club_id(
|
||
order=order,
|
||
request=request,
|
||
user=request.user,
|
||
applicant_id=getattr(request.user, 'UserUID', None),
|
||
),
|
||
)
|
||
for relative_url in uploaded_urls:
|
||
PenaltyAppealImage.query.create(
|
||
Penalty=penalty,
|
||
PenalizedUserID=dashou_id,
|
||
ImageURL=relative_url,
|
||
Purpose=1
|
||
)
|
||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||
lock_penalty_bonus(penalty)
|
||
|
||
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
|
||
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
class PunishDashouView(APIView):
|
||
"""
|
||
客服处罚打手接口(扣除积分)
|
||
POST /houtai/kpcf
|
||
权限: JWT认证 + 客服权限验证
|
||
|
||
请求参数:
|
||
username (必填) : 客服用户名(手机号)
|
||
OrderID (必填) : 订单ID
|
||
reason (可选) : 处罚原因
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username = request.data.get('username', '').strip()
|
||
OrderID = request.data.get('OrderID', '').strip()
|
||
reason = request.data.get('reason', '').strip()
|
||
|
||
if not all([username, OrderID]):
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
|
||
# 检查权限(打手身份 shenfen=1)
|
||
if not check_fadan_permission(permissions, 1):
|
||
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||
|
||
# 查询订单
|
||
try:
|
||
order = Order.query.get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 获取打手ID
|
||
dashou_id = order.PlayerID
|
||
if not dashou_id:
|
||
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
|
||
|
||
# 查询打手
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou = dashou_user.DashouProfile
|
||
except User.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '打手用户不存在'})
|
||
except ObjectDoesNotExist:
|
||
return Response({'code': 400, 'msg': '该用户不是打手'})
|
||
|
||
# 创建处罚记录(扣除积分5分)
|
||
jifen = 5
|
||
with transaction.atomic():
|
||
penalty_record = PenaltyRecord.query.create(
|
||
PlayerID=dashou_id,
|
||
ApplicantID=username,
|
||
PenaltyReason=reason or '',
|
||
ApplyStatus=0,
|
||
DeductedPoints=jifen,
|
||
OrderID=OrderID,
|
||
ClubID=resolve_penalty_record_club_id(
|
||
order_id=OrderID,
|
||
player_id=dashou_id,
|
||
request=request,
|
||
user=request.user,
|
||
applicant_id=getattr(request.user, 'UserUID', None) or username,
|
||
),
|
||
)
|
||
# 扣除打手积分
|
||
dashou.jifen = F('jifen') - jifen
|
||
dashou.save(update_fields=['jifen'])
|
||
|
||
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
|
||
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
|
||
|
||
except Exception as e:
|
||
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
|
||
return Response({'code': 500, 'msg': '系统繁忙'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|