Files
Django/users/views/kefu_grab_requirement.py
XingQue af513738a5 feat: 客服改平台订单/商品抢单要求(002ad 隔离接口)
仅状态1/7可改订单;批量只改本店类型下商品三字段;各俱乐部闸门隔离无白名单特殊化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 07:48:11 +08:00

307 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""客服改抢单要求(会员/押金门槛)— 隔离新接口,不影响旧抢单/支付主路径。"""
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})