"""客服改抢单要求(会员/押金门槛)— 隔离新接口,不影响旧抢单/支付主路径。""" 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})