fix: 俱乐部角色ORM过滤+商家订单权限002ab/003aa+legacy兜底+perm-debug
This commit is contained in:
@@ -7,6 +7,11 @@ from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
|
|||||||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||||
|
|
||||||
|
|
||||||
|
def _global_role_q():
|
||||||
|
"""集团全局角色:club_id 为空或 NULL。"""
|
||||||
|
return Q(ClubID='') | Q(ClubID__isnull=True)
|
||||||
|
|
||||||
|
|
||||||
def resolve_role_club_id(request, explicit=None):
|
def resolve_role_club_id(request, explicit=None):
|
||||||
"""创建/筛选角色时使用的俱乐部;集团视图可显式指定 target_club_id。"""
|
"""创建/筛选角色时使用的俱乐部;集团视图可显式指定 target_club_id。"""
|
||||||
if explicit:
|
if explicit:
|
||||||
@@ -27,7 +32,7 @@ def filter_roles_for_request(request):
|
|||||||
if scope == DATA_SCOPE_ALL:
|
if scope == DATA_SCOPE_ALL:
|
||||||
return qs
|
return qs
|
||||||
club_id = resolve_club_id_from_request(request)
|
club_id = resolve_club_id_from_request(request)
|
||||||
return qs.filter(ClubID=club_id)
|
return qs.filter(Q(ClubID=club_id) | _global_role_q())
|
||||||
|
|
||||||
|
|
||||||
def filter_kefu_users_by_admin_scope(qs, request):
|
def filter_kefu_users_by_admin_scope(qs, request):
|
||||||
@@ -87,15 +92,17 @@ def role_name_exists(role_name, club_id):
|
|||||||
|
|
||||||
|
|
||||||
def resolve_role_by_name(role_name, club_id=None):
|
def resolve_role_by_name(role_name, club_id=None):
|
||||||
"""按角色名解析 Role;优先匹配用户俱乐部,其次集团全局角色(club_id 为空)。"""
|
"""按角色名解析 Role;优先匹配指定俱乐部,其次全局角色(club_id 为空)。"""
|
||||||
name = (role_name or '').strip()
|
name = (role_name or '').strip()
|
||||||
if not name:
|
if not name:
|
||||||
return None
|
return None
|
||||||
qs = Role.objects.filter(RoleName=name, RoleStatus=1)
|
qs = Role.objects.filter(RoleName=name, RoleStatus=1)
|
||||||
cid = (club_id or '').strip()
|
cid = (club_id or '').strip()
|
||||||
if cid:
|
if cid:
|
||||||
return qs.filter(ClubID=cid).first()
|
hit = qs.filter(ClubID=cid).first()
|
||||||
return qs.filter(ClubID='').first()
|
if hit:
|
||||||
|
return hit
|
||||||
|
return qs.filter(_global_role_q()).first()
|
||||||
|
|
||||||
|
|
||||||
def role_uuid_bytes(role_uuid):
|
def role_uuid_bytes(role_uuid):
|
||||||
@@ -133,36 +140,22 @@ def resolve_roles_for_binding(role_names, club_id=None):
|
|||||||
def filter_role_uuids_for_club_scope(role_uuids, club_id, scope, allowed_club_ids=None):
|
def filter_role_uuids_for_club_scope(role_uuids, club_id, scope, allowed_club_ids=None):
|
||||||
"""
|
"""
|
||||||
按当前俱乐部上下文过滤 UserRole 绑定的 RoleUUID。
|
按当前俱乐部上下文过滤 UserRole 绑定的 RoleUUID。
|
||||||
SINGLE_CLUB:仅本俱乐部角色(Role.ClubID=当前俱乐部),不含其他俱乐部/全局。
|
SINGLE_CLUB:本俱乐部角色 + 全局角色(ClubID 空/NULL)。
|
||||||
ALL_CLUBS:已授权各俱乐部角色 + 全局角色(club_id 为空)。
|
ALL_CLUBS:已授权各俱乐部角色 + 全局角色。
|
||||||
"""
|
"""
|
||||||
if not role_uuids:
|
if not role_uuids:
|
||||||
return []
|
return []
|
||||||
wanted = {role_uuid_bytes(u) for u in role_uuids}
|
qs = Role.objects.filter(RoleUUID__in=role_uuids, RoleStatus=1)
|
||||||
|
|
||||||
if scope == DATA_SCOPE_ALL:
|
if scope == DATA_SCOPE_ALL:
|
||||||
club_ids = {c for c in (allowed_club_ids or []) if c}
|
club_ids = [c for c in (allowed_club_ids or []) if c]
|
||||||
matched = []
|
q = _global_role_q()
|
||||||
for role in Role.objects.filter(RoleStatus=1):
|
if club_ids:
|
||||||
key = role_uuid_bytes(role.RoleUUID)
|
q |= Q(ClubID__in=club_ids)
|
||||||
if key not in wanted:
|
qs = qs.filter(q)
|
||||||
continue
|
else:
|
||||||
rcid = (getattr(role, 'ClubID', None) or '').strip()
|
cid = (club_id or CLUB_ID_DEFAULT).strip()
|
||||||
if rcid == '' or rcid in club_ids:
|
qs = qs.filter(Q(ClubID=cid) | _global_role_q())
|
||||||
matched.append(role.RoleUUID)
|
return list(qs.values_list('RoleUUID', flat=True))
|
||||||
return matched
|
|
||||||
|
|
||||||
cid = (club_id or CLUB_ID_DEFAULT).strip()
|
|
||||||
matched = []
|
|
||||||
for role in Role.objects.filter(RoleStatus=1):
|
|
||||||
key = role_uuid_bytes(role.RoleUUID)
|
|
||||||
if key not in wanted:
|
|
||||||
continue
|
|
||||||
rcid = (getattr(role, 'ClubID', None) or '').strip()
|
|
||||||
# 本俱乐部角色 + 全局角色;不含其他俱乐部角色
|
|
||||||
if rcid == cid or rcid == '':
|
|
||||||
matched.append(role.RoleUUID)
|
|
||||||
return matched
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_permission_codes_for_club(user, club_id, allowed_club_ids=None, scope=None):
|
def resolve_permission_codes_for_club(user, club_id, allowed_club_ids=None, scope=None):
|
||||||
@@ -191,8 +184,7 @@ def resolve_permission_codes_for_club(user, club_id, allowed_club_ids=None, scop
|
|||||||
|
|
||||||
def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=None):
|
def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=None):
|
||||||
"""
|
"""
|
||||||
菜单与各业务接口统一权限码:当前俱乐部 gvsdsdk 角色。
|
菜单与各业务接口统一权限码:当前俱乐部 gvsdsdk 角色 + 过渡期 legacy 兜底。
|
||||||
gvsdsdk 完全无绑定时才兜底旧 user_role 表(旧表不区分俱乐部,避免多俱乐部串权)。
|
|
||||||
"""
|
"""
|
||||||
from backend.utils import _legacy_backend_role_permissions
|
from backend.utils import _legacy_backend_role_permissions
|
||||||
from gvsdsdk.models import UserRole as SdkUserRole
|
from gvsdsdk.models import UserRole as SdkUserRole
|
||||||
@@ -202,14 +194,54 @@ def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=No
|
|||||||
)
|
)
|
||||||
if codes:
|
if codes:
|
||||||
return codes
|
return codes
|
||||||
if SdkUserRole.objects.filter(UserUUID=user.UserUUID).exists():
|
|
||||||
return []
|
|
||||||
legacy_codes, _ = _legacy_backend_role_permissions(getattr(user, 'Phone', ''))
|
legacy_codes, _ = _legacy_backend_role_permissions(getattr(user, 'Phone', ''))
|
||||||
if legacy_codes:
|
if not legacy_codes:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not SdkUserRole.objects.filter(UserUUID=user.UserUUID).exists():
|
||||||
|
return list(legacy_codes)
|
||||||
|
|
||||||
|
# gvsdsdk 有绑定但当前俱乐部视图下无权限码:单俱乐部账号仍走 legacy(未迁移完)
|
||||||
|
allowed = {c for c in (allowed_club_ids or []) if c}
|
||||||
|
if len(allowed) <= 1:
|
||||||
return list(legacy_codes)
|
return list(legacy_codes)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def describe_roles_in_scope(user, club_id, scope, allowed_club_ids=None):
|
||||||
|
"""诊断:当前俱乐部上下文中生效的角色与权限码。"""
|
||||||
|
from gvsdsdk.models import Permission as SdkPermission, RolePermission as SdkRolePermission, UserRole as SdkUserRole
|
||||||
|
|
||||||
|
all_bound = list(SdkUserRole.objects.filter(
|
||||||
|
UserUUID=user.UserUUID,
|
||||||
|
).values_list('RoleUUID', flat=True))
|
||||||
|
in_scope = filter_role_uuids_for_club_scope(
|
||||||
|
all_bound, club_id, scope, allowed_club_ids,
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for role in Role.objects.filter(RoleUUID__in=in_scope, RoleStatus=1):
|
||||||
|
rcid = (getattr(role, 'ClubID', None) or '').strip() or '(全局)'
|
||||||
|
perm_uuids = SdkRolePermission.objects.filter(
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
).values_list('PermUUID', flat=True)
|
||||||
|
perm_codes = list(
|
||||||
|
SdkPermission.objects.filter(
|
||||||
|
PermUUID__in=perm_uuids, PermStatus=1,
|
||||||
|
).values_list('PermCode', flat=True)
|
||||||
|
)
|
||||||
|
rows.append({
|
||||||
|
'role_name': role.RoleName,
|
||||||
|
'club_id': rcid,
|
||||||
|
'perm_codes': perm_codes,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'bound_role_count': len(all_bound),
|
||||||
|
'in_scope_role_count': len(in_scope),
|
||||||
|
'roles': rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def user_bound_role_names(user, club_id=None, scope=None, allowed_club_ids=None):
|
def user_bound_role_names(user, club_id=None, scope=None, allowed_club_ids=None):
|
||||||
from backend.utils import _legacy_backend_role_permissions
|
from backend.utils import _legacy_backend_role_permissions
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from jituan.services.admin_context import (
|
|||||||
is_system_super_admin,
|
is_system_super_admin,
|
||||||
)
|
)
|
||||||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||||
from jituan.services.club_rbac import user_bound_role_names
|
from jituan.services.club_rbac import user_bound_role_names, describe_roles_in_scope
|
||||||
from jituan.services.kefu_menu_definitions import KEFU_MENU_ROW_DEFS
|
from jituan.services.kefu_menu_definitions import KEFU_MENU_ROW_DEFS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -145,6 +145,7 @@ def build_menu_access_payload(request, username=None):
|
|||||||
allowed = list(get_allowed_club_ids(request.user))
|
allowed = list(get_allowed_club_ids(request.user))
|
||||||
perm_codes = _permission_codes_list(permissions)
|
perm_codes = _permission_codes_list(permissions)
|
||||||
role_names = user_bound_role_names(request.user, club_id, scope, allowed)
|
role_names = user_bound_role_names(request.user, club_id, scope, allowed)
|
||||||
|
roles_detail = describe_roles_in_scope(request.user, club_id, scope, allowed)
|
||||||
|
|
||||||
perm_hint = ''
|
perm_hint = ''
|
||||||
if scope == DATA_SCOPE_SINGLE and not perm_codes:
|
if scope == DATA_SCOPE_SINGLE and not perm_codes:
|
||||||
@@ -174,6 +175,7 @@ def build_menu_access_payload(request, username=None):
|
|||||||
'club_scope': scope,
|
'club_scope': scope,
|
||||||
'effective_club_id': club_id,
|
'effective_club_id': club_id,
|
||||||
'perm_hint': perm_hint,
|
'perm_hint': perm_hint,
|
||||||
|
'roles_in_scope': roles_detail,
|
||||||
'can_switch_club': bool(club_ctx.get('can_switch_club')),
|
'can_switch_club': bool(club_ctx.get('can_switch_club')),
|
||||||
'is_group_admin': bool(club_ctx.get('is_group_admin')),
|
'is_group_admin': bool(club_ctx.get('is_group_admin')),
|
||||||
'menu_ready': True,
|
'menu_ready': True,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ KEFU_MENU_ROW_DEFS = [
|
|||||||
{'page_id': 'order.platform', 'name': '平台订单', 'path': '/order/platform', 'parent_id': 'order', 'sort_order': 31,
|
{'page_id': 'order.platform', 'name': '平台订单', 'path': '/order/platform', 'parent_id': 'order', 'sort_order': 31,
|
||||||
'perm_codes': ['002ab']},
|
'perm_codes': ['002ab']},
|
||||||
{'page_id': 'order.merchant', 'name': '商家订单', 'path': '/order/merchant', 'parent_id': 'order', 'sort_order': 32,
|
{'page_id': 'order.merchant', 'name': '商家订单', 'path': '/order/merchant', 'parent_id': 'order', 'sort_order': 32,
|
||||||
'perm_codes': ['002ac']},
|
'perm_codes': ['002ac', '002ab', '003aa']},
|
||||||
{'page_id': 'cross-platform', 'name': '跨平台管理', 'path': '', 'parent_id': '', 'sort_order': 40},
|
{'page_id': 'cross-platform', 'name': '跨平台管理', 'path': '', 'parent_id': '', 'sort_order': 40},
|
||||||
{'page_id': 'cross-platform.order', 'name': '跨平台订单', 'path': '/cross-order', 'parent_id': 'cross-platform', 'sort_order': 41,
|
{'page_id': 'cross-platform.order', 'name': '跨平台订单', 'path': '/cross-order', 'parent_id': 'cross-platform', 'sort_order': 41,
|
||||||
'perm_codes': ['003aa']},
|
'perm_codes': ['003aa']},
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from jituan.views import (
|
|||||||
ClubHuiyuanStatsView,
|
ClubHuiyuanStatsView,
|
||||||
ClubKefuLoginView,
|
ClubKefuLoginView,
|
||||||
ClubKefuMenuAccessView,
|
ClubKefuMenuAccessView,
|
||||||
|
ClubPermDebugView,
|
||||||
ClubListView,
|
ClubListView,
|
||||||
ClubMeContextView,
|
ClubMeContextView,
|
||||||
ClubOrderFinanceView,
|
ClubOrderFinanceView,
|
||||||
@@ -96,6 +97,7 @@ urlpatterns = [
|
|||||||
path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'),
|
path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'),
|
||||||
path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'),
|
path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'),
|
||||||
path('auth/menu-access', ClubKefuMenuAccessView.as_view(), name='jituan_menu_access'),
|
path('auth/menu-access', ClubKefuMenuAccessView.as_view(), name='jituan_menu_access'),
|
||||||
|
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
||||||
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
||||||
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
||||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||||
|
|||||||
@@ -229,6 +229,45 @@ class ClubKefuMenuAccessView(APIView):
|
|||||||
return Response({'code': 0, 'msg': 'ok', 'data': payload})
|
return Response({'code': 0, 'msg': 'ok', 'data': payload})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubPermDebugView(APIView):
|
||||||
|
"""GET /jituan/auth/perm-debug — 当前俱乐部权限解析明细。"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
from jituan.services.admin_context import get_allowed_club_ids, resolve_admin_effective_club
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||||
|
from jituan.services.club_rbac import describe_roles_in_scope
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
if not is_kefu_backend_account(user):
|
||||||
|
return Response({'code': 403, 'msg': '非后台账号'}, status=403)
|
||||||
|
|
||||||
|
requested_club = resolve_club_id_from_request(request)
|
||||||
|
requested_scope = resolve_club_scope(request)
|
||||||
|
eff_club, eff_scope, _ = resolve_admin_effective_club(user, request)
|
||||||
|
allowed = list(get_allowed_club_ids(user))
|
||||||
|
_, permissions = verify_kefu_permission(request, None)
|
||||||
|
codes = list(permissions) if isinstance(permissions, list) else ['*']
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'phone': user.Phone,
|
||||||
|
'requested_club_id': requested_club,
|
||||||
|
'requested_scope': requested_scope,
|
||||||
|
'effective_club_id': eff_club,
|
||||||
|
'effective_scope': eff_scope,
|
||||||
|
'allowed_club_ids': allowed,
|
||||||
|
'permission_codes': codes,
|
||||||
|
'roles_in_scope': describe_roles_in_scope(user, eff_club, eff_scope, allowed),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class ClubAdminAssignmentListView(APIView):
|
class ClubAdminAssignmentListView(APIView):
|
||||||
"""POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。"""
|
"""POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。"""
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from orders.utils import (
|
|||||||
from backend.utils import (
|
from backend.utils import (
|
||||||
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||||
update_shangjia_daily, update_zuzhang_daily_by_action,
|
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||||
verify_kefu_permission, has_perm_code,
|
verify_kefu_permission, has_perm_code, has_merchant_order_permission,
|
||||||
)
|
)
|
||||||
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||||
|
|
||||||
@@ -303,8 +303,19 @@ class KefuGetShangjiaOrderListView(APIView):
|
|||||||
if kefu is None:
|
if kefu is None:
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
if not has_perm_code(permissions, '002ac'):
|
if not has_merchant_order_permission(permissions):
|
||||||
return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
|
from backend.utils import _AllPermissions
|
||||||
|
if isinstance(permissions, _AllPermissions):
|
||||||
|
codes = ['*']
|
||||||
|
else:
|
||||||
|
codes = list(permissions or [])
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'msg': (
|
||||||
|
f'您无权查看商家订单列表(需要 002ac/002ab/003aa,'
|
||||||
|
f'当前俱乐部权限码:{",".join(codes) or "无"})'
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
# 4. 获取分页和筛选参数
|
# 4. 获取分页和筛选参数
|
||||||
page = int(request.data.get('page', 1))
|
page = int(request.data.get('page', 1))
|
||||||
|
|||||||
Reference in New Issue
Block a user