From af513738a50e5792fd48120d9e662ada38e72bd6 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 29 Jul 2026 07:48:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A2=E6=9C=8D=E6=94=B9=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=AE=A2=E5=8D=95/=E5=95=86=E5=93=81=E6=8A=A2?= =?UTF-8?q?=E5=8D=95=E8=A6=81=E6=B1=82=EF=BC=88002ad=20=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅状态1/7可改订单;批量只改本店类型下商品三字段;各俱乐部闸门隔离无白名单特殊化。 Co-authored-by: Cursor --- jituan/constants.py | 3 + .../commands/seed_gvsdsdk_permissions.py | 6 + users/urls.py | 5 +- users/views/__init__.py | 5 + users/views/kefu_grab_requirement.py | 306 ++++++++++++++++++ users/views/kefu_orders.py | 8 +- 6 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 users/views/kefu_grab_requirement.py diff --git a/jituan/constants.py b/jituan/constants.py index 8169224..2f997ee 100644 --- a/jituan/constants.py +++ b/jituan/constants.py @@ -29,6 +29,9 @@ ADMIN_SCOPE_CLUB = 'CLUB' # 集团最高权限账号(写死超级管理员,拥有全部功能权限与集团视图) SUPER_ADMIN_PHONES = frozenset({'1383714110'}) +# 权限码:修改平台订单/商品抢单要求(须角色显式绑定,不随本店超管 000001 自动解锁) +PERM_GRAB_REQUIREMENT_EDIT = '002ad' + # —— 支付通道 —— PAY_CHANNEL_WECHAT_JSAPI = 'wechat_jsapi' PAY_CHANNEL_WECHAT_V3 = 'wechat_v3' diff --git a/jituan/management/commands/seed_gvsdsdk_permissions.py b/jituan/management/commands/seed_gvsdsdk_permissions.py index e87863c..378d1db 100644 --- a/jituan/management/commands/seed_gvsdsdk_permissions.py +++ b/jituan/management/commands/seed_gvsdsdk_permissions.py @@ -74,6 +74,12 @@ EXTRA_PERMISSIONS = [ '配置打手/管事/组长用户协议并发布版本', '小程序配置', ), + ( + '002ad', + '修改抢单要求', + '修改平台订单/本店商品的抢单要求类型(会员/押金门槛)。须显式绑定;本店超管 000001 不会自动拥有。', + '订单管理', + ), # ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ---------- ('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'), ('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'), diff --git a/users/urls.py b/users/urls.py index ed72ee4..cb882f3 100644 --- a/users/urls.py +++ b/users/urls.py @@ -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='客服修改打手数据'), diff --git a/users/views/__init__.py b/users/views/__init__.py index efa5e75..02486be 100644 --- a/users/views/__init__.py +++ b/users/views/__init__.py @@ -123,6 +123,11 @@ from .kefu_orders import ( KefuTransferHallView, ) +from .kefu_grab_requirement import ( + KefuBatchProductGrabRequirementView, + KefuUpdateOrderGrabRequirementView, +) + from .kefu_dashou import ( KefuGetDashouDetailView, KefuGetDashouListView, diff --git a/users/views/kefu_grab_requirement.py b/users/views/kefu_grab_requirement.py new file mode 100644 index 0000000..029d16d --- /dev/null +++ b/users/views/kefu_grab_requirement.py @@ -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}) diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index 8f3cd6e..668781a 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -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}")