Răsfoiți Sursa

feat(menu): 新增菜单管理表结构(sys_menu/sys_role_menu)与数据库迁移+种子数据

asus 1 lună în urmă
părinte
comite
89101e99e0

+ 198 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -12,7 +12,9 @@ import org.springframework.util.StreamUtils;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -8737,5 +8739,201 @@ private void runMigration100() {
 			// 修改失败,忽略错误(幂等)
 		}
 
+		// 迁移229: 创建 sys_menu 表(管理后台侧边栏菜单,数据库驱动 + 角色授权)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS sys_menu (" +
+				"  id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"  parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID,0=顶级', " +
+				"  title VARCHAR(50) COMMENT '目录标题(有子菜单时用,对应 el-submenu)', " +
+				"  label VARCHAR(50) COMMENT '叶子菜单显示名(对应 el-menu-item)', " +
+				"  path VARCHAR(200) COMMENT '路由路径(叶子菜单必填,如 /families)', " +
+				"  icon VARCHAR(100) COMMENT 'Element UI 图标类名,如 el-icon-s-custom', " +
+				"  perm VARCHAR(100) COMMENT '权限标识,如 family:list / task:* / system:config', " +
+				"  sort INT DEFAULT 0 COMMENT '排序号,小的在前', " +
+				"  visible TINYINT DEFAULT 1 COMMENT '是否显示 1=显示 0=隐藏', " +
+				"  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"  INDEX idx_parent (parent_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台侧边栏菜单表'"
+			);
+			log.info("已创建sys_menu表");
+		} catch (Exception e) {
+			log.warn("创建sys_menu表失败: " + e.getMessage());
+		}
+
+		// 迁移230: 创建 sys_role_menu 表(角色-菜单授权)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS sys_role_menu (" +
+				"  id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"  role VARCHAR(50) NOT NULL COMMENT '角色标识: admin/teacher/nutritionist/article_manager/activity_manager/supplier_admin', " +
+				"  menu_id BIGINT NOT NULL COMMENT 'sys_menu.id', " +
+				"  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"  UNIQUE KEY uk_role_menu (role, menu_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-菜单授权表'"
+			);
+			log.info("已创建sys_role_menu表");
+		} catch (Exception e) {
+			log.warn("创建sys_role_menu表失败: " + e.getMessage());
+		}
+
+		// 迁移231: sys_menu 菜单种子数据(对应 Layout.vue 硬编码 menuItems,表为空才插入,幂等)
+		try {
+			Integer menuCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_menu", Integer.class);
+			if (menuCount == null || menuCount == 0) {
+				seedSysMenus();
+				log.info("sys_menu 种子数据已插入");
+			} else {
+				log.info("sys_menu 已有 {} 条数据,跳过种子插入", menuCount);
+			}
+		} catch (Exception e) {
+			log.warn("初始化sys_menu种子数据失败: " + e.getMessage());
+		}
+
+	}
+
+	/**
+	 * 迁移231辅助方法: 插入 sys_menu 种子数据
+	 * 结构与 cfc-web/src/views/Layout.vue 的 menuItems 硬编码一致
+	 * 数组格式: {parentKey(空=顶级), key, title(目录), label(叶子), path, icon, perm, sort}
+	 */
+	private void seedSysMenus() {
+		Map<String, Long> keyToId = new HashMap<>();
+		String[][] menus = {
+			// 顶级叶子
+			{"", "home", "", "首页", "/dashboard", "el-icon-s-home", "dashboard", "0"},
+			// 家庭运营
+			{"", "operation", "家庭运营", "", "", "el-icon-s-custom", "operation", "1"},
+			{"operation", "families", "", "家庭列表", "/families", "el-icon-s-custom", "family:list", "0"},
+			{"operation", "children", "", "孩子管理", "/children", "el-icon-user-solid", "family:children", "1"},
+			{"operation", "points", "", "积分管理", "/points", "el-icon-s-finance", "family:points", "2"},
+			{"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points", "3"},
+			{"operation", "wishes", "", "心愿管理", "/wishes", "el-icon-star-off", "family:wishes", "4"},
+			{"operation", "rewards", "", "奖励管理", "/rewards", "el-icon-s-goods", "reward:list", "5"},
+			// 任务中心
+			{"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "2"},
+			{"taskCenter", "tasks", "", "任务列表", "/tasks", "el-icon-s-order", "task:list", "0"},
+			{"taskCenter", "taskTemplates", "", "任务模板", "/task-templates", "el-icon-document", "task:templates", "1"},
+			{"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:list", "2"},
+			// 审核中心
+			{"", "reviewCenter", "", "审核中心", "/review-center", "el-icon-s-check", "audit", "3"},
+			{"", "serviceRoleApp", "", "服务角色申请审核", "/service-role-applications", "el-icon-s-check", "audit", "4"},
+			// 商城营销
+			{"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce", "5"},
+			{"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products", "0"},
+			{"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "commerce:products", "1"},
+			{"commerce", "ecomSupplier", "", "供应商管理", "/ecom-supplier", "el-icon-s-shop", "commerce:products", "2"},
+			{"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders", "3"},
+			{"commerce", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "commerce:orders", "4"},
+			{"commerce", "productProfitRate", "", "产品利润率", "/product-profit-rate", "el-icon-data-line", "commerce:profit", "5"},
+			{"commerce", "coupon", "", "优惠券管理", "/coupon", "el-icon-ticket", "marketing:coupon", "6"},
+			{"commerce", "couponGrantLog", "", "发券记录", "/coupon-grant-log", "el-icon-document", "marketing:coupon", "7"},
+			{"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "marketing:promotion", "8"},
+			{"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "marketing:promotion", "9"},
+			{"commerce", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "10"},
+			// 知识中心
+			{"", "knowledge", "知识中心", "", "", "el-icon-document", "content", "6"},
+			{"knowledge", "articleCategories", "", "知识分类", "/article-categories", "el-icon-folder", "articles:categories", "0"},
+			{"knowledge", "articleManage", "", "知识管理", "/article-manage", "el-icon-document", "articles:manage", "1"},
+			{"knowledge", "commentReview", "", "评论审核", "/comment-review", "el-icon-chat-dot-round", "articles:manage", "2"},
+			{"knowledge", "knowledgeTags", "", "知识标签", "/knowledge-tags", "el-icon-price-tag", "articles:categories", "3"},
+			{"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "system:config", "4"},
+			{"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "system:config", "5"},
+			// 活动管理
+			{"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "7"},
+			{"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list", "0"},
+			{"activity", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "activity:review", "1"},
+			{"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:review", "2"},
+			// 健康饮食
+			{"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health", "8"},
+			{"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
+			{"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "1"},
+			{"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "2"},
+			{"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "3"},
+			{"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "4"},
+			{"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "energy", "5"},
+			{"health", "energyRule", "", "能量规则管理", "/energy-rule", "el-icon-setting", "energy", "6"},
+			{"health", "energyBehaviorConfig", "", "能量行为配置", "/energy-behavior-config", "el-icon-edit-outline", "energy", "7"},
+			{"health", "periodicServiceConfig", "", "周期性服务配置", "/periodic-service-config", "el-icon-refresh", "energy", "8"},
+			{"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "9"},
+			{"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "10"},
+			{"health", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "system:config", "11"},
+			{"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:*", "12"},
+			// 报告解析系统
+			{"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "system:config", "9"},
+			{"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "system:config", "0"},
+			{"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "system:config", "1"},
+			{"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "system:config", "2"},
+			{"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "system:config", "3"},
+			// 家庭服务(规划师)
+			{"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "10"},
+			{"familyService", "myFamilies", "", "我的家庭", "/my-families", "el-icon-s-custom", "service:family", "0"},
+			{"familyService", "teacherTeam", "", "我的团队", "/teacher-team", "el-icon-s-custom", "service:team", "1"},
+			// 业务管理(规划师)
+			{"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "11"},
+			{"biz", "teacherPackages", "", "任务模板管理", "/teacher-packages", "el-icon-s-goods", "biz:packages", "0"},
+			{"biz", "teacherOrders", "", "订单佣金", "/teacher-orders", "el-icon-s-order", "biz:orders", "1"},
+			// 测评咨询(规划师)
+			{"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "12"},
+			{"assessment", "teacherAssessment", "", "DAN测评", "/teacher-assessment", "el-icon-edit", "assessment:dan", "0"},
+			{"assessment", "teacherConsult", "", "家长咨询", "/teacher-consult", "el-icon-chat-dot-round", "assessment:consult", "1"},
+			{"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "2"},
+			{"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "3"},
+			// 系统配置(三级)
+			{"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "13"},
+			{"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:config", "0"},
+			{"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:config", "0"},
+			{"baseManage", "users", "", "用户管理", "/users", "el-icon-user", "system:users", "1"},
+			{"baseManage", "operationLogs", "", "操作日志", "/operation-logs", "el-icon-s-order", "system:logs", "2"},
+			{"baseManage", "membershipCenter", "", "会员中心", "/membership-center", "el-icon-s-custom", "system:config", "3"},
+			{"baseManage", "badgeManage", "", "勋章管理", "/badge-manage", "el-icon-medal", "system:config", "4"},
+			{"baseManage", "virtualGoodsConfig", "", "虚拟支付道具", "/virtual-goods-config", "el-icon-goods", "system:config", "5"},
+			{"baseManage", "gates", "", "关卡配置", "/gates", "el-icon-key", "system:config", "6"},
+			{"baseManage", "menuManage", "", "菜单管理", "/menu-manage", "el-icon-menu", "system:menu", "7"},
+			{"system", "configCenter", "配置中心", "", "", "el-icon-setting", "system:config", "1"},
+			{"configCenter", "dimensionConfig", "", "维度配置", "/dimension-config", "el-icon-data-line", "system:config", "0"},
+			{"system", "supplyHierarchy", "供应商体系", "", "", "el-icon-s-management", "system:supply", "2"},
+			{"supplyHierarchy", "supplySystem", "", "供应商体系", "/supply-system", "el-icon-s-management", "system:supply", "0"},
+			{"system", "supplierManage", "供应商管理", "", "", "el-icon-s-shop", "supply:manage", "3"},
+			{"supplierManage", "supplyManage", "", "供应商管理", "/supply-manage", "el-icon-s-shop", "supply:manage", "0"},
+			{"supplierManage", "supplierProducts", "", "商品管理", "/supplier-products", "el-icon-s-goods", "commerce:product", "1"},
+			{"system", "healthConfig", "健康配置", "", "", "el-icon-first-aid-kit", "system:config", "4"},
+			{"healthConfig", "healthNormConfig", "", "健康常模", "/health-norm-config", "el-icon-data-line", "system:config", "0"},
+			{"healthConfig", "healthDataSource", "", "健康数据源", "/health-data-source", "el-icon-data-line", "system:config", "1"},
+			{"healthConfig", "healthEnergyConfig", "", "七维能量配置", "/health-energy-config", "el-icon-data-line", "system:config", "2"},
+			{"system", "lifeDimension", "人生维度", "", "", "el-icon-s-custom", "system:config", "5"},
+			{"lifeDimension", "zodiacConfigs", "", "星座配置", "/zodiac-configs", "el-icon-s-management", "system:config", "0"},
+			{"lifeDimension", "baziConfigs", "", "八字配置", "/bazi-configs", "el-icon-s-management", "system:config", "1"},
+			{"lifeDimension", "bloodTypeConfigs", "", "血型配置", "/blood-type-configs", "el-icon-s-management", "system:config", "2"},
+			{"system", "assessmentManage", "测评管理", "", "", "el-icon-edit", "assessment:dan", "6"},
+			{"assessmentManage", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:dan", "0"},
+			{"assessmentManage", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:dan", "1"},
+			{"assessmentManage", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:dan", "2"},
+			{"system", "virtualTeam", "虚拟团队", "", "", "el-icon-s-custom", "system:config", "7"},
+			{"virtualTeam", "virtualTeams", "", "虚拟团队", "/virtual-teams", "el-icon-s-custom", "system:config", "0"},
+		};
+
+		for (String[] m : menus) {
+			Long parentId = 0L;
+			if (m[0] != null && !m[0].isEmpty()) {
+				parentId = keyToId.get(m[0]);
+				if (parentId == null) {
+					parentId = 0L;
+				}
+			}
+			String title = m[2] != null && !m[2].isEmpty() ? m[2] : null;
+			String label = m[3] != null && !m[3].isEmpty() ? m[3] : null;
+			String path = m[4] != null && !m[4].isEmpty() ? m[4] : null;
+			String icon = m[5] != null && !m[5].isEmpty() ? m[5] : null;
+			String perm = m[6] != null && !m[6].isEmpty() ? m[6] : null;
+			int sort = Integer.parseInt(m[7]);
+			jdbcTemplate.update(
+				"INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
+				parentId, title, label, path, icon, perm, sort
+			);
+			Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+			keyToId.put(m[1], id);
+		}
 	}
 }

+ 25 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4698,3 +4698,28 @@ CREATE TABLE IF NOT EXISTS microbiome_article (
     INDEX idx_status (status),
     FULLTEXT INDEX ft_content (title, content)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菌群知识库文章';
+
+-- 后台侧边栏菜单表
+CREATE TABLE IF NOT EXISTS sys_menu (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID,0=顶级',
+    title VARCHAR(50) COMMENT '目录标题(有子菜单时用,对应 el-submenu)',
+    label VARCHAR(50) COMMENT '叶子菜单显示名(对应 el-menu-item)',
+    path VARCHAR(200) COMMENT '路由路径(叶子菜单必填,如 /families)',
+    icon VARCHAR(100) COMMENT 'Element UI 图标类名,如 el-icon-s-custom',
+    perm VARCHAR(100) COMMENT '权限标识,如 family:list / task:* / system:config',
+    sort INT DEFAULT 0 COMMENT '排序号,小的在前',
+    visible TINYINT DEFAULT 1 COMMENT '是否显示 1=显示 0=隐藏',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_parent (parent_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台侧边栏菜单表';
+
+-- 角色-菜单授权表
+CREATE TABLE IF NOT EXISTS sys_role_menu (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    role VARCHAR(50) NOT NULL COMMENT '角色标识: admin/teacher/nutritionist/article_manager/activity_manager/supplier_admin',
+    menu_id BIGINT NOT NULL COMMENT 'sys_menu.id',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_role_menu (role, menu_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-菜单授权表';