|
|
@@ -3042,14 +3042,46 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
// 迁移365: diet_preferences 添加无特殊偏好标记(问卷第3步「无特殊偏好」一键跳过落库)
|
|
|
ensureColumn("diet_preferences", "no_preference", "TINYINT(1) DEFAULT 0 COMMENT '无特殊偏好标记(1=用户明确选择无特殊偏好)'");
|
|
|
|
|
|
- // 迁移366: 新增 LITE 轻享会员等级(9.9元/月,解锁健康自检问卷与个性化方案)
|
|
|
+ // 迁移366: 9.9 元/月健康会员复用既有 BASIC「体验会员」等级(迁移360/schema.sql 已创建),
|
|
|
+ // 移除早期误建的重复等级 LITE(与 BASIC 同为 9.9 元/月),并迁移可能已落库的 LITE 记录。
|
|
|
+ // 三段独立 try,确保任一段异常都不会阻断后续的等级删除。
|
|
|
try {
|
|
|
- jdbcTemplate.execute("INSERT IGNORE INTO membership_levels (level_code, level_name, level_desc, price_monthly, price_quarterly, price_yearly, max_children, max_tasks_per_day, ai_review_enabled, priority_support, teacher_consultation) VALUES " +
|
|
|
- "('LITE', '轻享会员', '9.9元/月,解锁健康自检问卷与个性化方案', 990, 2970, 11880, 2, 10, 1, 0, 0)");
|
|
|
- log.info("迁移366: 已插入 LITE 轻享会员等级");
|
|
|
+ jdbcTemplate.update("UPDATE family_memberships SET level_code = 'BASIC' WHERE level_code = 'LITE'");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移366: 迁移 family_memberships 中的 LITE 记录失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ jdbcTemplate.update("UPDATE users SET member_level = 'BASIC' WHERE member_level = 'LITE'");
|
|
|
} catch (Exception e) {
|
|
|
- log.warn("迁移366: 插入 LITE 会员等级失败或已存在: {}", e.getMessage());
|
|
|
+ log.warn("迁移366: 迁移 users.member_level 中的 LITE 记录失败: {}", e.getMessage());
|
|
|
}
|
|
|
+ try {
|
|
|
+ int removed = jdbcTemplate.update("DELETE FROM membership_levels WHERE level_code = 'LITE'");
|
|
|
+ log.info("迁移366: 9.9 元/月健康会员统一使用 BASIC 体验会员,已移除重复的 LITE 等级({} 行)", removed);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移366: 清理 LITE 等级失败或不存在: {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移372: nutrition_product 添加 efficacy_tags 列(成分功效标签)
|
|
|
+ addNutritionProductEfficacyTags();
|
|
|
+
|
|
|
+ // 迁移373: products 添加 tags 列(文章-商品标签关联推荐)
|
|
|
+ addProductTagsColumn();
|
|
|
+
|
|
|
+ // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
|
|
|
+ addUserContentWeightsTable();
|
|
|
+
|
|
|
+ // 迁移375: 修复 article_quiz_records 表结构(旧表缺 member_id/total_questions/cf_earned/energy_earned,questions/answers 需为 TEXT)
|
|
|
+ fixArticleQuizRecordsTable();
|
|
|
+
|
|
|
+ // 迁移376: health_status 添加 lifestyle JSON 列(心情/睡眠/排便/抗生素/过敏表现)
|
|
|
+ ensureColumn("health_status", "lifestyle", "JSON DEFAULT NULL COMMENT '生活方式与肠道健康 {mood,sleepDuration,bedtime,bowelFrequency,bowelForm,antibioticHistory,allergySymptoms}'");
|
|
|
+
|
|
|
+ // 迁移377: diet_preferences 添加 diet_structure JSON 列(蔬果量/乳制品/含糖饮料频率)
|
|
|
+ ensureColumn("diet_preferences", "diet_structure", "JSON DEFAULT NULL COMMENT '饮食结构现状 {vegetableFruit,dairy,sugaryDrinks}'");
|
|
|
+
|
|
|
+ // 迁移378: 创建 member_indicator_current 表(成员指标当前值 + 相对上次变化 delta)
|
|
|
+ addMemberIndicatorCurrentTable();
|
|
|
}
|
|
|
|
|
|
private void migrateHealthDimensionsTable() {
|
|
|
@@ -9291,8 +9323,6 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 迁移249: sys_menu 菜单归类调整 + 权限点拆分(《角色权限与菜单归类整合方案》第二部分/B-3,幂等)
|
|
|
- migrateSysMenuReorg();
|
|
|
|
|
|
// 迁移250: 清理测试脏数据(B-7:TEST-Energy-Grant 测试任务、乱码昵称)
|
|
|
try {
|
|
|
@@ -9305,8 +9335,6 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
log.warn("迁移250失败: {}", e.getMessage());
|
|
|
}
|
|
|
|
|
|
- // 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
|
|
|
- migrateHealthMenuMount();
|
|
|
|
|
|
// 迁移252: 创建用户画像表
|
|
|
try {
|
|
|
@@ -9486,8 +9514,6 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
log.warn("扩展 tasks.status 枚举失败(可能已修改): {}", e.getMessage());
|
|
|
}
|
|
|
|
|
|
- // 迁移267: 方案中心菜单(插在家庭运营与任务中心之间)
|
|
|
- migratePlanCenterMenu();
|
|
|
// 迁移268: coupon 表添加 status 列(优惠券停用/启用)
|
|
|
migrateCouponStatus();
|
|
|
// 迁移276: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
|
|
|
@@ -9511,64 +9537,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
ensureColumn("users", "family_inviter_id", "BIGINT DEFAULT NULL COMMENT '家庭邀请人ID(首次注册时绑定,用于追踪邀请来源)'");
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 迁移267: 存量库插入「方案中心」一级菜单及子菜单(幂等)
|
|
|
- * 位置:家庭运营(sort=1) 与 任务中心 之间;任务中心及之后顶级菜单 sort+1
|
|
|
- */
|
|
|
- private void migratePlanCenterMenu() {
|
|
|
- try {
|
|
|
- Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='方案中心'");
|
|
|
- if (existed == null) {
|
|
|
- // 为任务中心及之后的顶级菜单腾出 sort=2
|
|
|
- jdbcTemplate.update(
|
|
|
- "UPDATE sys_menu SET sort = sort + 1 WHERE parent_id=0 AND sort >= 2 " +
|
|
|
- "AND title <> '方案中心'");
|
|
|
- jdbcTemplate.update(
|
|
|
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) " +
|
|
|
- "VALUES (0, '方案中心', NULL, NULL, 'el-icon-notebook-2', 'plan:*', 2, 1)");
|
|
|
- existed = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
|
|
- log.info("迁移267: 已插入方案中心一级菜单");
|
|
|
- }
|
|
|
- if (existed == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
- Long healthPlanId = insertPlanCenterChild(existed, "健康方案管理", "/health-plan-manage",
|
|
|
- "el-icon-first-aid-kit", "plan:health", 0);
|
|
|
- Long assessmentPlanId = insertPlanCenterChild(existed, "测评方案管理", "/assessment-plan-manage",
|
|
|
- "el-icon-edit", "plan:assessment", 1);
|
|
|
- Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
|
|
|
- if (grantCount != null && grantCount > 0) {
|
|
|
- String[] roles = {"admin", "butler", "nutritionist", "operator"};
|
|
|
- Long[] menuIds = {existed, healthPlanId, assessmentPlanId};
|
|
|
- for (String role : roles) {
|
|
|
- for (Long menuId : menuIds) {
|
|
|
- grantMenuToRole(role, menuId);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- log.info("迁移267: 方案中心菜单挂载完成");
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移267失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Long insertPlanCenterChild(Long parentId, String label, String path, String icon, String perm, int sort) {
|
|
|
- try {
|
|
|
- Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=" + parentId + " AND path='" + path + "'");
|
|
|
- if (existed != null) {
|
|
|
- return existed;
|
|
|
- }
|
|
|
- jdbcTemplate.update(
|
|
|
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, ?, ?, ?, ?, ?, 1)",
|
|
|
- parentId, label, path, icon, perm, sort
|
|
|
- );
|
|
|
- return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移267: 挂载子菜单「{}」失败: {}", label, e.getMessage());
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 迁移268: coupon 表添加 status 列(优惠券停用/启用,幂等)
|
|
|
* 存量数据 status 为 NULL,查询层按 ACTIVE 处理,不影响现有券。
|
|
|
@@ -9582,200 +9551,6 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 迁移249: 存量库 sys_menu 菜单归类调整 + 权限点拆分(幂等,按 path/title 定位)
|
|
|
- * 1. 审核中心改为分组,归集:服务角色申请审核/活动审核/待退款管理/收益提现审核
|
|
|
- * 2. 能量规则/能量行为/周期性服务 → 系统配置·健康配置
|
|
|
- * 3. LangGraph 管理 → 报告解析系统
|
|
|
- * 4. 测评管理/测评订单/待分配规划师 → 测评咨询(删除系统配置下原"测评管理"子组)
|
|
|
- * 5. 菜单组权限统一 模块:* 格式;system:config 拆分为 system:base/config:member/config:dimension/config:health/config:report-parser
|
|
|
- * 6. 术语统一:七维能量配置 → 五维能量配置
|
|
|
- */
|
|
|
- private void migrateSysMenuReorg() {
|
|
|
- try {
|
|
|
- // A. 审核中心:顶级叶子 → 分组,并插入"审核中心总览"子项
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET title='审核中心', label=NULL, path=NULL, perm='audit', sort=3 WHERE path='/review-center' AND parent_id=0 AND title IS NULL");
|
|
|
- Long reviewGroup = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='审核中心' AND (path IS NULL OR path='')");
|
|
|
- if (reviewGroup != null) {
|
|
|
- Integer childCount = jdbcTemplate.queryForObject(
|
|
|
- "SELECT COUNT(*) FROM sys_menu WHERE parent_id=" + reviewGroup + " AND path='/review-center'", Integer.class);
|
|
|
- if (childCount == null || childCount == 0) {
|
|
|
- jdbcTemplate.update(
|
|
|
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, '审核中心总览', '/review-center', 'el-icon-s-check', 'audit', 0, 1)",
|
|
|
- reviewGroup);
|
|
|
- }
|
|
|
- // 迁入审核中心
|
|
|
- moveMenu("/service-role-applications", reviewGroup, "audit:service-role", 1);
|
|
|
- moveMenu("/activity-review", reviewGroup, "audit:activity", 2);
|
|
|
- moveMenu("/pending-refund", reviewGroup, "audit:refund", 3);
|
|
|
- moveMenu("/family-earnings-withdraw", reviewGroup, "audit:withdraw", 4);
|
|
|
- }
|
|
|
-
|
|
|
- // B. 能量规则/能量行为/周期性服务 → 系统配置·健康配置
|
|
|
- Long healthConfig = queryMenuId("SELECT id FROM sys_menu WHERE title='健康配置' AND (path IS NULL OR path='')");
|
|
|
- if (healthConfig != null) {
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='config:health' WHERE id=" + healthConfig);
|
|
|
- moveMenu("/energy-rule", healthConfig, "config:health", 3);
|
|
|
- moveMenu("/energy-behavior-config", healthConfig, "config:health", 4);
|
|
|
- moveMenu("/periodic-service-config", healthConfig, "config:health", 5);
|
|
|
- }
|
|
|
-
|
|
|
- // C. LangGraph 管理 → 报告解析系统
|
|
|
- Long reportParser = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='报告解析系统'");
|
|
|
- if (reportParser != null) {
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='config:report-parser' WHERE id=" + reportParser);
|
|
|
- moveMenu("/langgraph-admin", reportParser, "config:report-parser", 4);
|
|
|
- updateMenuPermByPath("/report-types", "config:report-parser");
|
|
|
- updateMenuPermByPath("/report-parser-import", "config:report-parser");
|
|
|
- updateMenuPermByPath("/report-unknown-clusters", "config:report-parser");
|
|
|
- updateMenuPermByPath("/report-auto-learn", "config:report-parser");
|
|
|
- }
|
|
|
-
|
|
|
- // D. 测评管理/测评订单/待分配规划师 → 测评咨询
|
|
|
- Long assessment = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='测评咨询'");
|
|
|
- Long oldAssessmentManage = queryMenuId("SELECT id FROM sys_menu WHERE title='测评管理' AND (path IS NULL OR path='')");
|
|
|
- if (assessment != null) {
|
|
|
- moveMenu("/assessment-admin", assessment, "assessment:manage", 0);
|
|
|
- moveMenu("/assessment-orders", assessment, "assessment:orders", 1);
|
|
|
- moveMenu("/assessment-assign", assessment, "assessment:assign", 2);
|
|
|
- }
|
|
|
- if (oldAssessmentManage != null && assessment != null && !oldAssessmentManage.equals(assessment)) {
|
|
|
- // 子项已迁走,删除系统配置下空的"测评管理"子组及其授权关联
|
|
|
- jdbcTemplate.update("DELETE FROM sys_role_menu WHERE menu_id=" + oldAssessmentManage);
|
|
|
- jdbcTemplate.update("DELETE FROM sys_menu WHERE id=" + oldAssessmentManage);
|
|
|
- }
|
|
|
-
|
|
|
- // E. 菜单组权限统一 模块:* 格式(废弃 operation/content/commerce/health 单段写法)
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='family:*' WHERE parent_id=0 AND title='家庭运营'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='articles:*' WHERE parent_id=0 AND title='知识中心'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='commerce:*' WHERE parent_id=0 AND title='商城营销'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='health:*' WHERE parent_id=0 AND title='健康饮食'");
|
|
|
-
|
|
|
- // F. system:config 拆分
|
|
|
- updateMenuPermByPath("/sys-config", "system:base");
|
|
|
- updateMenuPermByPath("/gates", "system:base");
|
|
|
- updateMenuPermByPath("/membership-center", "config:member");
|
|
|
- updateMenuPermByPath("/badge-manage", "config:member");
|
|
|
- updateMenuPermByPath("/virtual-goods-config", "config:member");
|
|
|
- updateMenuPermByPath("/dimension-config", "config:dimension");
|
|
|
- updateMenuPermByPath("/zodiac-configs", "config:dimension");
|
|
|
- updateMenuPermByPath("/bazi-configs", "config:dimension");
|
|
|
- updateMenuPermByPath("/blood-type-configs", "config:dimension");
|
|
|
- updateMenuPermByPath("/health-norm-config", "config:health");
|
|
|
- updateMenuPermByPath("/health-data-source", "config:health");
|
|
|
- // 术语统一:七维能量配置 → 五维能量配置
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET label='五维能量配置', perm='config:health' WHERE path='/health-energy-config'");
|
|
|
- updateMenuPermByPath("/virtual-teams", "system:base");
|
|
|
- updateMenuPermByPath("/knowledge-base", "knowledge:base");
|
|
|
- updateMenuPermByPath("/health-knowledge", "knowledge:base");
|
|
|
- updateMenuPermByPath("/supplier-products", "supply:manage");
|
|
|
- updateMenuPermByPath("/nutrition-products", "health:products");
|
|
|
- updateMenuPermByPath("/energy-sandbox", "health:energy");
|
|
|
- // 子组权限
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='system:base' WHERE title='基础管理' AND (path IS NULL OR path='')");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='config:dimension' WHERE title='配置中心' AND (path IS NULL OR path='')");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='config:dimension' WHERE title='人生维度' AND (path IS NULL OR path='')");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm='system:base' WHERE title='虚拟团队' AND (path IS NULL OR path='')");
|
|
|
-
|
|
|
- // G. 商城营销叶子权限统一(B-4 映射可识别)
|
|
|
- updateMenuPermByPath("/product-manage", "commerce:products,commerce:self");
|
|
|
- updateMenuPermByPath("/inventory", "inventory:self");
|
|
|
- updateMenuPermByPath("/order-manage", "commerce:orders,order:self");
|
|
|
- updateMenuPermByPath("/ecom-supplier", "commerce:supplier");
|
|
|
- updateMenuPermByPath("/product-profit-rate", "finance:profit-rate");
|
|
|
- updateMenuPermByPath("/coupon", "commerce:coupon");
|
|
|
- updateMenuPermByPath("/coupon-grant-log", "commerce:coupon");
|
|
|
- updateMenuPermByPath("/promotion", "commerce:promotion");
|
|
|
- updateMenuPermByPath("/family-earnings", "finance:family-earnings");
|
|
|
- updateMenuPermByPath("/activities", "activity:list,activity:self");
|
|
|
- updateMenuPermByPath("/activity-registration-review", "activity:registration");
|
|
|
- updateMenuPermByPath("/families", "family:list,family:read,family:bound");
|
|
|
- updateMenuPermByPath("/children", "family:children,family:read,family:bound");
|
|
|
- updateMenuPermByPath("/points-log", "family:points,family:bound");
|
|
|
- updateMenuPermByPath("/growth-task", "task:growth");
|
|
|
-
|
|
|
- log.info("迁移249: sys_menu 菜单归类调整与权限拆分完成");
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移249失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 按 path 更新菜单权限(找不到时忽略)
|
|
|
- */
|
|
|
- private void updateMenuPermByPath(String path, String perm) {
|
|
|
- try {
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET perm=? WHERE path=?", perm, path);
|
|
|
- } catch (Exception ignored) {
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(存量库 sys_menu 幂等补齐)
|
|
|
- * 对应页面 ReportManagement.vue / HealthReportAudit.vue / SeasonalFoods.vue 已开发完成但此前未挂载;
|
|
|
- * 同时将健康饮食组排序对齐《浠艾福后台-角色权限与菜单归类整合方案》最终菜单树。
|
|
|
- */
|
|
|
- private void migrateHealthMenuMount() {
|
|
|
- try {
|
|
|
- Long healthGroup = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='健康饮食'");
|
|
|
- if (healthGroup == null) {
|
|
|
- log.warn("迁移251: 未找到健康饮食分组,跳过");
|
|
|
- return;
|
|
|
- }
|
|
|
- // 1. 存量菜单排序对齐(健康报告0..五维能量11),先更新防止插入冲突
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=0 WHERE parent_id=" + healthGroup + " AND path='/health-reports'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=3 WHERE parent_id=" + healthGroup + " AND path='/health-indicators'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=4 WHERE parent_id=" + healthGroup + " AND path='/health-checkins'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=5 WHERE parent_id=" + healthGroup + " AND path='/emotion-alert'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=6 WHERE parent_id=" + healthGroup + " AND path='/nutrition-mappings'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=7 WHERE parent_id=" + healthGroup + " AND path='/foods'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=8 WHERE parent_id=" + healthGroup + " AND path='/recipes'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=10 WHERE parent_id=" + healthGroup + " AND path='/nutrition-products'");
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET sort=11 WHERE parent_id=" + healthGroup + " AND path='/energy-sandbox'");
|
|
|
- // 2. 按 path 幂等插入 3 个新页面
|
|
|
- Long reportManagementId = insertHealthMenu(healthGroup, "报告管理", "/report-management", "el-icon-document", "health:reports", 1);
|
|
|
- Long reportAuditId = insertHealthMenu(healthGroup, "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", 2);
|
|
|
- Long seasonalFoodsId = insertHealthMenu(healthGroup, "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", 9);
|
|
|
- // 3. 条件授权:仅当已配置过角色授权(sys_role_menu 非空)时补授权,
|
|
|
- // 避免破坏"所有角色均未授权 → 返回全量可见树"的兼容逻辑(SysMenuService.userMenus)
|
|
|
- Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
|
|
|
- if (grantCount != null && grantCount > 0) {
|
|
|
- String[] roles = {"admin", "butler", "nutritionist"};
|
|
|
- Long[] menuIds = {reportManagementId, reportAuditId, seasonalFoodsId};
|
|
|
- for (String role : roles) {
|
|
|
- for (Long menuId : menuIds) {
|
|
|
- grantMenuToRole(role, menuId);
|
|
|
- }
|
|
|
- }
|
|
|
- log.info("迁移251: 已为 admin/butler/nutritionist 补授权新健康菜单");
|
|
|
- }
|
|
|
- log.info("迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材完成");
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移251失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 按 path 幂等插入健康饮食叶子菜单,返回菜单 id(已存在返回原 id)
|
|
|
- */
|
|
|
- private Long insertHealthMenu(Long parentId, String label, String path, String icon, String perm, int sort) {
|
|
|
- try {
|
|
|
- Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=" + parentId + " AND path='" + path + "'");
|
|
|
- if (existed != null) {
|
|
|
- return existed;
|
|
|
- }
|
|
|
- jdbcTemplate.update(
|
|
|
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, ?, ?, ?, ?, ?, 1)",
|
|
|
- parentId, label, path, icon, perm, sort
|
|
|
- );
|
|
|
- Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
|
|
- log.info("迁移251: 已挂载健康饮食菜单「{}」", label);
|
|
|
- return id;
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("迁移251: 挂载菜单「{}」失败: {}", label, e.getMessage());
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
/**
|
|
|
* 角色-菜单授权(幂等,唯一键 uk_role_menu 冲突忽略)
|
|
|
@@ -9790,16 +9565,6 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 将菜单(按 path)移动到指定父组并更新权限/排序(幂等)
|
|
|
- */
|
|
|
- private void moveMenu(String path, Long parentId, String perm, int sort) {
|
|
|
- try {
|
|
|
- jdbcTemplate.update("UPDATE sys_menu SET parent_id=?, perm=?, sort=? WHERE path=?", parentId, perm, sort, path);
|
|
|
- } catch (Exception ignored) {
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* 查询单个菜单 id,不存在返回 null
|
|
|
*/
|
|
|
@@ -9872,18 +9637,17 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
|
|
|
{"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:registration", "1"},
|
|
|
// 健康饮食(health:*,能量规则/行为/周期性服务已移入系统配置·健康配置,LangGraph已移入报告解析系统;报告管理/报告审核/应季食材按方案挂载)
|
|
|
{"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "8"},
|
|
|
- {"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
|
|
|
- {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "1"},
|
|
|
- {"health", "reportAudit", "", "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", "2"},
|
|
|
- {"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "3"},
|
|
|
- {"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "4"},
|
|
|
- {"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "5"},
|
|
|
- {"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "6"},
|
|
|
- {"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "7"},
|
|
|
- {"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "8"},
|
|
|
- {"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "9"},
|
|
|
- {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "10"},
|
|
|
- {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "11"},
|
|
|
+ {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "0"},
|
|
|
+ {"health", "proxyReportUpload", "", "代传报告", "/report-proxy-upload", "el-icon-upload", "health:reports", "1"},
|
|
|
+ {"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "2"},
|
|
|
+ {"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "3"},
|
|
|
+ {"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "4"},
|
|
|
+ {"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "5"},
|
|
|
+ {"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "6"},
|
|
|
+ {"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "7"},
|
|
|
+ {"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "8"},
|
|
|
+ {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "9"},
|
|
|
+ {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "10"},
|
|
|
// 报告解析系统(config:report-parser,LangGraph 管理归入)
|
|
|
{"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "9"},
|
|
|
{"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "config:report-parser", "0"},
|
|
|
@@ -12580,4 +12344,132 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
log.info("已添加 parse_finished_at 列到 health_report_drafts 表");
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
|
|
|
+ /**
|
|
|
+ * 迁移378: 创建 member_indicator_current 表
|
|
|
+ * 成员维度「当前指标 + 相对上次变化」:
|
|
|
+ * - 报告确认入库后,取该成员最新一份报告 vs 次新报告的同名指标,计算 delta
|
|
|
+ * - 方案生成(LangGraph)与前端指标卡片可直接读取,无需每次跨表 diff
|
|
|
+ * 幂等:CREATE TABLE IF NOT EXISTS
|
|
|
+ */
|
|
|
+ private void addMemberIndicatorCurrentTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "CREATE TABLE IF NOT EXISTS `member_indicator_current` (" +
|
|
|
+ "`id` bigint NOT NULL AUTO_INCREMENT," +
|
|
|
+ "`member_id` bigint NOT NULL COMMENT '家庭成员ID(family_members.id)'," +
|
|
|
+ "`definition_id` bigint NOT NULL COMMENT '指标定义ID(indicator_definitions.id)'," +
|
|
|
+ "`domain` varchar(50) NOT NULL DEFAULT 'physical' COMMENT '指标域(body/mind/physical/wisdom)'," +
|
|
|
+ "`category` varchar(50) DEFAULT NULL COMMENT '指标分类'," +
|
|
|
+ "`name` varchar(100) NOT NULL COMMENT '指标名称'," +
|
|
|
+ "`value` varchar(200) DEFAULT NULL COMMENT '最新值'," +
|
|
|
+ "`numeric_value` decimal(12,4) DEFAULT NULL COMMENT '最新数值(可计算)'," +
|
|
|
+ "`previous_value` varchar(200) DEFAULT NULL COMMENT '上一份报告的同类指标值'," +
|
|
|
+ "`previous_numeric` decimal(12,4) DEFAULT NULL COMMENT '上一份报告数值'," +
|
|
|
+ "`delta` decimal(12,4) DEFAULT NULL COMMENT 'numeric_value - previous_numeric'," +
|
|
|
+ "`trend` varchar(16) DEFAULT NULL COMMENT '变化方向: up/down/flat/new/na'," +
|
|
|
+ "`latest_report_id` bigint DEFAULT NULL COMMENT '最新来源报告ID'," +
|
|
|
+ "`previous_report_id` bigint DEFAULT NULL COMMENT '上次来源报告ID'," +
|
|
|
+ "`report_date` date DEFAULT NULL COMMENT '最新报告日期'," +
|
|
|
+ "`recorded_at` datetime DEFAULT CURRENT_TIMESTAMP," +
|
|
|
+ "`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
|
|
|
+ "PRIMARY KEY (`id`)," +
|
|
|
+ "UNIQUE KEY `uk_member_definition` (`member_id`, `definition_id`)," +
|
|
|
+ "KEY `idx_member_domain` (`member_id`, `domain`)," +
|
|
|
+ "KEY `idx_report` (`latest_report_id`)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员指标当前值+相对上次变化'");
|
|
|
+ log.info("迁移378: member_indicator_current 表就绪");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移378: 创建 member_indicator_current 表失败或已存在: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addUserContentWeightsTable() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_content_weights (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '用户ID(users.id)'," +
|
|
|
+ "family_id BIGINT NOT NULL COMMENT '家庭ID'," +
|
|
|
+ "content_type VARCHAR(16) NOT NULL COMMENT '内容类型: product/activity/article'," +
|
|
|
+ "item_id BIGINT NOT NULL COMMENT '内容ID(商品ID/活动ID/文章ID)'," +
|
|
|
+ "dimension_code VARCHAR(32) DEFAULT NULL COMMENT '维度编码: body/mind/wisdom/action/wealth'," +
|
|
|
+ "weight DECIMAL(5,2) NOT NULL DEFAULT 1.00 COMMENT '权重值 0.00-10.00,越大越优先'," +
|
|
|
+ "reason VARCHAR(200) DEFAULT NULL COMMENT '权重来源: user_set/auto_click/auto_purchase/manual/auto_profile'," +
|
|
|
+ "created_by BIGINT DEFAULT NULL COMMENT '创建人ID(manual场景记录运营人员)'," +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
|
|
|
+ "INDEX idx_user_content (user_id, content_type, item_id)," +
|
|
|
+ "INDEX idx_family_dimension (family_id, dimension_code)," +
|
|
|
+ "UNIQUE KEY uk_user_item (user_id, content_type, item_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户内容偏好权重表'");
|
|
|
+ log.info("迁移374: 已创建/确认 user_content_weights 表");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移374: 创建 user_content_weights 表失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移375: 修复 article_quiz_records 表结构(旧表由迁移299创建时已存在导致缺列)
|
|
|
+ private void fixArticleQuizRecordsTable() {
|
|
|
+ try {
|
|
|
+ if (!columnExists("article_quiz_records", "member_id")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN member_id BIGINT DEFAULT NULL COMMENT '家庭成员ID(作答人)'");
|
|
|
+ log.info("迁移375: 已添加 member_id 列到 article_quiz_records");
|
|
|
+ }
|
|
|
+ if (!columnExists("article_quiz_records", "total_questions")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN total_questions INT DEFAULT 0 COMMENT '题目总数'");
|
|
|
+ log.info("迁移375: 已添加 total_questions 列到 article_quiz_records");
|
|
|
+ }
|
|
|
+ if (!columnExists("article_quiz_records", "cf_earned")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN cf_earned INT DEFAULT 0 COMMENT '发放的CF值'");
|
|
|
+ log.info("迁移375: 已添加 cf_earned 列到 article_quiz_records");
|
|
|
+ }
|
|
|
+ if (!columnExists("article_quiz_records", "energy_earned")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN energy_earned INT DEFAULT 0 COMMENT '发放的能量值'");
|
|
|
+ log.info("迁移375: 已添加 energy_earned 列到 article_quiz_records");
|
|
|
+ }
|
|
|
+ // questions/answers 旧表为 VARCHAR(255),实体存 JSON 快照会超长,与 schema.sql 对齐为 TEXT
|
|
|
+ if (!columnTypeStartsWith("article_quiz_records", "questions", "text")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records MODIFY COLUMN questions TEXT COMMENT '题目快照JSON(含正确答案与维度)'");
|
|
|
+ log.info("迁移375: questions 列已改为 TEXT");
|
|
|
+ }
|
|
|
+ if (!columnTypeStartsWith("article_quiz_records", "answers", "text")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records MODIFY COLUMN answers TEXT COMMENT '用户作答JSON(含每题所选选项),作答前为NULL'");
|
|
|
+ log.info("迁移375: answers 列已改为 TEXT");
|
|
|
+ }
|
|
|
+ // 补充索引(幂等;重复创建会报 Duplicate key name,捕获忽略)
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD INDEX idx_member (member_id)");
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ log.info("迁移375: article_quiz_records 表结构修复完成");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移375: 修复 article_quiz_records 表结构失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean columnTypeStartsWith(String table, String column, String typePrefix) {
|
|
|
+ try {
|
|
|
+ String t = jdbcTemplate.queryForObject(
|
|
|
+ "SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
|
|
|
+ String.class, table, column);
|
|
|
+ return t != null && t.toLowerCase().startsWith(typePrefix);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移372: nutrition_product 添加 efficacy_tags 列(成分功效标签)
|
|
|
+ private void addNutritionProductEfficacyTags() {
|
|
|
+ if (!columnExists("nutrition_product", "efficacy_tags")) {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE nutrition_product ADD COLUMN efficacy_tags TEXT COMMENT '成分功效标签JSON数组(如 [\"补钙\",\"补锌\",\"益生菌\",\"维生素D\"])'");
|
|
|
+ log.info("已添加 efficacy_tags 列到 nutrition_product 表");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 迁移373: products 添加 tags 列(文章-商品标签关联推荐)
|
|
|
+ private void addProductTagsColumn() {
|
|
|
+ ensureColumn("products", "tags", "VARCHAR(500) DEFAULT '' COMMENT '商品标签 JSON 数组,用于文章-商品标签匹配推荐'");
|
|
|
+ log.info("迁移373: products.tags 已确认存在");
|
|
|
+ }
|
|
|
}
|