仅状态1/7可改订单;批量只改本店类型下商品三字段;各俱乐部闸门隔离无白名单特殊化。 Co-authored-by: Cursor <cursoragent@cursor.com>
307 lines
12 KiB
Python
307 lines
12 KiB
Python
"""客服改抢单要求(会员/押金门槛)— 隔离新接口,不影响旧抢单/支付主路径。"""
|
||
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})
|