fix: 俱乐部超管可审本店提现与订单,禁跨店改单

放宽身份校验与权限推断,列表带回俱乐部信息,写操作按本店范围拦截。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 18:39:46 +08:00
parent 17b38d07a9
commit 955b76cbea
6 changed files with 193 additions and 52 deletions

View File

@@ -57,7 +57,9 @@ from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission, has_perm_code, has_merchant_order_permission,
has_platform_order_permission,
)
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
@@ -101,6 +103,29 @@ import logging
logger = logging.getLogger(__name__)
def _club_label_map(club_ids):
"""club_id -> 展示名"""
ids = [c for c in set(club_ids or []) if c]
if not ids:
return {}
try:
from jituan.models import Club
return {
c.club_id: (c.name or c.club_id)
for c in Club.objects.filter(club_id__in=ids)
}
except Exception:
return {cid: cid for cid in ids}
def _attach_order_club_fields(item, order, name_map=None):
cid = (getattr(order, 'ClubID', None) or '').strip()
item['club_id'] = cid
item['club_name'] = (name_map or {}).get(cid) or cid or ''
return item
class KefuGetOrderListView(APIView):
"""
客服获取平台订单列表接口(发单平台=1非跨平台
@@ -135,8 +160,8 @@ class KefuGetOrderListView(APIView):
if kefu is None:
return permissions
if not has_perm_code(permissions, '002ab'):
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
if not has_platform_order_permission(permissions):
return Response({'code': 403, 'msg': '您无权查看平台订单列表(需要 002ab 或本店超级管理 000001'})
# 4. 获取分页和筛选参数
page = int(request.data.get('page', 1))
@@ -235,6 +260,7 @@ class KefuGetOrderListView(APIView):
nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
from jituan.services.order_deal import order_auto_settle_payload
club_names = _club_label_map([(getattr(o, 'ClubID', None) or '') for o in orders_page])
result_list = []
for order in orders_page:
ext = ext_map.get(order.OrderID)
@@ -251,6 +277,7 @@ class KefuGetOrderListView(APIView):
'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
'youxi_nicheng': youxi_nicheng
}
_attach_order_club_fields(item, order, club_names)
item.update(order_auto_settle_payload(order))
result_list.append(item)
@@ -424,6 +451,7 @@ class KefuGetShangjiaOrderListView(APIView):
nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
from jituan.services.order_deal import order_auto_settle_payload
club_names = _club_label_map([(getattr(o, 'ClubID', None) or '') for o in orders_page])
result_list = []
for order in orders_page:
ext = ext_map.get(order.OrderID)
@@ -438,8 +466,11 @@ class KefuGetShangjiaOrderListView(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,
'shangjia_id': shangjia_id,
'shangjia_nicheng': nickname_map.get(shangjia_id, ''),
}
_attach_order_club_fields(item, order, club_names)
item.update(order_auto_settle_payload(order))
result_list.append(item)
@@ -494,11 +525,11 @@ class KefuChangeDashouView(APIView):
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -514,6 +545,11 @@ class KefuChangeDashouView(APIView):
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -638,10 +674,10 @@ class KefuRecoverOrderView(APIView):
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -655,6 +691,11 @@ class KefuRecoverOrderView(APIView):
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -706,11 +747,11 @@ class KefuPlatformRefundView(APIView):
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -728,6 +769,11 @@ class KefuPlatformRefundView(APIView):
with transaction.atomic():
order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
# 4. 验证平台订单
if order.Platform != 1:
return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
@@ -996,10 +1042,10 @@ class KefuRejectRefundView(APIView):
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1015,6 +1061,11 @@ class KefuRejectRefundView(APIView):
with transaction.atomic():
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
# 4. 验证订单状态为退款审核中4
if order.Status != 4:
return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
@@ -1158,10 +1209,10 @@ class KefuMerchantRefundView(APIView):
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1176,6 +1227,11 @@ class KefuMerchantRefundView(APIView):
# 查询订单,预加载商家扩展表
try:
order = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -1264,7 +1320,7 @@ class KefuMerchantRefundView(APIView):
# 管理员无客服扩展表,跳过计数器更新
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
if not is_admin:
kefu = current_user.KefuProfile
kefu.jinrichuli += 1
@@ -1305,11 +1361,11 @@ class KefuRejectSettlementView(APIView):
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1325,6 +1381,11 @@ class KefuRejectSettlementView(APIView):
# 3. 查询订单,预加载平台扩展表
try:
order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -1419,9 +1480,9 @@ class KefuTransferHallView(APIView):
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1434,6 +1495,11 @@ class KefuTransferHallView(APIView):
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -1490,9 +1556,9 @@ class KefuCancelDesignationView(APIView):
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1505,6 +1571,11 @@ class KefuCancelDesignationView(APIView):
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
@@ -1544,11 +1615,11 @@ class KefuGetOrderDetailView(APIView):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 验证用户类型
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服扩展表
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1570,15 +1641,25 @@ class KefuGetOrderDetailView(APIView):
logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
return Response({'code': 500, 'msg': '服务器错误'}, status=500)
from jituan.services.club_user_access import forbid_if_order_out_of_scope
from jituan.services.club_user_access import (
forbid_if_order_out_of_scope,
forbid_if_order_mutate_out_of_scope,
order_belongs_to_request,
)
deny = forbid_if_order_out_of_scope(request, dingdan_obj)
if deny:
return deny
_detail_can_mutate = forbid_if_order_mutate_out_of_scope(request, dingdan_obj) is None
# 4. 构建基础数据
try:
_cid = (getattr(dingdan_obj, 'ClubID', None) or '').strip()
_cname = _club_label_map([_cid]).get(_cid, _cid)
response_data = {
'dingdan_id': dingdan_obj.OrderID or '',
'club_id': _cid,
'club_name': _cname,
'can_mutate': bool(_detail_can_mutate and order_belongs_to_request(dingdan_obj, request)),
'zhuangtai': dingdan_obj.Status or 0,
'fadanpingtai': dingdan_obj.Platform or 0,
'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
@@ -1741,10 +1822,10 @@ class KefuForceCompleteView(APIView):
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'})
if current_user.UserType not in ('kefu', 'admin'):
if not is_kefu_backend_account(current_user):
return Response({'code': 401, 'msg': '认证失败'})
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
@@ -1760,6 +1841,11 @@ class KefuForceCompleteView(APIView):
with transaction.atomic():
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
_ms = forbid_if_order_mutate_out_of_scope(request, order)
if _ms:
return _ms
# 3. 校验订单状态
if order.Status != 8:
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})

View File

@@ -419,24 +419,25 @@ class KefuWithdrawDetailView(APIView):
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return Response({'code': 403, 'msg': '身份不匹配'}, status=status.HTTP_403_FORBIDDEN)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
if not is_kefu_backend_account(current_user):
logger.warning(f"非后台账号: {current_user.UserUID}")
return Response({'code': 403, 'msg': '非后台账号'}, status=status.HTTP_403_FORBIDDEN)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return Response({'code': 403, 'msg': '客服账号不存在'}, status=status.HTTP_403_FORBIDDEN)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return Response({'code': 403, 'msg': '客服账号已被禁用'}, status=status.HTTP_403_FORBIDDEN)
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, err_resp = require_withdraw_audit_access(request, phone)
@@ -539,22 +540,23 @@ class KefuWithdrawActionView(APIView):
parser_classes = [JSONParser]
def _verify_kefu(self, request, phone):
"""与 menu-access / verify_kefu_permission 对齐:按后台账号放行,不因 UserType 推断失败而 401。"""
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
if not phone:
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
return None, Response({'code': 400, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
return None, Response({'code': 403, 'msg': '身份不匹配'}, status=status.HTTP_403_FORBIDDEN)
if not is_kefu_backend_account(current_user):
return None, Response({'code': 403, 'msg': '非后台账号'}, status=status.HTTP_403_FORBIDDEN)
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return None, Response({'code': 403, 'msg': '客服账号不存在'}, status=status.HTTP_403_FORBIDDEN)
if kefu.zhuangtai != 1:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return None, Response({'code': 403, 'msg': '客服账号已被禁用'}, status=status.HTTP_403_FORBIDDEN)
return current_user, None
def _parse_items(self, request):