From ff1e1a73dde969c65b952e01350f7c3e27324ecf Mon Sep 17 00:00:00 2001 From: XingQue Date: Thu, 25 Jun 2026 04:05:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A2=E6=9C=8D=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8C=89=E6=9D=83=E9=99=90=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=BE=A7=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jituan/management/commands/seed_kefu_menu.py | 133 +++++++++++++++++++ jituan/migrations/0004_kefu_menu_page.py | 32 +++++ jituan/models.py | 18 +++ jituan/services/admin_context.py | 2 +- jituan/services/kefu_menu.py | 60 +++++++++ jituan/urls.py | 2 + jituan/views.py | 24 ++++ 7 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 jituan/management/commands/seed_kefu_menu.py create mode 100644 jituan/migrations/0004_kefu_menu_page.py create mode 100644 jituan/services/kefu_menu.py diff --git a/jituan/management/commands/seed_kefu_menu.py b/jituan/management/commands/seed_kefu_menu.py new file mode 100644 index 0000000..4ca85ba --- /dev/null +++ b/jituan/management/commands/seed_kefu_menu.py @@ -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)} 条' + )) diff --git a/jituan/migrations/0004_kefu_menu_page.py b/jituan/migrations/0004_kefu_menu_page.py new file mode 100644 index 0000000..3208a3c --- /dev/null +++ b/jituan/migrations/0004_kefu_menu_page.py @@ -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'], + }, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index 3a18b72..d171688 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -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'] diff --git a/jituan/services/admin_context.py b/jituan/services/admin_context.py index 98d2583..8c59022 100644 --- a/jituan/services/admin_context.py +++ b/jituan/services/admin_context.py @@ -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='子公司客服(默认)', ) diff --git a/jituan/services/kefu_menu.py b/jituan/services/kefu_menu.py new file mode 100644 index 0000000..76817a7 --- /dev/null +++ b/jituan/services/kefu_menu.py @@ -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 diff --git a/jituan/urls.py b/jituan/urls.py index 9ea56b3..a6a7e33 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -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'), diff --git a/jituan/views.py b/jituan/views.py index a66dde5..4045ca5 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -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 功能权限)。"""