feat: 客服后台菜单按权限展示侧栏

This commit is contained in:
XingQue
2026-06-25 04:05:15 +08:00
parent 7a2723fef7
commit ff1e1a73dd
7 changed files with 270 additions and 1 deletions

View File

@@ -0,0 +1,133 @@
"""一次性写入客服后台菜单与权限映射。用法: python manage.py seed_kefu_menu"""
from django.core.management.base import BaseCommand
from jituan.models import KefuMenuPage
# 固定 page_id与前端 Layout 一致
MENU_ROWS = [
# 板块与财务
{'page_id': 'finance', 'name': '板块与财务', 'path': '', 'parent_id': '', 'sort_order': 10},
{'page_id': 'finance.bankuai', 'name': '板块配置', 'path': '/finance/bankuai', 'parent_id': 'finance', 'sort_order': 11,
'perm_codes': ['bankuai']},
{'page_id': 'finance.data', 'name': '财务数据', 'path': '/finance/data', 'parent_id': 'finance', 'sort_order': 12,
'perm_codes': ['caiwu']},
# 考核
{'page_id': 'assessment', 'name': '考核管理', 'path': '', 'parent_id': '', 'sort_order': 20},
{'page_id': 'assessment.examiner', 'name': '考核官管理', 'path': '/assessment/examiner', 'parent_id': 'assessment', 'sort_order': 21,
'perm_codes': ['kaohepeizhi']},
{'page_id': 'assessment.config', 'name': '考核配置', 'path': '/assessment/config', 'parent_id': 'assessment', 'sort_order': 22,
'perm_codes': ['kaohepeizhi']},
{'page_id': 'assessment.record', 'name': '考核记录', 'path': '/assessment/record', 'parent_id': 'assessment', 'sort_order': 23,
'perm_codes': ['kaohepeizhi']},
# 订单
{'page_id': 'order', 'name': '订单管理', 'path': '', 'parent_id': '', 'sort_order': 30},
{'page_id': 'order.platform', 'name': '平台订单', 'path': '/order/platform', 'parent_id': 'order', 'sort_order': 31,
'perm_codes': ['002ab']},
{'page_id': 'order.merchant', 'name': '商家订单', 'path': '/order/merchant', 'parent_id': 'order', 'sort_order': 32,
'perm_codes': ['002ac']},
# 跨平台
{'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,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.settings', 'name': '跨平台设置', 'path': '/cross-settings', 'parent_id': 'cross-platform', 'sort_order': 42,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.data', 'name': '跨平台数据', 'path': '/cross-data', 'parent_id': 'cross-platform', 'sort_order': 43,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.pre', 'name': '预结算管理', 'path': '/cross-pre', 'parent_id': 'cross-platform', 'sort_order': 44,
'perm_codes': ['003aa']},
# 用户
{'page_id': 'user', 'name': '用户管理', 'path': '', 'parent_id': '', 'sort_order': 50},
{'page_id': 'user.player', 'name': '打手管理', 'path': '/user/player', 'parent_id': 'user', 'sort_order': 51,
'perm_codes': ['001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg']},
{'page_id': 'user.guanli', 'name': '管事管理', 'path': '/user/guanli', 'parent_id': 'user', 'sort_order': 52,
'perm_codes': ['4400a', '4400b', '4400c', '4400d', '4400e', '4400f']},
{'page_id': 'user.shangjia', 'name': '商家管理', 'path': '/user/shangjia', 'parent_id': 'user', 'sort_order': 53,
'perm_codes': ['1100a', '1100b']},
{'page_id': 'user.zuzhang', 'name': '组长管理', 'path': '/user/zuzhang', 'parent_id': 'user', 'sort_order': 54,
'perm_codes': ['6600a', '6600b', '6600c', '6600d', '6600e']},
# 提现
{'page_id': 'withdraw', 'name': '提现管理', 'path': '', 'parent_id': '', 'sort_order': 60},
{'page_id': 'withdraw.audit', 'name': '提现审核', 'path': '/withdraw/audit', 'parent_id': 'withdraw', 'sort_order': 61,
'perm_codes': ['005bb']},
{'page_id': 'withdraw.data', 'name': '提现数据', 'path': '/withdraw/data', 'parent_id': 'withdraw', 'sort_order': 62,
'perm_codes': ['005bb']},
{'page_id': 'withdraw.settings', 'name': '提现设置', 'path': '/withdraw/settings', 'parent_id': 'withdraw', 'sort_order': 63,
'perm_codes': ['5500a', '5500b', '5500c']},
# 管理员
{'page_id': 'admin', 'name': '管理员管理', 'path': '', 'parent_id': '', 'sort_order': 70},
{'page_id': 'admin.role', 'name': '角色管理', 'path': '/admin/role', 'parent_id': 'admin', 'sort_order': 71,
'require_super': True},
{'page_id': 'admin.data-scope', 'name': '数据范围配置', 'path': '/admin/data-scope', 'parent_id': 'admin', 'sort_order': 72,
'require_group_manage': True},
{'page_id': 'admin.club-config', 'name': '俱乐部密钥配置', 'path': '/admin/club-config', 'parent_id': 'admin', 'sort_order': 73,
'require_super': True},
{'page_id': 'admin.user', 'name': '后台用户', 'path': '/admin/user', 'parent_id': 'admin', 'sort_order': 74,
'require_super': True},
{'page_id': 'admin.operation-log', 'name': '操作日志', 'path': '/admin/operation-log', 'parent_id': 'admin', 'sort_order': 75,
'perm_codes': ['czrz666']},
# 处罚
{'page_id': 'punishment', 'name': '处罚管理', 'path': '/punishment', 'parent_id': '', 'sort_order': 80,
'perm_codes': ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs']},
# 商品
{'page_id': 'product', 'name': '商品管理', 'path': '', 'parent_id': '', 'sort_order': 90},
{'page_id': 'product.list', 'name': '商品列表', 'path': '/product/list', 'parent_id': 'product', 'sort_order': 91,
'perm_codes': ['2200a']},
{'page_id': 'product.type-zone', 'name': '商品类型专区管理', 'path': '/product/type-zone', 'parent_id': 'product', 'sort_order': 92,
'perm_codes': ['7007a']},
{'page_id': 'product.data', 'name': '商品数据分析', 'path': '/product/data', 'parent_id': 'product', 'sort_order': 93,
'perm_codes': ['2200a']},
# 会员
{'page_id': 'member', 'name': '会员管理', 'path': '', 'parent_id': '', 'sort_order': 100},
{'page_id': 'member.list', 'name': '会员管理', 'path': '/member/list', 'parent_id': 'member', 'sort_order': 101,
'perm_codes': ['3300a']},
{'page_id': 'member.data', 'name': '会员数据分析', 'path': '/member/data', 'parent_id': 'member', 'sort_order': 102,
'perm_codes': ['huiyuancaiwu']},
# 小程序
{'page_id': 'miniapp', 'name': '小程序配置', 'path': '', 'parent_id': '', 'sort_order': 110},
{'page_id': 'miniapp.carousel', 'name': '轮播图与公告', 'path': '/miniapp/carousel', 'parent_id': 'miniapp', 'sort_order': 111,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.popup', 'name': '弹窗公告管理', 'path': '/miniapp/popup-notice', 'parent_id': 'miniapp', 'sort_order': 112,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.withdraw-settings', 'name': '提现与收款设置', 'path': '/withdraw/settings', 'parent_id': 'miniapp', 'sort_order': 113,
'perm_codes': ['5500a', '5500b', '5500c']},
# 店铺
{'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120},
{'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
{'page_id': 'shop.withdraw', 'name': '店铺提现申请', 'path': '/shop/withdraw-apply', 'parent_id': 'shop', 'sort_order': 122,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
{'page_id': 'shop.products', 'name': '店铺商品管理', 'path': '/shop/products', 'parent_id': 'shop', 'sort_order': 123,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
# 会话
{'page_id': 'chat.agent', 'name': '会话管理', 'path': '/chat/agent', 'parent_id': '', 'sort_order': 130,
'perm_codes': ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']},
]
class Command(BaseCommand):
help = '写入客服后台菜单页与权限映射(可重复执行,幂等更新)'
def handle(self, *args, **options):
created, updated = 0, 0
for row in MENU_ROWS:
defaults = {
'name': row['name'],
'path': row.get('path', ''),
'parent_id': row.get('parent_id', ''),
'sort_order': row.get('sort_order', 0),
'perm_codes': row.get('perm_codes', []),
'require_super': row.get('require_super', False),
'require_group_manage': row.get('require_group_manage', False),
}
existing = KefuMenuPage.query.filter(page_id=row['page_id']).first()
if existing:
for key, val in defaults.items():
setattr(existing, key, val)
existing.save()
updated += 1
else:
KefuMenuPage(page_id=row['page_id'], **defaults).save()
created += 1
self.stdout.write(self.style.SUCCESS(
f'kefu_menu_page 种子完成:新增 {created},更新 {updated},共 {len(MENU_ROWS)}'
))

View File

@@ -0,0 +1,32 @@
# Generated manually for kefu menu pages
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jituan', '0003_role_club_id'),
]
operations = [
migrations.CreateModel(
name='KefuMenuPage',
fields=[
('page_id', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='页面ID')),
('name', models.CharField(max_length=64, verbose_name='展示名')),
('path', models.CharField(blank=True, default='', max_length=128, verbose_name='路由路径,分组为空')),
('parent_id', models.CharField(blank=True, default='', db_index=True, max_length=64)),
('sort_order', models.IntegerField(default=0)),
('perm_codes', models.JSONField(blank=True, default=list, verbose_name='拥有任一权限即可见')),
('require_super', models.BooleanField(default=False, verbose_name='仅超级管理000001')),
('require_group_manage', models.BooleanField(default=False, verbose_name='仅集团超管任职')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'kefu_menu_page',
'ordering': ['sort_order', 'page_id'],
},
),
]

View File

@@ -155,3 +155,21 @@ class ClubFadanFenhongLilv(QModel):
class Meta:
db_table = 'club_fadan_fenhong_lilv'
unique_together = [['club_id', 'shenfen']]
class KefuMenuPage(QModel):
"""客服后台左侧菜单页(固定 page_id与前端路由对应"""
page_id = models.CharField(max_length=64, primary_key=True, verbose_name='页面ID')
name = models.CharField(max_length=64, verbose_name='展示名')
path = models.CharField(max_length=128, blank=True, default='', verbose_name='路由路径,分组为空')
parent_id = models.CharField(max_length=64, blank=True, default='', db_index=True)
sort_order = models.IntegerField(default=0)
perm_codes = models.JSONField(default=list, blank=True, verbose_name='拥有任一权限即可见')
require_super = models.BooleanField(default=False, verbose_name='仅超级管理000001')
require_group_manage = models.BooleanField(default=False, verbose_name='仅集团超管任职')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'kefu_menu_page'
ordering = ['sort_order', 'page_id']

View File

@@ -60,7 +60,7 @@ def build_admin_club_context(user):
is_group_admin=False,
assignments=[],
clubs=all_clubs,
can_switch_club=len(all_clubs) > 1,
can_switch_club=False,
role_code='CLUB_ADMIN',
role_name='子公司客服(默认)',
)

View File

@@ -0,0 +1,60 @@
"""客服后台菜单可见性(侧栏展示,不影响各页面原有权限校验)。"""
from backend.utils import _AllPermissions, verify_kefu_permission
from jituan.models import KefuMenuPage
from jituan.services.admin_context import build_admin_club_context, can_manage_admin_assignments
def _is_super_permissions(permissions):
return isinstance(permissions, _AllPermissions)
def _page_visible(page, permissions, user, is_super):
if is_super:
return True
if page.require_super:
return False
if page.require_group_manage:
perms_list = list(permissions) if not _is_super_permissions(permissions) else ['000001']
return can_manage_admin_assignments(user, perms_list)
codes = page.perm_codes or []
if not codes:
return False
return any(code in permissions for code in codes)
def build_menu_access_payload(request, username=None):
"""
根据用户 gvsdsdk 权限计算可见菜单 page_id 列表。
返回 (payload, error_response)error_response 非空时表示校验失败。
"""
phone = (username or getattr(request.user, 'Phone', '') or '').strip()
kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None:
return None, permissions
is_super = _is_super_permissions(permissions)
pages = list(KefuMenuPage.query.all().order_by('sort_order', 'page_id'))
visible_ids = []
for page in pages:
if not page.path:
continue
if _page_visible(page, permissions, request.user, is_super):
visible_ids.append(page.page_id)
for page in pages:
if page.path:
continue
children = [p for p in pages if p.parent_id == page.page_id and p.path]
if any(child.page_id in visible_ids for child in children):
visible_ids.append(page.page_id)
club_ctx = build_admin_club_context(request.user)
path_by_id = {p.page_id: p.path for p in pages if p.path}
visible_paths = [path_by_id[pid] for pid in visible_ids if pid in path_by_id]
return {
'visible_page_ids': sorted(set(visible_ids)),
'visible_paths': visible_paths,
'can_switch_club': bool(club_ctx.get('can_switch_club')),
'is_group_admin': bool(club_ctx.get('is_group_admin')),
}, None

View File

@@ -33,6 +33,7 @@ from jituan.views import (
ClubHuiyuanBankuaiView,
ClubHuiyuanStatsView,
ClubKefuLoginView,
ClubKefuMenuAccessView,
ClubListView,
ClubMeContextView,
ClubOrderFinanceView,
@@ -46,6 +47,7 @@ urlpatterns = [
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_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/menu-access', ClubKefuMenuAccessView.as_view(), name='jituan_menu_access'),
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),

View File

@@ -13,6 +13,7 @@ from rest_framework_simplejwt.tokens import RefreshToken
from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT, DATA_SCOPE_ALL, DATA_SCOPE_SINGLE
from jituan.models import Club, AdminAssignment
from jituan.services.admin_context import build_admin_club_context, can_manage_admin_assignments
from jituan.services.kefu_menu import build_menu_access_payload
from jituan.services.wechat_login import login_or_register_by_club
from jituan.services.caiwu_stats import build_caiwu_payload
from jituan.services.szxx_stats import build_szxx_payload
@@ -153,6 +154,9 @@ class ClubKefuLoginView(APIView):
token = str(refresh.access_token)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
club_context = build_admin_club_context(target_user)
# 登录请求尚未带 JWT需用本次登录用户计算菜单
request.user = target_user
menu_access, _menu_err = build_menu_access_payload(request, phone)
return Response({
'code': 0,
@@ -162,6 +166,12 @@ class ClubKefuLoginView(APIView):
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
'club_context': club_context,
'menu_access': menu_access if menu_access else {
'visible_page_ids': [],
'visible_paths': [],
'can_switch_club': False,
'is_group_admin': False,
},
},
})
@@ -199,6 +209,20 @@ class ClubMeContextView(APIView):
})
class ClubKefuMenuAccessView(APIView):
"""GET /jituan/auth/menu-access — 已登录客服可见侧栏菜单。"""
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
if user.UserType not in ('kefu', 'admin') and not user.IsSuperuser:
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
payload, err = build_menu_access_payload(request)
if err is not None:
return err
return Response({'code': 0, 'msg': 'ok', 'data': payload})
class ClubAdminAssignmentListView(APIView):
"""POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。"""