From d6e922cc2f0153c1e07a80aa8d843f435cbad0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E7=A0=81-=E6=96=B9=E6=99=93=E8=BE=89?= Date: Fri, 6 Feb 2026 10:17:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3(OSHI)=E3=80=81=E8=8F=9C=E5=8D=95=E5=88=A0?= =?UTF-8?q?=E9=99=A4SQL=E3=80=81=E5=89=8D=E7=AB=AF=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E9=A1=B5=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /monitor/server 接口:ServerController + ServerVo,OSHI 采集 CPU/内存/JVM/磁盘 - 根 pom 与 pangu-system 增加 oshi-core 依赖 - 前端服务监控页:请求失败时关闭 loading 并提示 - SQL:V1.0.6 应用子菜单、V1.0.7 隐藏系统菜单、V1.0.8 删除系统菜单、V1.0.9 删除租户按钮菜单 - 发布计划与 PROD SQL 变更检查文档、年级/区域等脚本与业务调整 Co-authored-by: Cursor --- backend/.flattened-pom.xml | 6 + .../pangu-business/sql/pangu_menu.sql | 26 ++- .../pangu-business/sql/pangu_tables.sql | 2 +- .../h5/service/impl/H5AuthServiceImpl.java | 87 ++++---- .../pangu/openapi/domain/vo/OpenSchoolVo.java | 2 +- .../dromara/pangu/school/domain/PgSchool.java | 2 +- .../pangu-system/.flattened-pom.xml | 4 + backend/pangu-modules/pangu-system/pom.xml | 6 + .../controller/monitor/ServerController.java | 206 ++++++++++++++++++ .../dromara/system/domain/vo/ServerVo.java | 90 ++++++++ backend/pom.xml | 9 + docs/发布计划_2026-02-05.md | 46 ++-- docs/发布计划_PROD_SQL脚本变更检查.md | 102 +++++++++ .../src/views/business/base/region/index.vue | 63 +++++- .../school/components/SchoolDialog.vue | 16 +- frontend/src/views/monitor/server/index.vue | 3 + scripts/clear-h5-captcha-cache.sh | 36 +++ scripts/sql/V1.0.4__grade_add_stage.sql | 48 ++-- scripts/sql/V1.0.5__clean_region_data.sql | 32 ++- .../sql/V1.0.6__menu_application_submenu.sql | 25 +++ scripts/sql/V1.0.7__hide_system_menus.sql | 26 +++ scripts/sql/V1.0.8__delete_system_menus.sql | 31 +++ .../V1.0.9__delete_tenant_button_menus.sql | 15 ++ 23 files changed, 774 insertions(+), 109 deletions(-) create mode 100644 backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/controller/monitor/ServerController.java create mode 100644 backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/domain/vo/ServerVo.java create mode 100644 docs/发布计划_PROD_SQL脚本变更检查.md create mode 100644 scripts/clear-h5-captcha-cache.sh create mode 100644 scripts/sql/V1.0.6__menu_application_submenu.sql create mode 100644 scripts/sql/V1.0.7__hide_system_menus.sql create mode 100644 scripts/sql/V1.0.8__delete_system_menus.sql create mode 100644 scripts/sql/V1.0.9__delete_tenant_button_menus.sql diff --git a/backend/.flattened-pom.xml b/backend/.flattened-pom.xml index c97e1af..ce445d4 100644 --- a/backend/.flattened-pom.xml +++ b/backend/.flattened-pom.xml @@ -28,6 +28,7 @@ 1.5.0 UTF-8 1.2.83 + 6.4.0 true 8.7.3-20251210 0.15.0 @@ -237,6 +238,11 @@ ip2region ${ip2region.version} + + com.github.oshi + oshi-core + ${oshi.version} + com.alibaba fastjson diff --git a/backend/pangu-modules/pangu-business/sql/pangu_menu.sql b/backend/pangu-modules/pangu-business/sql/pangu_menu.sql index cdffd15..5b1706b 100644 --- a/backend/pangu-modules/pangu-business/sql/pangu_menu.sql +++ b/backend/pangu-modules/pangu-business/sql/pangu_menu.sql @@ -45,15 +45,25 @@ INSERT INTO sys_menu VALUES (2204, '学生删除', 2200, 4, '', '', '', 1, 0, 'F INSERT INTO sys_menu VALUES (2205, '学生导入', 2200, 5, '', '', '', 1, 0, 'F', '0', '0', 'business:student:import', '#', 103, 1, NOW(), NULL, NULL, ''); INSERT INTO sys_menu VALUES (2206, '学生导出', 2200, 6, '', '', '', 1, 0, 'F', '0', '0', 'business:student:export', '#', 103, 1, NOW(), NULL, NULL, ''); --- ===================== 应用管理(一级菜单,order_num=4) ===================== +-- ===================== 应用管理(一级目录,order_num=4,下挂 应用列表 + 接口字典) ===================== INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) -VALUES (2300, '应用管理', 0, 4, 'application', 'business/application/index', '', 1, 0, 'C', '0', '0', 'business:application:list', 'component', 103, 1, NOW()); -INSERT INTO sys_menu VALUES (2301, '应用查询', 2300, 1, '', '', '', 1, 0, 'F', '0', '0', 'business:application:query', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu VALUES (2302, '应用新增', 2300, 2, '', '', '', 1, 0, 'F', '0', '0', 'business:application:add', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu VALUES (2303, '应用修改', 2300, 3, '', '', '', 1, 0, 'F', '0', '0', 'business:application:edit', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu VALUES (2304, '应用删除', 2300, 4, '', '', '', 1, 0, 'F', '0', '0', 'business:application:remove', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu VALUES (2305, '重置密钥', 2300, 5, '', '', '', 1, 0, 'F', '0', '0', 'business:application:resetSecret', '#', 103, 1, NOW(), NULL, NULL, ''); -INSERT INTO sys_menu VALUES (2306, '接口授权', 2300, 6, '', '', '', 1, 0, 'F', '0', '0', 'business:application:api', '#', 103, 1, NOW(), NULL, NULL, ''); +VALUES (2300, '应用管理', 0, 4, 'application', NULL, '', 1, 0, 'M', '0', '0', '', 'component', 103, 1, NOW()); +-- 应用列表(二级菜单) +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) +VALUES (2307, '应用列表', 2300, 1, 'list', 'business/application/index', '', 1, 0, 'C', '0', '0', 'business:application:list', '#', 103, 1, NOW()); +INSERT INTO sys_menu VALUES (2301, '应用查询', 2307, 1, '', '', '', 1, 0, 'F', '0', '0', 'business:application:query', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2302, '应用新增', 2307, 2, '', '', '', 1, 0, 'F', '0', '0', 'business:application:add', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2303, '应用修改', 2307, 3, '', '', '', 1, 0, 'F', '0', '0', 'business:application:edit', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2304, '应用删除', 2307, 4, '', '', '', 1, 0, 'F', '0', '0', 'business:application:remove', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2305, '重置密钥', 2307, 5, '', '', '', 1, 0, 'F', '0', '0', 'business:application:resetSecret', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2306, '接口授权', 2307, 6, '', '', '', 1, 0, 'F', '0', '0', 'business:application:api', '#', 103, 1, NOW(), NULL, NULL, ''); +-- 接口字典(二级菜单) +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) +VALUES (2310, '接口字典', 2300, 2, 'apiDict', 'business/apiDict/index', '', 1, 0, 'C', '0', '0', 'business:apiDict:list', 'list', 103, 1, NOW()); +INSERT INTO sys_menu VALUES (2311, '接口字典查询', 2310, 1, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:query', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2312, '接口字典新增', 2310, 2, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:add', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2313, '接口字典修改', 2310, 3, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:edit', '#', 103, 1, NOW(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (2314, '接口字典删除', 2310, 4, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:remove', '#', 103, 1, NOW(), NULL, NULL, ''); -- ===================== 基础数据(一级目录,order_num=5) ===================== INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) diff --git a/backend/pangu-modules/pangu-business/sql/pangu_tables.sql b/backend/pangu-modules/pangu-business/sql/pangu_tables.sql index 511e197..0b8f678 100644 --- a/backend/pangu-modules/pangu-business/sql/pangu_tables.sql +++ b/backend/pangu-modules/pangu-business/sql/pangu_tables.sql @@ -151,7 +151,7 @@ CREATE TABLE `pg_school` ( `school_id` bigint NOT NULL COMMENT '学校ID', `school_code` varchar(32) NOT NULL COMMENT '学校编码', `school_name` varchar(100) NOT NULL COMMENT '学校名称', - `school_type` char(1) DEFAULT '1' COMMENT '学校类型(1小学 2初中 3高中 4完全中学)', + `school_type` char(1) DEFAULT '1' COMMENT '学校类型(1小学 2初中 3高中 4中专 5大学)', `region_id` bigint DEFAULT NULL COMMENT '所属区域ID', `region_path` varchar(500) DEFAULT NULL COMMENT '区域路径', `address` varchar(500) DEFAULT NULL COMMENT '详细地址', diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java index 66542f3..b2035d9 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5AuthServiceImpl.java @@ -216,27 +216,33 @@ public class H5AuthServiceImpl implements H5AuthService { // 发送短信 if (smsProperties.isEnabled()) { - try { - LinkedHashMap map = new LinkedHashMap<>(1); - map.put("code", code); - SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName()); - String templateId = "login".equals(type) ? smsProperties.getLoginTemplateId() : smsProperties.getRegisterTemplateId(); - SmsResponse smsResponse; - if (StringUtils.isNotBlank(templateId)) { - smsResponse = smsBlend.sendMessage(phone, templateId, map); - } else { - smsResponse = smsBlend.sendMessage(phone, map); - } - if (!smsResponse.isSuccess()) { - log.error("短信发送失败: phone={}, response={}", phone, smsResponse); + SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName()); + if (smsBlend == null) { + // 配置了启用但未配置短信实现(如生产未配 sms4j)时降级为测试模式,避免 NPE,验证码已写入 Redis 可正常登录 + log.warn("短信服务未配置(smsBlend=null),降级为测试模式,验证码仅写入Redis并打印日志"); + log.info("【测试模式】短信验证码: phone={}, code={}, type={}", phone, code, type); + } else { + try { + LinkedHashMap map = new LinkedHashMap<>(1); + map.put("code", code); + String templateId = "login".equals(type) ? smsProperties.getLoginTemplateId() : smsProperties.getRegisterTemplateId(); + SmsResponse smsResponse; + if (StringUtils.isNotBlank(templateId)) { + smsResponse = smsBlend.sendMessage(phone, templateId, map); + } else { + smsResponse = smsBlend.sendMessage(phone, map); + } + if (!smsResponse.isSuccess()) { + log.error("短信发送失败: phone={}, response={}", phone, smsResponse); + throw new ServiceException("短信发送失败,请稍后重试"); + } + log.info("短信发送成功: phone={}, type={}, ip={}", phone, type, clientIp); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("短信发送异常: phone={}, error={}", phone, e.getMessage()); throw new ServiceException("短信发送失败,请稍后重试"); } - log.info("短信发送成功: phone={}, type={}, ip={}", phone, type, clientIp); - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - log.error("短信发送异常: phone={}, error={}", phone, e.getMessage()); - throw new ServiceException("短信发送失败,请稍后重试"); } } else { // 测试模式,打印验证码到日志 @@ -823,27 +829,32 @@ public class H5AuthServiceImpl implements H5AuthService { incrementCounter(dailyPhoneKey, Duration.ofDays(1)); if (smsProperties.isEnabled()) { - try { - LinkedHashMap map = new LinkedHashMap<>(1); - map.put("code", code); - SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName()); - String templateId = smsProperties.getLoginTemplateId(); - SmsResponse smsResponse; - if (StringUtils.isNotBlank(templateId)) { - smsResponse = smsBlend.sendMessage(phone, templateId, map); - } else { - smsResponse = smsBlend.sendMessage(phone, map); - } - if (!smsResponse.isSuccess()) { - log.error("短信发送失败: phone={}, response={}", phone, smsResponse); + SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName()); + if (smsBlend == null) { + log.warn("短信服务未配置(smsBlend=null),降级为测试模式"); + log.info("【测试模式】微信绑定短信验证码: phone={}, code={}", phone, code); + } else { + try { + LinkedHashMap map = new LinkedHashMap<>(1); + map.put("code", code); + String templateId = smsProperties.getLoginTemplateId(); + SmsResponse smsResponse; + if (StringUtils.isNotBlank(templateId)) { + smsResponse = smsBlend.sendMessage(phone, templateId, map); + } else { + smsResponse = smsBlend.sendMessage(phone, map); + } + if (!smsResponse.isSuccess()) { + log.error("短信发送失败: phone={}, response={}", phone, smsResponse); + throw new ServiceException("短信发送失败,请稍后重试"); + } + log.info("微信绑定短信发送成功: phone={}, ip={}", phone, clientIp); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("短信发送异常: phone={}, error={}", phone, e.getMessage()); throw new ServiceException("短信发送失败,请稍后重试"); } - log.info("微信绑定短信发送成功: phone={}, ip={}", phone, clientIp); - } catch (ServiceException e) { - throw e; - } catch (Exception e) { - log.error("短信发送异常: phone={}, error={}", phone, e.getMessage()); - throw new ServiceException("短信发送失败,请稍后重试"); } } else { log.info("【测试模式】微信绑定短信验证码: phone={}, code={}", phone, code); diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/openapi/domain/vo/OpenSchoolVo.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/openapi/domain/vo/OpenSchoolVo.java index c3db8e5..97286c5 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/openapi/domain/vo/OpenSchoolVo.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/openapi/domain/vo/OpenSchoolVo.java @@ -23,7 +23,7 @@ public class OpenSchoolVo implements Serializable { @Schema(description = "学校名称") private String schoolName; - @Schema(description = "学校类型(1小学 2初中 3高中 4完全中学)") + @Schema(description = "学校类型(1小学 2初中 3高中 4中专 5大学)") private String schoolType; @Schema(description = "区域ID") diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchool.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchool.java index 40b2f67..f4304c0 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchool.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchool.java @@ -27,7 +27,7 @@ public class PgSchool extends BaseEntity { private String schoolName; /** - * 学校类型(1小学 2初中 3高中 4完全中学) + * 学校类型(1小学 2初中 3高中 4中专 5大学) */ private String schoolType; diff --git a/backend/pangu-modules/pangu-system/.flattened-pom.xml b/backend/pangu-modules/pangu-system/.flattened-pom.xml index ade9960..40d84c4 100644 --- a/backend/pangu-modules/pangu-system/.flattened-pom.xml +++ b/backend/pangu-modules/pangu-system/.flattened-pom.xml @@ -76,5 +76,9 @@ org.dromara pangu-common-sse + + com.github.oshi + oshi-core + diff --git a/backend/pangu-modules/pangu-system/pom.xml b/backend/pangu-modules/pangu-system/pom.xml index b1b444b..cc1abcd 100644 --- a/backend/pangu-modules/pangu-system/pom.xml +++ b/backend/pangu-modules/pangu-system/pom.xml @@ -100,6 +100,12 @@ pangu-common-sse + + + com.github.oshi + oshi-core + + diff --git a/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/controller/monitor/ServerController.java b/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/controller/monitor/ServerController.java new file mode 100644 index 0000000..8edec66 --- /dev/null +++ b/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/controller/monitor/ServerController.java @@ -0,0 +1,206 @@ +package org.dromara.system.controller.monitor; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import lombok.extern.slf4j.Slf4j; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import org.dromara.common.core.domain.R; +import org.dromara.system.domain.vo.ServerVo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.RuntimeMXBean; +import java.net.InetAddress; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * 服务监控 + * + * @author pangu + */ +@Slf4j +@RestController +@RequestMapping("/monitor/server") +public class ServerController { + + private static final int WAIT_MS = 300; + + /** + * 获取服务监控信息(CPU、内存、JVM、服务器信息、磁盘) + */ + @SaCheckPermission("monitor:server:list") + @GetMapping() + public R getInfo() { + try { + ServerVo server = new ServerVo(); + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + OperatingSystem os = si.getOperatingSystem(); + + // CPU + server.setCpu(buildCpu(hal.getProcessor())); + + // 内存 + server.setMem(buildMem(hal.getMemory())); + + // JVM + server.setJvm(buildJvm()); + + // 服务器信息 + server.setSys(buildSys()); + + // 磁盘 + server.setSysFiles(buildSysFiles(os)); + + return R.ok(server); + } catch (Exception e) { + log.warn("获取服务监控信息失败", e); + return R.fail("获取服务监控信息失败: " + e.getMessage()); + } + } + + private static ServerVo.CpuVo buildCpu(CentralProcessor processor) { + ServerVo.CpuVo cpu = new ServerVo.CpuVo(); + cpu.setCpuNum(processor.getLogicalProcessorCount()); + long[][] prevTicks = processor.getProcessorCpuLoadTicks(); + try { + Thread.sleep(WAIT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + double[] loadPerCpu = processor.getProcessorCpuLoadBetweenTicks(prevTicks); + double load = 0; + if (loadPerCpu != null && loadPerCpu.length > 0) { + for (double v : loadPerCpu) load += v; + load = load / loadPerCpu.length; + } + double used = load >= 0 ? load * 100 : 0; + cpu.setUsed(round(used, 2)); + cpu.setSys(0.0); + cpu.setFree(round(100 - used, 2)); + return cpu; + } + + private static ServerVo.MemVo buildMem(GlobalMemory memory) { + ServerVo.MemVo mem = new ServerVo.MemVo(); + long total = memory.getTotal(); + long available = memory.getAvailable(); + long used = total - available; + double totalG = total / (1024.0 * 1024.0 * 1024.0); + double usedG = used / (1024.0 * 1024.0 * 1024.0); + double freeG = available / (1024.0 * 1024.0 * 1024.0); + mem.setTotal(round(totalG, 2)); + mem.setUsed(round(usedG, 2)); + mem.setFree(round(freeG, 2)); + mem.setUsage(total > 0 ? round(used * 100.0 / total, 2) : 0.0); + return mem; + } + + private static ServerVo.JvmVo buildJvm() { + ServerVo.JvmVo jvm = new ServerVo.JvmVo(); + Runtime runtime = Runtime.getRuntime(); + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); + + long total = memoryMXBean.getHeapMemoryUsage().getMax() > 0 + ? memoryMXBean.getHeapMemoryUsage().getMax() + : runtime.totalMemory(); + long used = memoryMXBean.getHeapMemoryUsage().getUsed(); + long free = total - used; + + double totalM = total / (1024.0 * 1024.0); + double usedM = used / (1024.0 * 1024.0); + double freeM = free / (1024.0 * 1024.0); + + jvm.setTotal(round(totalM, 2)); + jvm.setUsed(round(usedM, 2)); + jvm.setFree(round(freeM, 2)); + jvm.setUsage(total > 0 ? round(used * 100.0 / total, 2) : 0.0); + jvm.setName(runtimeMXBean.getVmName()); + jvm.setVersion(System.getProperty("java.version")); + jvm.setStartTime(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date(runtimeMXBean.getStartTime()))); + long runMs = System.currentTimeMillis() - runtimeMXBean.getStartTime(); + jvm.setRunTime(formatRunTime(runMs)); + jvm.setHome(System.getProperty("java.home")); + jvm.setInputArgs(runtimeMXBean.getInputArguments().toString()); + return jvm; + } + + private static ServerVo.SysVo buildSys() { + ServerVo.SysVo sys = new ServerVo.SysVo(); + try { + InetAddress addr = InetAddress.getLocalHost(); + sys.setComputerName(addr.getHostName()); + sys.setComputerIp(addr.getHostAddress()); + } catch (Exception e) { + sys.setComputerName("未知"); + sys.setComputerIp("未知"); + } + sys.setOsName(System.getProperty("os.name")); + sys.setOsArch(System.getProperty("os.arch")); + sys.setUserDir(System.getProperty("user.dir")); + return sys; + } + + private static List buildSysFiles(OperatingSystem os) { + List list = new ArrayList<>(); + try { + for (OSFileStore fs : os.getFileSystem().getFileStores()) { + ServerVo.SysFileVo vo = new ServerVo.SysFileVo(); + vo.setDirName(fs.getMount()); + vo.setSysTypeName(fs.getType()); + vo.setTypeName(fs.getName()); + long total = fs.getTotalSpace(); + long free = fs.getUsableSpace(); + long used = total - free; + vo.setTotal(formatSize(total)); + vo.setFree(formatSize(free)); + vo.setUsed(formatSize(used)); + vo.setUsage(total > 0 ? round(used * 100.0 / total, 2) : 0.0); + list.add(vo); + } + } catch (Exception e) { + log.debug("获取磁盘信息失败", e); + } + return list; + } + + private static double round(double value, int scale) { + return Math.round(value * Math.pow(10, scale)) / Math.pow(10, scale); + } + + private static String formatSize(long size) { + double g = size / (1024.0 * 1024.0 * 1024.0); + if (g >= 1) { + return new DecimalFormat("#.##").format(g) + "G"; + } + double m = size / (1024.0 * 1024.0); + if (m >= 1) { + return new DecimalFormat("#.##").format(m) + "M"; + } + return size / 1024 + "K"; + } + + private static String formatRunTime(long ms) { + long day = ms / (24 * 3600 * 1000); + long hour = (ms % (24 * 3600 * 1000)) / (3600 * 1000); + long minute = (ms % (3600 * 1000)) / (60 * 1000); + long second = (ms % (60 * 1000)) / 1000; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hour > 0) sb.append(hour).append("小时"); + if (minute > 0) sb.append(minute).append("分钟"); + sb.append(second).append("秒"); + return sb.toString(); + } +} diff --git a/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/domain/vo/ServerVo.java b/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/domain/vo/ServerVo.java new file mode 100644 index 0000000..103e97d --- /dev/null +++ b/backend/pangu-modules/pangu-system/src/main/java/org/dromara/system/domain/vo/ServerVo.java @@ -0,0 +1,90 @@ +package org.dromara.system.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 服务监控信息视图对象(与前端 /monitor/server 页面对应) + * + * @author pangu + */ +@Data +public class ServerVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** CPU 信息 */ + private CpuVo cpu; + /** 内存信息 */ + private MemVo mem; + /** JVM 信息 */ + private JvmVo jvm; + /** 服务器系统信息 */ + private SysVo sys; + /** 磁盘列表 */ + private List sysFiles; + + @Data + public static class CpuVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Integer cpuNum; + private Double used; + private Double sys; + private Double free; + } + + @Data + public static class MemVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Double total; + private Double used; + private Double free; + private Double usage; + } + + @Data + public static class JvmVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Double total; + private Double used; + private Double free; + private Double usage; + private String name; + private String version; + private String startTime; + private String runTime; + private String home; + private String inputArgs; + } + + @Data + public static class SysVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private String computerName; + private String computerIp; + private String osName; + private String osArch; + private String userDir; + } + + @Data + public static class SysFileVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private String dirName; + private String sysTypeName; + private String typeName; + private String total; + private String free; + private String used; + private Double usage; + } +} diff --git a/backend/pom.xml b/backend/pom.xml index cac50f5..6fe2781 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -49,6 +49,8 @@ 8.7.3-20251210 1.8.4 + + 6.4.0 3.4.2 @@ -326,6 +328,13 @@ ${ip2region.version} + + + com.github.oshi + oshi-core + ${oshi.version} + + com.alibaba fastjson diff --git a/docs/发布计划_2026-02-05.md b/docs/发布计划_2026-02-05.md index fa022c7..1c51be5 100644 --- a/docs/发布计划_2026-02-05.md +++ b/docs/发布计划_2026-02-05.md @@ -11,8 +11,8 @@ | 项目 | 本地最新版本 | 服务器当前版本 | 待发布提交数 | |------|-------------|---------------|-------------| -| pangu-user-platform (后端+管理后台) | add00c9 | 2026-02-03 22:29 部署 | 5 个 | -| user_authentication_center_front (H5前端) | 70fc1ad | 2026-02-03 22:04 部署 | 2 个 | +| pangu-user-platform (后端+管理后台) | a9bbb30 | 2026-02-03 22:29 部署 | 6 个 | +| user_authentication_center_front (H5前端) | 76f27ce | 2026-02-03 22:04 部署 | 3 个 | --- @@ -22,28 +22,33 @@ | 序号 | Commit ID | 提交说明 | 类型 | |------|----------|---------|------| -| 1 | add00c9 | 新增学校自动添加年级 + 修复区域层级查询 + 清理区域数据 | feat | -| 2 | 72cb666 | 年级管理增加学段字段(小学/初中/高中/中专/大学) | feat | -| 3 | 80dd406 | 新增OpenApi基础数据接口 + 学生完整数据接口 + UI文案优化 | feat | -| 4 | 1a0b75e | 同步需求与技术方案文档 | docs | -| 5 | 6027a8c | 修改后端欢迎语为盘古后台管理系统 | refactor | +| 1 | a9bbb30 | 业务功能按钮权限统一(v-hasPermi,与角色配置一致) | fix | +| 2 | add00c9 | 新增学校自动添加年级 + 修复区域层级查询 + 清理区域数据 | feat | +| 3 | 72cb666 | 年级管理增加学段字段(小学/初中/高中/中专/大学) | feat | +| 4 | 80dd406 | 新增OpenApi基础数据接口 + 学生完整数据接口 + UI文案优化 | feat | +| 5 | 1a0b75e | 同步需求与技术方案文档 | docs | +| 6 | 6027a8c | 修改后端欢迎语为盘古后台管理系统 | refactor | ### 2.2 功能变更摘要 -1. **学校管理优化** +1. **业务功能按钮权限统一(本次新增)** + - 学生、会员、学校、应用管理、接口字典、年级、班级、学科、区域等列表页的「新增」「编辑」「删除」「导入」「导出」等按钮增加 `v-hasPermi` 控制 + - 与角色管理中配置的菜单权限一致,无权限用户不显示对应按钮 + +2. **学校管理优化** - 新增学校时自动添加对应学段的年级 - 修复选择省/市时无法显示学校的Bug(支持区域层级查询) - 区域树默认只展开湖北省,平行显示市级 -2. **年级管理增强** +3. **年级管理增强** - 新增学段字段(小学/初中/高中/中专/大学) - 支持按学段筛选年级 -3. **OpenAPI接口扩展** +4. **OpenAPI接口扩展** - 新增学校/年级/班级基础数据查询接口 - 新增学生完整数据接口(不脱敏,需特殊授权) -4. **UI文案优化** +5. **UI文案优化** - "教育身份" 改为 "任教信息" - 后端欢迎语改为 "盘古后台管理系统" @@ -91,16 +96,22 @@ | 序号 | Commit ID | 提交说明 | 类型 | |------|----------|---------|------| -| 1 | 70fc1ad | 优化登录注册页面交互和弹窗内容 | feat | -| 2 | 842d64f | 允许通过IP地址访问H5前端 | fix | +| 1 | 76f27ce | 会员支持所在区域:注册和个人中心可填可改,教育身份和绑定学生表单默认带出区域 | feat | +| 2 | 70fc1ad | 优化登录注册页面交互和弹窗内容 | feat | +| 3 | 842d64f | 允许通过IP地址访问H5前端 | fix | ### 3.2 功能变更摘要 -1. **登录注册优化** +1. **会员所在区域(本次新增)** + - 注册时增加所在区域选择(级联) + - 个人中心可查看、修改所在区域 + - 添加教育身份、绑定学生时区域默认带出会员区域,可修改 + +2. **登录注册优化** - 优化登录注册页面交互体验 - 改进弹窗内容展示 -2. **访问限制修复** +3. **访问限制修复** - 允许通过IP地址直接访问H5前端 ### 3.3 服务器部署路径 @@ -195,11 +206,13 @@ cd /opt/pangu-user-platform/scripts |--------|---------|---------| | 后端服务 | `curl http://localhost:9083/actuator/health` | {"status":"UP"} | | 管理后台登录 | 浏览器访问管理后台 | 正常登录 | +| 权限按钮显隐 | 用无增删改权限账号登录,进入学生/会员等列表 | 仅显示查询,不显示新增/编辑/删除等按钮 | | 学校管理-区域树 | 查看区域树展开状态 | 默认展开湖北省 | | 学校管理-层级查询 | 选择武汉市查看学校 | 显示武汉市下所有学校 | | 年级管理-学段 | 查看年级列表 | 显示学段列 | | OpenAPI接口 | 调用新增的接口 | 正常返回数据 | | H5前端登录 | 浏览器访问H5 | 正常登录 | +| H5会员区域 | 注册选区域、个人中心改区域,教育身份/绑定学生表单 | 区域可选且默认带出会员区域 | --- @@ -287,4 +300,5 @@ mysql -h 8.148.25.55 -uroot -paly2024A pguser-db < /opt/backup/pg_region_YYYYMMD --- -*文档生成时间: 2026-02-05* +*文档生成时间: 2026-02-05* +*更新说明: 已纳入业务功能按钮权限统一(a9bbb30)与 H5 会员所在区域(76f27ce)两项提交。* diff --git a/docs/发布计划_PROD_SQL脚本变更检查.md b/docs/发布计划_PROD_SQL脚本变更检查.md new file mode 100644 index 0000000..1c7ef75 --- /dev/null +++ b/docs/发布计划_PROD_SQL脚本变更检查.md @@ -0,0 +1,102 @@ +# PROD 发布 - SQL 脚本变更检查 + +**检查日期**: 2026-02-05 +**目标**: 部署前确认 PROD 库需执行的增量脚本及执行顺序 + +--- + +## 一、本次发布涉及的 SQL 脚本(scripts/sql/) + +| 序号 | 脚本文件 | 说明 | 涉及表 | 风险 | 是否幂等 | +|------|----------|------|--------|------|----------| +| 1 | V1.0.3__open_api_dict.sql | 开放API接口字典数据 | pg_api_dict | 低 | 是(ON DUPLICATE KEY UPDATE) | +| 2 | V1.0.4__grade_add_stage.sql | 年级表增加学段字段 | pg_grade | 中 | 否(重复执行会报错:列已存在) | +| 3 | V1.0.5__clean_region_data.sql | 清理非湖北省区域数据 | pg_region | 高 | 否(物理删除,不可逆) | + +--- + +## 二、各脚本变更内容摘要 + +### V1.0.3__open_api_dict.sql + +- **操作类型**: INSERT(存在则 UPDATE) +- **变更内容**: + - 学生:OPEN_STUDENT_LIST、OPEN_STUDENT_LIST_FULL(2 条) + - 学校:OPEN_SCHOOL_LIST、OPEN_SCHOOL_LIST_ALL、OPEN_SCHOOL_INFO(3 条) + - 年级:OPEN_GRADE_LIST、OPEN_GRADE_LIST_ALL、OPEN_GRADE_INFO(3 条) + - 班级:OPEN_CLASS_LIST、OPEN_CLASS_LIST_ALL、OPEN_CLASS_INFO(3 条) +- **共 11 条** pg_api_dict 记录 +- **执行前**: 确认 PROD 存在表 `pg_api_dict`,且主键/唯一约束与脚本一致(api_id 或 api_code) +- **重复执行**: 安全,仅更新名称/路径/描述 + +### V1.0.4__grade_add_stage.sql + +- **操作类型**: ALTER TABLE + UPDATE +- **变更内容**: + 1. `ALTER TABLE pg_grade ADD COLUMN stage CHAR(1) NULL COMMENT '学段(1小学 2初中 3高中 4中专 5大学)' AFTER grade_name;` + 2. 按年级名称回填:小学(1)/初中(2)/高中(3),未匹配默认小学(1) + 3. `ALTER TABLE pg_grade MODIFY COLUMN stage ... NOT NULL DEFAULT '1';` +- **执行前**: 必须确认 PROD 的 `pg_grade` **没有** `stage` 列,否则跳过或先判断列是否存在 +- **重复执行**: 会报错(列已存在),需做存在性检查或仅执行一次 + +### V1.0.5__clean_region_data.sql + +- **操作类型**: 物理 DELETE +- **变更内容**: + - 删除所有「非湖北省及其下级」的区域(ancestors 不以 `0,42` 开头,且 region_id≠42) + - 再删除其他省份根节点(parent_id=0 且 region_id≠42) +- **执行前**: 必须备份 `pg_region`;确认湖北省 region_id=42;建议业务低峰期、单独审批后执行 +- **重复执行**: 可执行(已删的不会再删),但不可逆 + +--- + +## 三、PROD 执行前必做检查 + +1. **确认表是否存在** + - `pg_api_dict`、`pg_grade`、`pg_region` 在 PROD 是否存在且结构一致。 + +2. **确认是否已执行过** + - **V1.0.4**:在 PROD 执行 + `SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='pg_grade' AND COLUMN_NAME='stage';` + - 若结果为 1,说明已有 `stage`,**不要**再执行 V1.0.4。 + - **V1.0.3**:可重复执行,无需跳过。 + +3. **V1.0.5 是否在本次执行** + - 若 PROD 需保留多省数据,**不要**执行 V1.0.5。 + - 若只保留湖北省,执行前必须备份: + `mysqldump -h -u<> -p pguser-db pg_region > pg_region_backup_YYYYMMDD.sql` + +4. **会员区域字段(本次 H5 会员区域功能依赖)** + - 若 PROD 的 `pg_member` 表是早期建表、**没有** `region_id` 列,需先执行增量加列(见下节),再发布应用与 H5。 + +--- + +## 四、可选:pg_member.region_id 增量脚本 + +若 PROD 的 `pg_member` 尚无 `region_id` 列,需在应用发布前执行(仅执行一次): + +```sql +-- 检查是否已有列(结果为 0 表示需要执行下面的 ADD) +-- SELECT COUNT(*) FROM information_schema.COLUMNS +-- WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='pg_member' AND COLUMN_NAME='region_id'; + +ALTER TABLE pg_member +ADD COLUMN region_id bigint DEFAULT NULL COMMENT '区域ID' AFTER open_id; +``` + +建议将上述内容保存为 `scripts/sql/V1.0.2__member_region_id.sql`(或 V1.0.6,按你们版本号规范),并在发布计划中注明执行顺序:在 V1.0.3 之前或之后均可(与 pg_grade、pg_region 无依赖)。 + +--- + +## 五、推荐执行顺序(PROD) + +| 顺序 | 脚本 | 条件 | +|------|------|------| +| 1 | V1.0.2__member_region_id.sql(若有) | 仅当 pg_member 无 region_id 时执行 | +| 2 | V1.0.3__open_api_dict.sql | 必执行 | +| 3 | V1.0.4__grade_add_stage.sql | 仅当 pg_grade 无 stage 列时执行 | +| 4 | V1.0.5__clean_region_data.sql | 仅当确定只保留湖北省且已备份 pg_region 时执行 | + +--- + +*检查说明:按上述顺序与条件执行,可避免重复加列报错与误删区域数据。* diff --git a/frontend/src/views/business/base/region/index.vue b/frontend/src/views/business/base/region/index.vue index 97b1317..b660610 100644 --- a/frontend/src/views/business/base/region/index.vue +++ b/frontend/src/views/business/base/region/index.vue @@ -29,6 +29,7 @@ { + if (!list || list.length === 0) return [] + const k = (keyword || '').trim().toLowerCase() + const filter = (nodes) => { + return nodes + .map((node) => ({ ...node, children: node.children ? filter(node.children) : [] })) + .filter((node) => { + const matchName = !k || (node.regionName && node.regionName.toLowerCase().includes(k)) + const matchStatus = !status || String(node.status) === String(status) + const childMatch = node.children && node.children.length > 0 + return (matchName && matchStatus) || childMatch + }) + } + return filter(list) +} + +// 获取区域列表(树形);有搜索条件时强制刷新并过滤 const getList = async () => { loading.value = true try { - // 使用 Store,自动缓存 - const data = await baseDataStore.fetchRegionTree() - tableData.value = data || [] + const hasQuery = (queryParams.value.regionName || '').trim() || (queryParams.value.status !== '' && queryParams.value.status !== undefined) + const data = await baseDataStore.fetchRegionTree(!!hasQuery) + let tree = data || [] + if ((queryParams.value.regionName || '').trim()) { + tree = filterTreeByKeyword(tree, queryParams.value.regionName, queryParams.value.status) + } else if (queryParams.value.status !== '' && queryParams.value.status !== undefined) { + tree = filterTreeByKeyword(tree, '', queryParams.value.status) + } + tableData.value = tree + // 默认展开湖北省及二级(只展开到市),有 tableRef 时在 nextTick 后执行 + nextTick(() => { + if (!isExpandAll.value) expandToLevel2() + }) } finally { loading.value = false } } +// 递归收集树中所有节点(扁平) +const flattenTree = (list) => { + const out = [] + const walk = (nodes) => { + if (!nodes) return + nodes.forEach((row) => { + out.push(row) + walk(row.children) + }) + } + walk(list) + return out +} + +// 默认展开一级(省)和二级(市),不展开三级及以下 +const expandToLevel2 = () => { + if (!tableRef.value || !tableData.value.length) return + const rows = flattenTree(tableData.value) + rows.forEach((row) => { + const level = row.level + if (level === 1 || level === 2) { + tableRef.value.toggleRowExpansion(row, true) + } + }) +} + // 获取区域树选项(用于下拉选择) const getRegionOptions = async () => { const data = await baseDataStore.fetchRegionTree() diff --git a/frontend/src/views/business/school/components/SchoolDialog.vue b/frontend/src/views/business/school/components/SchoolDialog.vue index 4cbf912..9a2bccd 100644 --- a/frontend/src/views/business/school/components/SchoolDialog.vue +++ b/frontend/src/views/business/school/components/SchoolDialog.vue @@ -23,8 +23,8 @@ - - + + @@ -184,14 +184,14 @@ const handleRegionChange = (value) => { } } -// 学校类型对应学段映射 +// 学校类型对应学段映射(与 pg_grade.stage 一致:1小学 2初中 3高中 4中专 5大学) const getStagesBySchoolType = (schoolType) => { const map = { - '1': ['1'], // 小学 - '2': ['2'], // 初中 - '3': ['3'], // 高中 - '4': ['1', '2'], // 九年一贯制 - '5': ['2', '3'] // 完全中学 + '1': ['1'], // 小学 + '2': ['2'], // 初中 + '3': ['3'], // 高中 + '4': ['4'], // 中专 + '5': ['5'] // 大学 } return map[schoolType] || [] } diff --git a/frontend/src/views/monitor/server/index.vue b/frontend/src/views/monitor/server/index.vue index 60ff80c..2b4a1d9 100644 --- a/frontend/src/views/monitor/server/index.vue +++ b/frontend/src/views/monitor/server/index.vue @@ -180,6 +180,9 @@ function getList() { getServer().then(response => { server.value = response.data proxy.$modal.closeLoading() + }).catch(() => { + proxy.$modal.closeLoading() + proxy.$modal.msgError("加载服务监控数据失败") }) } diff --git a/scripts/clear-h5-captcha-cache.sh b/scripts/clear-h5-captcha-cache.sh new file mode 100644 index 0000000..2bf029f --- /dev/null +++ b/scripts/clear-h5-captcha-cache.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# 清理 H5 图形验证码、短信验证码、限流与黑名单等 Redis 缓存 +# 使用场景:验证码异常、发送过于频繁、需要重置限流时,在生产服务器执行 +# +# 用法(在生产服务器 192.168.71.56 上执行): +# cd /opt/pangu-user-platform && bash scripts/clear-h5-captcha-cache.sh +# REDIS_PASSWORD=你的密码 bash scripts/clear-h5-captcha-cache.sh +# 若后端使用 application-test.yml,密码请以该文件为准。 + +set -e +REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +REDIS_PORT="${REDIS_PORT:-6379}" +REDIS_PASSWORD="${REDIS_PASSWORD:-}" + +if ! command -v redis-cli &>/dev/null; then + echo "未找到 redis-cli,请安装 Redis 客户端或在已安装的机器上执行" + exit 1 +fi + +CLI=(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT") +if [[ -n "$REDIS_PASSWORD" ]]; then + CLI+=(-a "$REDIS_PASSWORD") +fi + +echo "清理 H5 验证码与短信相关缓存 (host=$REDIS_HOST port=$REDIS_PORT) ..." + +for pattern in "global:captcha_codes:*" "h5:sms:*"; do + count=0 + while IFS= read -r key; do + [[ -z "$key" ]] && continue + "${CLI[@]}" DEL "$key" &>/dev/null && ((count++)) || true + done < <("${CLI[@]}" --no-auth-warning KEYS "$pattern" 2>/dev/null || true) + echo " 已删除 pattern=$pattern 数量=$count" +done + +echo "清理完成。" diff --git a/scripts/sql/V1.0.4__grade_add_stage.sql b/scripts/sql/V1.0.4__grade_add_stage.sql index 08d9724..daa681f 100644 --- a/scripts/sql/V1.0.4__grade_add_stage.sql +++ b/scripts/sql/V1.0.4__grade_add_stage.sql @@ -1,21 +1,41 @@ -- ===================================================== --- 年级表增加学段字段 - V1.0.4 +-- 年级表增加学段字段 - V1.0.4(幂等,可重复执行) -- 执行时间: 2026-02-04 -- 说明: 年级管理增加学段(小学/初中/高中/中专/大学) +-- PROD 对比: 若 pg_grade 已有 stage 列则仅做空值回填,否则加列并初始化 -- ===================================================== --- 1. 新增学段字段 -ALTER TABLE pg_grade ADD COLUMN stage CHAR(1) NULL COMMENT '学段(1小学 2初中 3高中 4中专 5大学)' AFTER grade_name; +DELIMITER // +DROP PROCEDURE IF EXISTS proc_grade_add_stage// +CREATE PROCEDURE proc_grade_add_stage() +BEGIN + DECLARE col_exists INT DEFAULT 0; --- 2. 初始化现有数据(根据年级名称自动匹配学段) --- 小学 -UPDATE pg_grade SET stage = '1' WHERE grade_name IN ('一年级', '二年级', '三年级', '四年级', '五年级', '六年级'); --- 初中 -UPDATE pg_grade SET stage = '2' WHERE grade_name IN ('七年级', '八年级', '九年级'); --- 高中 -UPDATE pg_grade SET stage = '3' WHERE grade_name IN ('高一', '高二', '高三'); --- 未匹配的默认设为小学 -UPDATE pg_grade SET stage = '1' WHERE stage IS NULL; + SELECT COUNT(*) INTO col_exists + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pg_grade' + AND COLUMN_NAME = 'stage'; --- 3. 设置为非空并添加默认值 -ALTER TABLE pg_grade MODIFY COLUMN stage CHAR(1) NOT NULL DEFAULT '1' COMMENT '学段(1小学 2初中 3高中 4中专 5大学)'; + IF col_exists = 0 THEN + -- 1. 新增学段字段 + ALTER TABLE pg_grade ADD COLUMN stage CHAR(1) NULL COMMENT '学段(1小学 2初中 3高中 4中专 5大学)' AFTER grade_name; + -- 2. 初始化现有数据 + UPDATE pg_grade SET stage = '1' WHERE grade_name IN ('一年级', '二年级', '三年级', '四年级', '五年级', '六年级'); + UPDATE pg_grade SET stage = '2' WHERE grade_name IN ('七年级', '八年级', '九年级'); + UPDATE pg_grade SET stage = '3' WHERE grade_name IN ('高一', '高二', '高三'); + UPDATE pg_grade SET stage = '1' WHERE stage IS NULL; + -- 3. 设置为非空并添加默认值 + ALTER TABLE pg_grade MODIFY COLUMN stage CHAR(1) NOT NULL DEFAULT '1' COMMENT '学段(1小学 2初中 3高中 4中专 5大学)'; + ELSE + -- 列已存在:仅回填可能为空的(兼容已加列但未回填的环境) + UPDATE pg_grade SET stage = '1' WHERE grade_name IN ('一年级', '二年级', '三年级', '四年级', '五年级', '六年级') AND (stage IS NULL OR stage = ''); + UPDATE pg_grade SET stage = '2' WHERE grade_name IN ('七年级', '八年级', '九年级') AND (stage IS NULL OR stage = ''); + UPDATE pg_grade SET stage = '3' WHERE grade_name IN ('高一', '高二', '高三') AND (stage IS NULL OR stage = ''); + UPDATE pg_grade SET stage = '1' WHERE (stage IS NULL OR stage = ''); + END IF; +END// +DELIMITER ; + +CALL proc_grade_add_stage(); +DROP PROCEDURE IF EXISTS proc_grade_add_stage; diff --git a/scripts/sql/V1.0.5__clean_region_data.sql b/scripts/sql/V1.0.5__clean_region_data.sql index 0afcd6e..7982d72 100644 --- a/scripts/sql/V1.0.5__clean_region_data.sql +++ b/scripts/sql/V1.0.5__clean_region_data.sql @@ -1,30 +1,26 @@ -- ================================================================ --- V1.0.5 清理非湖北省区域数据 --- 执行说明:先检查湖北省的region_id,确认为42后再执行删除 --- 执行时间:需在业务低峰期执行 +-- V1.0.5 清理非湖北省区域数据(幂等,可重复执行) +-- 执行说明:先确认湖北省 region_id=42;若当前仅含湖北省则删除 0 行 +-- PROD 对比: 当前 pg_region 仅湖北省及其下级(120 条),执行本脚本将删除 0 行 -- ================================================================ --- 0. 首先确认湖北省的region_id +-- 0. 确认湖北省的 region_id(预期 region_id = 42) SELECT region_id, region_name, parent_id, ancestors FROM pg_region WHERE region_name = '湖北省'; --- 预期结果:region_id = 42 --- 1. 查看要删除的数据量(预检查,不会执行删除) -SELECT COUNT(*) as '待删除的区域数量' FROM pg_region -WHERE region_id != 42 +-- 1. 预检查:将要删除的行数(非湖北省及其下级的区域;湖北省 region_id=42,其下级 ancestors 以 0,42 开头) +SELECT COUNT(*) AS '待删除的区域数量(非湖北)' FROM pg_region +WHERE region_id != 42 AND ancestors NOT LIKE '0,42%'; --- 2. 物理删除非湖北省的区域数据 --- 删除所有ancestors不以"0,42"开头的区域(即非湖北省及其下级区域) --- 同时排除湖北省本身(region_id=42) -DELETE FROM pg_region -WHERE region_id != 42 +-- 2. 物理删除:非湖北省及其下级(保留 region_id=42 及 ancestors 以 0,42 开头的行) +DELETE FROM pg_region +WHERE region_id != 42 AND ancestors NOT LIKE '0,42%'; --- 3. 删除其他省份的根节点(parent_id = 0 且 region_id != 42) -DELETE FROM pg_region -WHERE parent_id = 0 +-- 3. 删除其他省份根节点(parent_id=0 且 region_id 非 42) +DELETE FROM pg_region +WHERE parent_id = 0 AND region_id != 42; -- 4. 验证结果 -SELECT COUNT(*) as '剩余区域数量' FROM pg_region; -SELECT region_name, level, COUNT(*) as '数量' FROM pg_region GROUP BY level, region_name LIMIT 20; +SELECT COUNT(*) AS '剩余区域数量' FROM pg_region; diff --git a/scripts/sql/V1.0.6__menu_application_submenu.sql b/scripts/sql/V1.0.6__menu_application_submenu.sql new file mode 100644 index 0000000..59d9665 --- /dev/null +++ b/scripts/sql/V1.0.6__menu_application_submenu.sql @@ -0,0 +1,25 @@ +-- ===================================================== +-- 菜单调整:应用管理改为目录,下挂 应用列表 + 接口字典(幂等,可重复执行) +-- 适用:已存在旧版「应用管理」单页菜单的环境,无需重跑全量 pangu_menu.sql +-- ===================================================== + +-- 1. 应用管理改为目录(M),去掉 component +UPDATE sys_menu SET menu_type = 'M', component = NULL, perms = '' WHERE menu_id = 2300; + +-- 2. 新增「应用列表」二级菜单(若不存在) +INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) +VALUES (2307, '应用列表', 2300, 1, 'list', 'business/application/index', '', 1, 0, 'C', '0', '0', 'business:application:list', '#', 103, 1, NOW()); + +-- 3. 应用相关按钮归属到「应用列表」下 +UPDATE sys_menu SET parent_id = 2307 WHERE menu_id IN (2301, 2302, 2303, 2304, 2305, 2306); + +-- 4. 新增「接口字典」二级菜单(若不存在) +INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) +VALUES (2310, '接口字典', 2300, 2, 'apiDict', 'business/apiDict/index', '', 1, 0, 'C', '0', '0', 'business:apiDict:list', 'list', 103, 1, NOW()); + +-- 5. 接口字典按钮(若不存在) +INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time) VALUES +(2311, '接口字典查询', 2310, 1, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:query', '#', 103, 1, NOW()), +(2312, '接口字典新增', 2310, 2, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:add', '#', 103, 1, NOW()), +(2313, '接口字典修改', 2310, 3, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:edit', '#', 103, 1, NOW()), +(2314, '接口字典删除', 2310, 4, '', '', '', 1, 0, 'F', '0', '0', 'business:apiDict:remove', '#', 103, 1, NOW()); diff --git a/scripts/sql/V1.0.7__hide_system_menus.sql b/scripts/sql/V1.0.7__hide_system_menus.sql new file mode 100644 index 0000000..499982e --- /dev/null +++ b/scripts/sql/V1.0.7__hide_system_menus.sql @@ -0,0 +1,26 @@ +-- ===================================================== +-- 隐藏指定系统菜单(与菜单管理界面中圈出的项一致) +-- 显示状态:0=显示 1=隐藏 +-- 幂等,可重复执行 +-- +-- 在 192.168.71.56 服务器上执行(更新程序使用的库 pguser-db@8.148.25.55): +-- 1. 上传本文件到服务器:scp scripts/sql/V1.0.7__hide_system_menus.sql root@192.168.71.56:/opt/pangu-user-platform/scripts/ +-- 2. ssh root@192.168.71.56 +-- 3. mysql -h 8.148.25.55 -uroot -p pguser-db < /opt/pangu-user-platform/scripts/V1.0.7__hide_system_menus.sql +-- 执行后管理员重新登录或刷新菜单后即可生效。 +-- ===================================================== + +-- Admin监控、任务调度中心、系统工具、租户管理、PLUS官网、测试菜单及其子菜单 +UPDATE sys_menu SET visible = '1' WHERE menu_id IN ( + 117, -- Admin监控 + 120, -- 任务调度中心 + 3, -- 系统工具 + 6, -- 租户管理(目录) + 121, -- 租户管理(页面) + 4, -- PLUS官网 + 5, -- 测试菜单(目录) + 1500, -- 测试单表 + 1501, 1502, 1503, 1504, 1505, -- 测试单表按钮 + 1506, -- 测试树表 + 1507, 1508, 1509, 1510, 1511 -- 测试树表按钮 +); diff --git a/scripts/sql/V1.0.8__delete_system_menus.sql b/scripts/sql/V1.0.8__delete_system_menus.sql new file mode 100644 index 0000000..d0a13b1 --- /dev/null +++ b/scripts/sql/V1.0.8__delete_system_menus.sql @@ -0,0 +1,31 @@ +-- ===================================================== +-- 删除指定系统菜单(与 V1.0.7 隐藏的菜单一致,改为物理删除) +-- 删除范围:Admin监控、任务调度中心、系统工具(及代码生成等子菜单)、 +-- 租户管理(及子菜单)、PLUS官网、测试菜单(及测试单表/树表等子菜单) +-- 执行前请确认数据库与备份;执行顺序:先删角色-菜单关联,再删菜单。 +-- ===================================================== + +-- 1. 删除角色菜单关联 +DELETE FROM sys_role_menu WHERE menu_id IN ( + 117, -- Admin监控 + 120, -- 任务调度中心 + 3, -- 系统工具(目录) + 115, -- 代码生成 + 116, -- 修改生成配置 + 1055, 1056, 1057, 1058, 1059, 1060, -- 代码生成按钮 + 6, -- 租户管理(目录) + 121, -- 租户管理 + 122, -- 租户套餐管理 + 4, -- PLUS官网 + 5, -- 测试菜单(目录) + 1500, -- 测试单表 + 1501, 1502, 1503, 1504, 1505, -- 测试单表按钮 + 1506, -- 测试树表 + 1507, 1508, 1509, 1510, 1511 -- 测试树表按钮 +); + +-- 2. 删除菜单 +DELETE FROM sys_menu WHERE menu_id IN ( + 117, 120, 3, 115, 116, 1055, 1056, 1057, 1058, 1059, 1060, + 6, 121, 122, 4, 5, 1500, 1501, 1502, 1503, 1504, 1505, 1506, 1507, 1508, 1509, 1510, 1511 +); diff --git a/scripts/sql/V1.0.9__delete_tenant_button_menus.sql b/scripts/sql/V1.0.9__delete_tenant_button_menus.sql new file mode 100644 index 0000000..7f7c4f9 --- /dev/null +++ b/scripts/sql/V1.0.9__delete_tenant_button_menus.sql @@ -0,0 +1,15 @@ +-- ===================================================== +-- 删除租户/租户套餐相关按钮菜单(父菜单 121、122 已在 V1.0.8 中删除,此处清理遗留按钮项) +-- 租户查询/新增/修改/删除/导出(1606-1610)、租户套餐查询/新增/修改/删除/导出(1611-1615) +-- ===================================================== + +-- 1. 删除角色菜单关联 +DELETE FROM sys_role_menu WHERE menu_id IN ( + 1606, 1607, 1608, 1609, 1610, -- 租户-按钮 + 1611, 1612, 1613, 1614, 1615 -- 租户套餐-按钮 +); + +-- 2. 删除菜单 +DELETE FROM sys_menu WHERE menu_id IN ( + 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615 +);