Browse Source

feat: add multi-role support (nutritionist, activity_admin, article_admin)

Backend:
- AdminAuthController: allow nutritionist/activity_admin/article_admin roles
- Auto-detect role on login for new role types

Frontend (cfc-web):
- Router: role-based route access control (admin bypasses all)
- Layout.vue: role-specific sidebar menus
- Add Activities.vue route for activity_admin
- Add .omo/ to gitignore
User 4 months ago
parent
commit
c9de072c20

+ 1 - 0
.gitignore

@@ -45,3 +45,4 @@ coverage/
 # OpenCode
 .opencode/
 dev-browser/
+.omo/

+ 19 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java

@@ -61,8 +61,14 @@ public class AdminAuthController {
             return Result.error("该手机号未注册");
         }
 
-        // 检查是否有管理员或规划师角色
-        if (!"admin".equals(user.getRole()) && !userService.hasRole(user.getId(), "teacher")) {
+        // 检查是否有管理员、规划师、营养师、活动管理员或文章管理员角色
+        String role = user.getRole();
+        if (!"admin".equals(role) && !"teacher".equals(role)
+                && !"nutritionist".equals(role) && !"activity_admin".equals(role) && !"article_admin".equals(role)
+                && !userService.hasRole(user.getId(), "teacher")
+                && !userService.hasRole(user.getId(), "nutritionist")
+                && !userService.hasRole(user.getId(), "activity_admin")
+                && !userService.hasRole(user.getId(), "article_admin")) {
             return Result.error("该账号没有管理权限");
         }
 
@@ -260,13 +266,20 @@ public class AdminAuthController {
 
         // 确定角色
         String role;
-        if ("admin".equals(user.getRole())) {
+        String userRole = user.getRole();
+        if ("admin".equals(userRole)) {
             role = "admin";
-        } else if ("teacher".equals(user.getRole()) || userService.hasRole(user.getId(), "teacher")) {
+        } else if ("teacher".equals(userRole) || userService.hasRole(user.getId(), "teacher")) {
             role = "teacher";
+        } else if ("nutritionist".equals(userRole) || userService.hasRole(user.getId(), "nutritionist")) {
+            role = "nutritionist";
+        } else if ("activity_admin".equals(userRole) || userService.hasRole(user.getId(), "activity_admin")) {
+            role = "activity_admin";
+        } else if ("article_admin".equals(userRole) || userService.hasRole(user.getId(), "article_admin")) {
+            role = "article_admin";
         } else {
-            // 如果用户既不是admin也不是teacher,给个默认角色,但记录警告
-            role = user.getRole() != null ? user.getRole() : "admin";
+            // 如果用户没有匹配的角色,给个默认角色,但记录警告
+            role = userRole != null ? userRole : "admin";
             log.warn("用户 {} 登录后台管理,角色为: {}", user.getId(), role);
         }
 

+ 44 - 32
cfc-web/src/router/index.js

@@ -289,6 +289,13 @@ const routes = [
         name: 'DanShopOrder',
         component: () => import('@/views/admin/OrderManage.vue'),
         meta: { title: 'DanShop订单' }
+      },
+      // ========== 活动管理(活动管理员) ==========
+      {
+        path: 'activities',
+        name: 'Activities',
+        component: () => import('@/views/admin/Activities.vue'),
+        meta: { title: '活动管理' }
       }
     ]
   }
@@ -352,42 +359,47 @@ router.beforeEach(async (to, from, next) => {
     return next('/login')
   }
 
-  // 4. 登录后首页重定向:规划师跳转到工作台(使用 replace 避免历史栈膨胀)
-  if (to.path === '/dashboard' && role === 'teacher') {
-    return next({ path: '/teacher-dashboard', replace: true })
+  // 4. 登录后首页重定向
+  const roleHomeMap = {
+    'teacher': '/teacher-dashboard',
+    'nutritionist': '/dashboard',
+    'activity_admin': '/dashboard',
+    'article_admin': '/dashboard'
+  }
+  if (to.path === '/dashboard' && roleHomeMap[role]) {
+    return next({ path: roleHomeMap[role], replace: true })
   }
 
-  // 5. 规划师专属路由检查
-  const teacherRoutes = [
-    'TeacherDashboard', 'TeacherFamilies', 'TeacherTeam',
-    'TeacherPackages', 'TeacherOrders', 'TeacherMessages',
-    'TeacherAssessment', 'GuidePackages', 'GuideFamilyTask',
-    'TeacherConsult'
-  ]
-  if (teacherRoutes.includes(to.name)) {
-    if (!role || role !== 'teacher') {
-      console.warn('Access denied: Teacher role required for', to.name)
-      return next({ path: '/dashboard', query: { accessDenied: 'teacher-only' }, replace: true })
-    }
+  // 5. 角色专属路由定义
+  const roleRoutes = {
+    teacher: [
+      'TeacherDashboard', 'TeacherFamilies', 'TeacherTeam',
+      'TeacherPackages', 'TeacherOrders', 'TeacherMessages',
+      'TeacherAssessment', 'GuidePackages', 'GuideFamilyTask',
+      'TeacherConsult'
+    ],
+    nutritionist: [
+      'Dashboard', 'Tasks', 'TaskTemplates'
+    ],
+    activity_admin: [
+      'Dashboard', 'Activities'
+    ],
+    article_admin: [
+      'Dashboard', 'ArticleCategory', 'ArticleManage', 'ArticleEdit'
+    ]
+  }
+
+  // 管理员可访问所有路由(无视角色限制)
+  if (role === 'admin') {
+    return next()
   }
 
-  // 6. 管理员专属路由检查(规划师不可访问)
-  const adminRoutes = [
-    'Families', 'Children', 'Points',
-    'Tasks', 'TaskTemplates', 'Rewards',
-    'Users', 'PackageAudit', 'GuideAudit', 'OperationLogs',
-    'ServiceTypes', 'ServiceContents', 'PackageTemplates',
-    'AssessmentAdmin', 'VendorReview',
-    'ProductManage', 'OrderManage', 'SysConfig', 'EnergySandbox',
-    'ArticleCategory', 'ArticleManage', 'ArticleEdit',
-    'EducationSystems',
-    'ZodiacConfigs', 'BaziConfigs', 'BloodTypeConfigs',
-    'DanShopCategory', 'DanShopProfitRule', 'DanShopLogistics',
-    'DanShopProduct', 'DanShopOrder'
-  ]
-  if (adminRoutes.includes(to.name) && role === 'teacher') {
-    console.warn('Access denied: Admin role required for', to.name)
-    return next({ path: '/teacher-dashboard', query: { accessDenied: 'admin-only' }, replace: true })
+  // 检查当前路由是否在当前角色的允许列表中
+  const allowedRoutes = roleRoutes[role] || []
+  if (to.name && !allowedRoutes.includes(to.name)) {
+    const fallbackPath = roleHomeMap[role] || '/dashboard'
+    console.warn(`Access denied: Role "${role}" cannot access route "${to.name}"`)
+    return next({ path: fallbackPath, query: { accessDenied: 'role-restricted' }, replace: true })
   }
 
   return next()

+ 74 - 0
cfc-web/src/views/Layout.vue

@@ -68,6 +68,62 @@
           </el-menu-item>
         </template>
 
+        <!-- 营养师菜单 -->
+        <template v-else-if="isNutritionist">
+          <el-menu-item index="/dashboard">
+            <i class="el-icon-s-data"></i>
+            <span slot="title">工作台</span>
+          </el-menu-item>
+          <el-submenu index="task">
+            <template slot="title">
+              <i class="el-icon-s-order"></i>
+              <span>任务管理</span>
+            </template>
+            <el-menu-item index="/tasks">
+              <i class="el-icon-s-order"></i>
+              <span>任务列表</span>
+            </el-menu-item>
+            <el-menu-item index="/task-templates">
+              <i class="el-icon-document"></i>
+              <span>任务模板</span>
+            </el-menu-item>
+          </el-submenu>
+        </template>
+
+        <!-- 活动管理员菜单 -->
+        <template v-else-if="isActivityAdmin">
+          <el-menu-item index="/dashboard">
+            <i class="el-icon-s-data"></i>
+            <span slot="title">工作台</span>
+          </el-menu-item>
+          <el-menu-item index="/activities">
+            <i class="el-icon-s-order"></i>
+            <span slot="title">活动管理</span>
+          </el-menu-item>
+        </template>
+
+        <!-- 文章管理员菜单 -->
+        <template v-else-if="isArticleAdmin">
+          <el-menu-item index="/dashboard">
+            <i class="el-icon-s-home"></i>
+            <span slot="title">首页</span>
+          </el-menu-item>
+          <el-submenu index="article">
+            <template slot="title">
+              <i class="el-icon-document"></i>
+              <span>文章管理</span>
+            </template>
+            <el-menu-item index="/article-categories">
+              <i class="el-icon-document"></i>
+              <span>文章分类</span>
+            </el-menu-item>
+            <el-menu-item index="/article-manage">
+              <i class="el-icon-document"></i>
+              <span>文章列表</span>
+            </el-menu-item>
+          </el-submenu>
+        </template>
+
         <!-- 服务商通用菜单 (vendorType=activity_provider/product_supplier, but not planner) -->
         <template v-else-if="isVendor">
           <el-menu-item index="/teacher-dashboard">
@@ -371,6 +427,15 @@ export default {
     isTeacher() {
       return this.currentRole === 'teacher'
     },
+    isNutritionist() {
+      return this.currentRole === 'nutritionist'
+    },
+    isActivityAdmin() {
+      return this.currentRole === 'activity_admin'
+    },
+    isArticleAdmin() {
+      return this.currentRole === 'article_admin'
+    },
     isVendor() {
       return !!this.vendorType
     },
@@ -381,6 +446,15 @@ export default {
       if (this.isPlanner) {
         return '规划师'
       }
+      if (this.isNutritionist) {
+        return '营养师'
+      }
+      if (this.isActivityAdmin) {
+        return '活动管理员'
+      }
+      if (this.isArticleAdmin) {
+        return '文章管理员'
+      }
       if (this.isVendor) {
         return '服务商'
       }