feat: 客服改平台订单/商品抢单要求(002ad 隔离接口)
仅状态1/7可改订单;批量只改本店类型下商品三字段;各俱乐部闸门隔离无白名单特殊化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,7 +11,8 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP
|
||||
WeixinOfficialCallbackView,KefuLoginView, KefuStatsView, KefuGetOrderTypesView, KefuGetOrderListView, \
|
||||
KefuGetShangjiaOrderListView, KefuGetOrderDetailView, KefuChangeDashouView, KefuRecoverOrderView, \
|
||||
KefuPlatformRefundView, KefuRejectRefundView, KefuMerchantRefundView, KefuRejectSettlementView, \
|
||||
KefuTransferHallView, KefuCancelDesignationView, KefuGetDashouListView, KefuGetDashouDetailView, \
|
||||
KefuTransferHallView, KefuCancelDesignationView, KefuUpdateOrderGrabRequirementView, \
|
||||
KefuBatchProductGrabRequirementView, KefuGetDashouListView, KefuGetDashouDetailView, \
|
||||
KefuUpdateDashouView, KefuPunishmentListView, KefuPunishmentActionView, KefuPunishmentDetailView, \
|
||||
KefuWithdrawListView, KefuWithdrawDetailView, KefuWithdrawActionView, KefuPunishView, AdKfglView, AdKftjView, \
|
||||
AdKfxgView,GuanshiRegisterView, WechatLoginAndGuanshiRegisterView,ZuzhangZhuceView, ZuzhangXinxiView,KefuForceCompleteView,HuoQuYaoQingRenView, DashouJianquanView, LaobanJianquanView,\
|
||||
@@ -107,6 +108,8 @@ urlpatterns = [
|
||||
path('kefujjjs', KefuRejectSettlementView.as_view(), name='客服拒绝结算'),
|
||||
path('kefuzydt', KefuTransferHallView.as_view(), name='客服订单返回大厅'),
|
||||
path('kefuqxzd', KefuCancelDesignationView.as_view(), name='客服取消指定'),
|
||||
path('kefu_gai_qiangdan_yaoqiu', KefuUpdateOrderGrabRequirementView.as_view(), name='客服改订单抢单要求'),
|
||||
path('kefu_batch_qiangdan_yaoqiu', KefuBatchProductGrabRequirementView.as_view(), name='客服批量改商品抢单要求'),
|
||||
path('kefuhqdslb', KefuGetDashouListView.as_view(), name='客服获取打手数据'),
|
||||
path('kefuhqdsxq', KefuGetDashouDetailView.as_view(), name='客服获取打手具体信息'),
|
||||
path('kefuxgds', KefuUpdateDashouView.as_view(), name='客服修改打手数据'),
|
||||
|
||||
@@ -123,6 +123,11 @@ from .kefu_orders import (
|
||||
KefuTransferHallView,
|
||||
)
|
||||
|
||||
from .kefu_grab_requirement import (
|
||||
KefuBatchProductGrabRequirementView,
|
||||
KefuUpdateOrderGrabRequirementView,
|
||||
)
|
||||
|
||||
from .kefu_dashou import (
|
||||
KefuGetDashouDetailView,
|
||||
KefuGetDashouListView,
|
||||
|
||||
306
users/views/kefu_grab_requirement.py
Normal file
306
users/views/kefu_grab_requirement.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""客服改抢单要求(会员/押金门槛)— 隔离新接口,不影响旧抢单/支付主路径。"""
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.db import transaction
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from backend.utils import verify_kefu_permission, has_perm_code
|
||||
from jituan.constants import (
|
||||
CLUB_ID_DEFAULT,
|
||||
PERM_GRAB_REQUIREMENT_EDIT,
|
||||
)
|
||||
from jituan.models import AdminAuditLog
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from orders.models import Order
|
||||
from products.models import Huiyuan, Shangpin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_ORDER_STATUSES = frozenset({1, 7})
|
||||
GRAB_TYPE_MEMBER = 1
|
||||
GRAB_TYPE_DEPOSIT = 2
|
||||
|
||||
|
||||
def _client_ip(request):
|
||||
xff = (request.META.get('HTTP_X_FORWARDED_FOR') or '').split(',')[0].strip()
|
||||
return xff or (request.META.get('REMOTE_ADDR') or '')[:64]
|
||||
|
||||
|
||||
def _normalize_club_id(club_id):
|
||||
cid = (club_id or '').strip()
|
||||
return cid or CLUB_ID_DEFAULT
|
||||
|
||||
|
||||
def _require_002ad(request):
|
||||
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||||
kefu, permissions = verify_kefu_permission(request, phone or None)
|
||||
if kefu is None:
|
||||
return None, None, permissions
|
||||
if not has_perm_code(permissions, PERM_GRAB_REQUIREMENT_EDIT):
|
||||
return None, None, Response({
|
||||
'code': 403,
|
||||
'msg': f'您无权修改抢单要求(需要 {PERM_GRAB_REQUIREMENT_EDIT})',
|
||||
})
|
||||
return kefu, permissions, None
|
||||
|
||||
|
||||
def _parse_grab_payload(data):
|
||||
"""返回 (yaoqiuleixing, huiyuan_id, yongjin, error_response)。"""
|
||||
try:
|
||||
yaoqiu = int(data.get('yaoqiuleixing'))
|
||||
except (TypeError, ValueError):
|
||||
return None, None, None, Response({'code': 400, 'msg': '抢单要求类型无效(须为1会员或2押金)'})
|
||||
if yaoqiu not in (GRAB_TYPE_MEMBER, GRAB_TYPE_DEPOSIT):
|
||||
return None, None, None, Response({'code': 400, 'msg': '抢单要求类型无效(须为1会员或2押金)'})
|
||||
|
||||
if yaoqiu == GRAB_TYPE_MEMBER:
|
||||
huiyuan_id = (data.get('huiyuan_id') or '').strip()
|
||||
if not huiyuan_id:
|
||||
return None, None, None, Response({'code': 400, 'msg': '会员要求须填写会员ID'})
|
||||
if len(huiyuan_id) > 6:
|
||||
return None, None, None, Response({'code': 400, 'msg': '会员ID过长'})
|
||||
if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists():
|
||||
return None, None, None, Response({'code': 400, 'msg': f'会员ID不存在:{huiyuan_id}'})
|
||||
return yaoqiu, huiyuan_id, None, None
|
||||
|
||||
raw = data.get('yongjin', data.get('yajin'))
|
||||
if raw is None or raw == '':
|
||||
return None, None, None, Response({'code': 400, 'msg': '押金要求须填写押金金额'})
|
||||
try:
|
||||
yongjin = Decimal(str(raw)).quantize(Decimal('0.01'))
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return None, None, None, Response({'code': 400, 'msg': '押金金额格式错误'})
|
||||
if yongjin < 0:
|
||||
return None, None, None, Response({'code': 400, 'msg': '押金金额不能为负'})
|
||||
return yaoqiu, None, yongjin, None
|
||||
|
||||
|
||||
def _snapshot_order(order):
|
||||
return {
|
||||
'yaoqiuleixing': order.GrabRequirement,
|
||||
'huiyuan_id': order.MembershipID or '',
|
||||
'yongjin': str(order.CommissionReq) if order.CommissionReq is not None else None,
|
||||
}
|
||||
|
||||
|
||||
def _snapshot_product_fields(yaoqiu, huiyuan_id, yongjin):
|
||||
return {
|
||||
'yaoqiuleixing': yaoqiu,
|
||||
'huiyuan_id': huiyuan_id or '',
|
||||
'yongjin': str(yongjin) if yongjin is not None else None,
|
||||
}
|
||||
|
||||
|
||||
def _write_audit(*, request, club_id, action, resource_type, resource_id, before, after, remark=''):
|
||||
try:
|
||||
op = getattr(request.user, 'UserUID', None) or getattr(request.user, 'Phone', '') or ''
|
||||
AdminAuditLog.query.create(
|
||||
club_id=_normalize_club_id(club_id),
|
||||
operator_yonghuid=str(op)[:16],
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id)[:64],
|
||||
field_name='yaoqiuleixing,huiyuan_id,yongjin',
|
||||
value_before=json.dumps(before, ensure_ascii=False)[:2000],
|
||||
value_after=json.dumps(after, ensure_ascii=False)[:2000],
|
||||
request_ip=_client_ip(request),
|
||||
remark=(remark or '')[:500],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('抢单要求审计写入失败: %s', e)
|
||||
|
||||
|
||||
class KefuUpdateOrderGrabRequirementView(APIView):
|
||||
"""
|
||||
POST /yonghu/kefu_gai_qiangdan_yaoqiu
|
||||
改单笔平台订单抢单要求。权限 002ad;按俱乐部闸门各管各的;仅状态 1/7。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
kefu, _permissions, err = _require_002ad(request)
|
||||
if err:
|
||||
return err
|
||||
|
||||
dingdan_id = (request.data.get('dingdan_id') or '').strip()
|
||||
if not dingdan_id:
|
||||
return Response({'code': 400, 'msg': '缺少订单ID'})
|
||||
|
||||
yaoqiu, huiyuan_id, yongjin, perr = _parse_grab_payload(request.data)
|
||||
if perr:
|
||||
return perr
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
order = (
|
||||
Order.query.select_for_update()
|
||||
.filter(OrderID=dingdan_id)
|
||||
.first()
|
||||
)
|
||||
if not order:
|
||||
return Response({'code': 404, 'msg': '订单不存在'})
|
||||
|
||||
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
|
||||
scope_deny = forbid_if_order_mutate_out_of_scope(request, order)
|
||||
if scope_deny:
|
||||
return scope_deny
|
||||
|
||||
order_club = _normalize_club_id(getattr(order, 'ClubID', None))
|
||||
|
||||
if int(order.Platform or 0) != 1:
|
||||
return Response({'code': 400, 'msg': '仅平台订单可修改抢单要求'})
|
||||
|
||||
if int(order.Status or 0) not in ALLOWED_ORDER_STATUSES:
|
||||
return Response({
|
||||
'code': 400,
|
||||
'msg': '仅下单中(1)/指定中(7)的订单可修改抢单要求',
|
||||
})
|
||||
|
||||
before = _snapshot_order(order)
|
||||
order.GrabRequirement = yaoqiu
|
||||
if yaoqiu == GRAB_TYPE_MEMBER:
|
||||
order.MembershipID = huiyuan_id
|
||||
order.CommissionReq = None
|
||||
else:
|
||||
order.MembershipID = None
|
||||
order.CommissionReq = yongjin
|
||||
order.save(update_fields=[
|
||||
'GrabRequirement', 'MembershipID', 'CommissionReq', 'UpdateTime',
|
||||
])
|
||||
|
||||
# 并发接单后状态可能已变:再读一次,若已不在允许状态则回滚
|
||||
order.refresh_from_db()
|
||||
if int(order.Status or 0) not in ALLOWED_ORDER_STATUSES:
|
||||
raise RuntimeError('ORDER_STATUS_CHANGED')
|
||||
|
||||
after = _snapshot_order(order)
|
||||
phone = getattr(request.user, 'Phone', '') or ''
|
||||
_write_audit(
|
||||
request=request,
|
||||
club_id=order_club,
|
||||
action='grab_requirement_order',
|
||||
resource_type='order',
|
||||
resource_id=dingdan_id,
|
||||
before=before,
|
||||
after=after,
|
||||
remark=f'客服{phone}改平台订单抢单要求',
|
||||
)
|
||||
except RuntimeError as e:
|
||||
if str(e) == 'ORDER_STATUS_CHANGED':
|
||||
return Response({
|
||||
'code': 409,
|
||||
'msg': '订单状态已变更,无法修改抢单要求,请刷新后重试',
|
||||
})
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception('改订单抢单要求失败 dingdan_id=%s', dingdan_id)
|
||||
return Response({'code': 500, 'msg': '修改失败'})
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '修改成功',
|
||||
'data': {
|
||||
'dingdan_id': dingdan_id,
|
||||
'yaoqiuleixing': yaoqiu,
|
||||
'huiyuan_id': huiyuan_id or '',
|
||||
'yajin': float(yongjin) if yongjin is not None else None,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class KefuBatchProductGrabRequirementView(APIView):
|
||||
"""
|
||||
POST /yonghu/kefu_batch_qiangdan_yaoqiu
|
||||
预览/确认:批量改本店某商品类型下 Shangpin 的抢单要求字段。
|
||||
confirm 缺省/false 只预览;true 才写库。不碰全局 ShangpinLeixing。
|
||||
须切换到具体俱乐部(不跨店)。
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
kefu, _permissions, err = _require_002ad(request)
|
||||
if err:
|
||||
return err
|
||||
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
from jituan.services.club_context import resolve_club_scope
|
||||
|
||||
if resolve_club_scope(request) == DATA_SCOPE_ALL:
|
||||
return Response({
|
||||
'code': 403,
|
||||
'msg': '请切换到具体俱乐部后再批量修改商品抢单要求',
|
||||
})
|
||||
|
||||
club_id = _normalize_club_id(resolve_club_id_from_request(request))
|
||||
|
||||
try:
|
||||
leixing_id = int(request.data.get('leixing_id'))
|
||||
except (TypeError, ValueError):
|
||||
return Response({'code': 400, 'msg': '缺少或无效的商品类型ID'})
|
||||
|
||||
yaoqiu, huiyuan_id, yongjin, perr = _parse_grab_payload(request.data)
|
||||
if perr:
|
||||
return perr
|
||||
|
||||
confirm = request.data.get('confirm') in (True, 'true', '1', 1, 'on')
|
||||
|
||||
qs = Shangpin.query.filter(club_id=club_id, leixing_id=leixing_id)
|
||||
total = qs.count()
|
||||
samples = list(qs.order_by('id').values(
|
||||
'id', 'biaoqian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin',
|
||||
)[:5])
|
||||
for row in samples:
|
||||
if row.get('yongjin') is not None:
|
||||
row['yongjin'] = float(row['yongjin'])
|
||||
|
||||
preview = {
|
||||
'club_id': club_id,
|
||||
'leixing_id': leixing_id,
|
||||
'affected_count': total,
|
||||
'samples': samples,
|
||||
'will_set': _snapshot_product_fields(yaoqiu, huiyuan_id, yongjin),
|
||||
}
|
||||
|
||||
if not confirm:
|
||||
return Response({'code': 0, 'msg': '预览成功(未写入)', 'data': preview})
|
||||
|
||||
if total == 0:
|
||||
return Response({'code': 400, 'msg': '该俱乐部下该类型暂无商品可改'})
|
||||
|
||||
updates = {
|
||||
'yaoqiuleixing': yaoqiu,
|
||||
'huiyuan_id': huiyuan_id if yaoqiu == GRAB_TYPE_MEMBER else None,
|
||||
'yongjin': yongjin if yaoqiu == GRAB_TYPE_DEPOSIT else None,
|
||||
}
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
updated = Shangpin.query.filter(
|
||||
club_id=club_id, leixing_id=leixing_id,
|
||||
).update(**updates)
|
||||
phone = getattr(request.user, 'Phone', '') or ''
|
||||
_write_audit(
|
||||
request=request,
|
||||
club_id=club_id,
|
||||
action='grab_requirement_product_batch',
|
||||
resource_type='shangpin_leixing',
|
||||
resource_id=f'{club_id}:{leixing_id}',
|
||||
before={'affected_count': total, 'samples': samples},
|
||||
after={**updates, 'updated_count': updated},
|
||||
remark=f'客服{phone}批量改商品抢单要求 count={updated}',
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'批量改商品抢单要求失败 club=%s leixing=%s', club_id, leixing_id,
|
||||
)
|
||||
return Response({'code': 500, 'msg': '批量修改失败'})
|
||||
|
||||
preview['updated_count'] = updated
|
||||
return Response({'code': 0, 'msg': f'已更新 {updated} 个商品', 'data': preview})
|
||||
@@ -275,7 +275,10 @@ class KefuGetOrderListView(APIView):
|
||||
'jiage': float(order.Amount) if order.Amount else 0.00,
|
||||
'tupian_url': order.ImageURL or '',
|
||||
'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
|
||||
'youxi_nicheng': youxi_nicheng
|
||||
'youxi_nicheng': youxi_nicheng,
|
||||
'yaoqiuleixing': order.GrabRequirement,
|
||||
'huiyuan_id': order.MembershipID or '',
|
||||
'yajin': float(order.CommissionReq) if order.CommissionReq is not None else None,
|
||||
}
|
||||
_attach_order_club_fields(item, order, club_names)
|
||||
item.update(order_auto_settle_payload(order))
|
||||
@@ -1622,6 +1625,9 @@ class KefuGetOrderDetailView(APIView):
|
||||
'clkf': dingdan_obj.AssignedCS or '',
|
||||
'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
|
||||
'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
|
||||
'yaoqiuleixing': dingdan_obj.GrabRequirement,
|
||||
'huiyuan_id': dingdan_obj.MembershipID or '',
|
||||
'yajin': float(dingdan_obj.CommissionReq) if dingdan_obj.CommissionReq is not None else None,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
|
||||
|
||||
Reference in New Issue
Block a user