Compare commits
3 Commits
a9bbb30f99
...
f3b302c9fd
| Author | SHA1 | Date |
|---|---|---|
|
|
f3b302c9fd | |
|
|
0b528e03d8 | |
|
|
d6e922cc2f |
69
README.md
69
README.md
|
|
@ -10,46 +10,38 @@
|
||||||
|
|
||||||
```
|
```
|
||||||
pangu-user-platform/ # 项目根目录
|
pangu-user-platform/ # 项目根目录
|
||||||
├── pom.xml # ⭐ Maven父项目配置 (必须在根目录)
|
|
||||||
├── README.md # 项目说明文档
|
├── README.md # 项目说明文档
|
||||||
├── backend/ # 后端代码
|
├── backend/ # 后端代码 (RuoYi-Vue-Plus 5.5.x)
|
||||||
│ ├── README.md # 后端构建说明 (重要!)
|
│ ├── README.md # 后端构建说明 (重要!)
|
||||||
│ ├── pangu-admin/ # 启动模块 (Spring Boot)
|
│ ├── pom.xml # 后端父 POM
|
||||||
│ │ └── pom.xml # 子模块配置
|
│ ├── pangu-admin/ # 启动与认证入口 (Spring Boot)
|
||||||
│ ├── pangu-common/ # 公共模块 (工具类、常量)
|
│ ├── pangu-common/ # 公共模块 (core/redis/security/satoken 等)
|
||||||
│ │ └── pom.xml
|
│ ├── pangu-modules/ # 业务与系统模块
|
||||||
│ ├── pangu-framework/ # 框架模块 (配置、拦截器)
|
│ │ ├── pangu-system/ # 系统 (用户/角色/菜单/部门/字典/监控)
|
||||||
│ │ └── pom.xml
|
│ │ ├── pangu-business/ # 业务 (学校/会员/学生/应用/基础数据/H5/开放API)
|
||||||
│ ├── pangu-system/ # 业务模块 (学校、会员、学生、应用)
|
│ │ ├── pangu-generator/ # 代码生成
|
||||||
│ │ └── pom.xml
|
│ │ └── pangu-job/ # 定时任务
|
||||||
│ ├── sql/ # 数据库脚本
|
│ ├── pangu-extend/ # 扩展 (Monitor Admin、SnailJob Server 等)
|
||||||
│ └── scripts/ # 运维脚本
|
│ ├── script/ # 数据库脚本等
|
||||||
├── frontend/ # 前端代码
|
│ └── 各模块 pom.xml
|
||||||
│ └── frontend/ # Vue3 + Element Plus + Vite
|
├── frontend/ # 管理后台前端 (Vue3 + Element Plus + Vite)
|
||||||
│ ├── package.json # 前端依赖配置
|
│ ├── package.json
|
||||||
│ └── README.md # 前端使用说明
|
│ └── src/
|
||||||
└── docs/ # 项目文档
|
└── docs/ # 项目文档
|
||||||
├── 01-需求文档/
|
├── 01-需求文档/
|
||||||
├── 02-系统设计/
|
├── 02-系统设计/
|
||||||
├── 03-数据库设计/
|
├── 03-数据库设计/
|
||||||
├── 04-接口文档/
|
├── 04-接口文档/
|
||||||
├── 05-前端UI规范/
|
├── 05-前端UI规范/、05-技术方案/、05-模块技术方案/
|
||||||
├── 05-技术方案/
|
|
||||||
├── 05-模块技术方案/
|
|
||||||
├── 06-测试文档/
|
├── 06-测试文档/
|
||||||
└── 07-运维文档/
|
└── 07-运维文档/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📌 重要说明
|
### 📌 重要说明
|
||||||
|
|
||||||
**根目录的 `pom.xml` 为什么不在 backend 目录?**
|
- **后端构建**:在 `backend` 目录执行 `mvn clean package -DskipTests -pl pangu-admin -am`,生成 `pangu-admin/target/pangu-admin.jar`。
|
||||||
|
- **前端构建**:在 `frontend` 目录执行 `npm run build:prod`,产出 `dist/`。
|
||||||
这是 Maven 多模块项目的标准结构:
|
- 详细说明请查看: [backend/README.md](backend/README.md)
|
||||||
- 根 `pom.xml` 是**父项目配置**,管理所有子模块
|
|
||||||
- Maven 构建**必须从根目录**执行: `mvn clean package`
|
|
||||||
- 不要移动到 backend 目录,否则构建会失败
|
|
||||||
|
|
||||||
详细说明请查看: [backend/README.md](backend/README.md)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -81,7 +73,7 @@ mvn spring-boot:run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入前端目录
|
# 进入前端目录
|
||||||
cd frontend/frontend
|
cd frontend
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
|
|
@ -90,7 +82,7 @@ npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**访问地址**: `http://localhost:80`
|
**访问地址**: 以 vite 配置为准(如 `http://localhost:80`)
|
||||||
|
|
||||||
**默认账号**: admin / admin123
|
**默认账号**: admin / admin123
|
||||||
|
|
||||||
|
|
@ -102,13 +94,12 @@ npm run dev
|
||||||
|
|
||||||
| 技术 | 说明 | 版本 |
|
| 技术 | 说明 | 版本 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| Spring Boot | 基础框架 | 2.7.18 |
|
| Spring Boot | 基础框架 | 3.5.x |
|
||||||
| Spring Security | 安全框架 | 5.7.x |
|
| Sa-Token | 认证与权限 | 1.44.x |
|
||||||
| MyBatis-Plus | ORM框架 | 3.5.x |
|
| MyBatis-Plus | ORM框架 | 3.5.x |
|
||||||
| MySQL | 数据库 | 8.0.x |
|
| MySQL | 数据库 | 8.0+ |
|
||||||
| Redis | 缓存 | 6.0.x |
|
| Redis / Redisson | 缓存 | 6.0+ |
|
||||||
| JWT | 认证 | 0.9.1 |
|
| RuoYi-Vue-Plus | 基础框架 | 5.5.x |
|
||||||
| Druid | 连接池 | 1.2.x |
|
|
||||||
|
|
||||||
### 前端技术
|
### 前端技术
|
||||||
|
|
||||||
|
|
@ -140,7 +131,7 @@ npm run dev
|
||||||
- 用户认证 (手机号+密码/验证码/微信)
|
- 用户认证 (手机号+密码/验证码/微信)
|
||||||
- 权限管理 (超级管理员/分公司用户/学校用户)
|
- 权限管理 (超级管理员/分公司用户/学校用户)
|
||||||
- 数据权限 (按区域/学校隔离)
|
- 数据权限 (按区域/学校隔离)
|
||||||
- 系统监控 (在线用户、日志、缓存)
|
- 系统监控 (在线用户、登录/操作日志、**服务监控**(CPU/内存/JVM/磁盘)、缓存监控)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -237,8 +228,8 @@ java -jar target/pangu-admin.jar
|
||||||
### 前端部署
|
### 前端部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend/frontend
|
cd frontend
|
||||||
npm run build
|
npm run build:prod
|
||||||
# 将 dist/ 目录部署到 Nginx
|
# 将 dist/ 目录部署到 Nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -274,4 +265,4 @@ npm run build
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*最后更新: 2026-02-02*
|
*最后更新: 2026-02-05*
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
|
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<fastjson.version>1.2.83</fastjson.version>
|
<fastjson.version>1.2.83</fastjson.version>
|
||||||
|
<oshi.version>6.4.0</oshi.version>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
<anyline.version>8.7.3-20251210</anyline.version>
|
<anyline.version>8.7.3-20251210</anyline.version>
|
||||||
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
||||||
|
|
@ -237,6 +238,11 @@
|
||||||
<artifactId>ip2region</artifactId>
|
<artifactId>ip2region</artifactId>
|
||||||
<version>${ip2region.version}</version>
|
<version>${ip2region.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.oshi</groupId>
|
||||||
|
<artifactId>oshi-core</artifactId>
|
||||||
|
<version>${oshi.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>fastjson</artifactId>
|
<artifactId>fastjson</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2019 RuoYi-Vue-Plus
|
Copyright (c) 2019 PanGu
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# 生产环境部署指南 - 学校班级ID迁移
|
||||||
|
|
||||||
|
## 背景说明
|
||||||
|
|
||||||
|
由于雪花算法生成的 ID(19位数字)超出了 JavaScript 安全整数范围(`9007199254740991`),导致前端在处理 `pg_school_class.id` 时会丢失精度。
|
||||||
|
|
||||||
|
本次更新将 `pg_school_class` 表的 ID 生成策略从雪花算法改为数据库自增。
|
||||||
|
|
||||||
|
## 涉及的代码变更
|
||||||
|
|
||||||
|
| 文件 | 变更内容 |
|
||||||
|
|------|----------|
|
||||||
|
| `PgSchoolClass.java` | `@TableId(type = IdType.ASSIGN_ID)` → `@TableId(type = IdType.AUTO)` |
|
||||||
|
| `SchoolClassIdMigration.java` | 新增,应用启动时自动迁移(仅开发环境使用) |
|
||||||
|
|
||||||
|
## 部署步骤
|
||||||
|
|
||||||
|
### 1. 准备工作
|
||||||
|
|
||||||
|
- [ ] 通知相关人员,安排维护窗口
|
||||||
|
- [ ] 备份生产数据库(完整备份)
|
||||||
|
- [ ] 准备好回滚方案
|
||||||
|
|
||||||
|
### 2. 执行数据库迁移
|
||||||
|
|
||||||
|
**重要:在部署新代码之前执行!**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 连接到生产数据库
|
||||||
|
mysql -h <host> -u <user> -p <database>
|
||||||
|
|
||||||
|
# 执行迁移脚本
|
||||||
|
source production_migrate_school_class_id.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本执行过程中会显示:
|
||||||
|
- 需要迁移的记录数
|
||||||
|
- ID 映射详情(旧ID → 新ID)
|
||||||
|
- 数据完整性验证结果
|
||||||
|
|
||||||
|
### 3. 验证迁移结果
|
||||||
|
|
||||||
|
确认脚本输出中所有检查项都显示 `✓ 通过`:
|
||||||
|
- 数据完整性检查
|
||||||
|
- 学生关联检查
|
||||||
|
- ID范围检查
|
||||||
|
|
||||||
|
### 4. 部署新代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止应用服务
|
||||||
|
systemctl stop pangu-business
|
||||||
|
|
||||||
|
# 部署新版本 JAR
|
||||||
|
cp pangu-business-new.jar /opt/pangu/pangu-business.jar
|
||||||
|
|
||||||
|
# 启动应用服务
|
||||||
|
systemctl start pangu-business
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:新代码中包含 `SchoolClassIdMigration.java`,但由于数据库已经迁移完成,该组件会检测到无需迁移并跳过。
|
||||||
|
|
||||||
|
### 5. 功能验证
|
||||||
|
|
||||||
|
- [ ] 登录系统,访问学生管理页面
|
||||||
|
- [ ] 查看学生列表,确认班级名称正常显示
|
||||||
|
- [ ] 编辑学生,选择班级后保存
|
||||||
|
- [ ] 再次编辑该学生,确认班级信息正确回显
|
||||||
|
- [ ] 新增班级,确认功能正常
|
||||||
|
|
||||||
|
### 6. 清理临时表
|
||||||
|
|
||||||
|
确认功能正常后,执行清理:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DROP TABLE IF EXISTS tmp_school_class_id_mapping;
|
||||||
|
DROP TABLE IF EXISTS pg_school_class_backup_prod;
|
||||||
|
DROP TABLE IF EXISTS pg_student_class_backup_prod;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚方案
|
||||||
|
|
||||||
|
如果迁移出现问题,执行回滚脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -h <host> -u <user> -p <database>
|
||||||
|
source production_rollback_school_class_id.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
回滚后需要部署旧版本代码。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 迁移脚本执行失败怎么办?
|
||||||
|
|
||||||
|
A: 脚本在执行过程中创建了备份表,可以通过回滚脚本恢复。如果回滚脚本也失败,使用完整数据库备份恢复。
|
||||||
|
|
||||||
|
### Q: 新代码部署后,自动迁移组件会重复执行吗?
|
||||||
|
|
||||||
|
A: 不会。`SchoolClassIdMigration.java` 会检测是否存在超出安全范围的 ID,如果没有则跳过迁移。
|
||||||
|
|
||||||
|
### Q: 迁移后新增的班级 ID 是什么样的?
|
||||||
|
|
||||||
|
A: 新增班级的 ID 将是小的自增数字(如 113, 114, 115...),不再是 19 位的雪花 ID。
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `production_migrate_school_class_id.sql` | 生产环境迁移脚本 |
|
||||||
|
| `production_rollback_school_class_id.sql` | 回滚脚本 |
|
||||||
|
| `DEPLOYMENT_GUIDE.md` | 本文档 |
|
||||||
|
|
@ -198,9 +198,10 @@ cd /Users/felix/pgWorkSpace/pangu-user-platform/backend
|
||||||
|
|
||||||
## 🔗 相关文档
|
## 🔗 相关文档
|
||||||
|
|
||||||
- [应用管理技术方案](/docs/应用管理-需求与技术设计方案.md)
|
- [技术方案](/docs/2-技术方案.md)
|
||||||
- [开放接口实现说明](/docs/开放接口实现说明.md)
|
- [接口文档](/docs/3-接口文档.md)
|
||||||
- [PHP 客户端使用指南](/scripts/test/README.md)
|
- [运维文档](/docs/4-运维文档.md)
|
||||||
|
- [PHP 客户端使用指南](/test/README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- 修复学生表的 dept_id 字段
|
||||||
|
--
|
||||||
|
-- 问题:学生创建时未根据所属学校设置 dept_id,导致数据权限过滤失效
|
||||||
|
-- 解决:根据学生所属学校的 dept_id 更新学生的 dept_id
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 查看需要修复的数据
|
||||||
|
SELECT
|
||||||
|
s.student_id,
|
||||||
|
s.student_name,
|
||||||
|
s.school_id,
|
||||||
|
s.dept_id AS student_dept_id,
|
||||||
|
sc.school_name,
|
||||||
|
sc.dept_id AS school_dept_id
|
||||||
|
FROM pg_student s
|
||||||
|
LEFT JOIN pg_school sc ON s.school_id = sc.school_id
|
||||||
|
WHERE s.del_flag = '0'
|
||||||
|
AND (s.dept_id IS NULL OR s.dept_id != sc.dept_id);
|
||||||
|
|
||||||
|
-- 执行修复:将学生的 dept_id 更新为所属学校的 dept_id
|
||||||
|
UPDATE pg_student s
|
||||||
|
INNER JOIN pg_school sc ON s.school_id = sc.school_id
|
||||||
|
SET s.dept_id = sc.dept_id
|
||||||
|
WHERE s.del_flag = '0'
|
||||||
|
AND sc.dept_id IS NOT NULL
|
||||||
|
AND (s.dept_id IS NULL OR s.dept_id != sc.dept_id);
|
||||||
|
|
||||||
|
-- 验证修复结果
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS '修复后仍有问题的记录数'
|
||||||
|
FROM pg_student s
|
||||||
|
LEFT JOIN pg_school sc ON s.school_id = sc.school_id
|
||||||
|
WHERE s.del_flag = '0'
|
||||||
|
AND sc.dept_id IS NOT NULL
|
||||||
|
AND (s.dept_id IS NULL OR s.dept_id != sc.dept_id);
|
||||||
|
|
@ -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 (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, '');
|
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)
|
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());
|
VALUES (2300, '应用管理', 0, 4, 'application', NULL, '', 1, 0, 'M', '0', '0', '', '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 (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)
|
||||||
INSERT INTO sys_menu VALUES (2303, '应用修改', 2300, 3, '', '', '', 1, 0, 'F', '0', '0', 'business:application:edit', '#', 103, 1, NOW(), NULL, NULL, '');
|
VALUES (2307, '应用列表', 2300, 1, 'list', 'business/application/index', '', 1, 0, 'C', '0', '0', 'business:application:list', '#', 103, 1, NOW());
|
||||||
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 (2301, '应用查询', 2307, 1, '', '', '', 1, 0, 'F', '0', '0', 'business:application:query', '#', 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 (2302, '应用新增', 2307, 2, '', '', '', 1, 0, 'F', '0', '0', 'business:application:add', '#', 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, '');
|
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) =====================
|
-- ===================== 基础数据(一级目录,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)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 盘古用户平台 - 业务模块建表脚本(适配 RuoYi-Vue-Plus 5.x)
|
-- 盘古用户平台 - 业务模块建表脚本
|
||||||
-- 作者:pangu
|
-- 作者:pangu
|
||||||
-- 创建时间:2026-02-02
|
-- 创建时间:2026-02-02
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
@ -151,7 +151,7 @@ CREATE TABLE `pg_school` (
|
||||||
`school_id` bigint NOT NULL COMMENT '学校ID',
|
`school_id` bigint NOT NULL COMMENT '学校ID',
|
||||||
`school_code` varchar(32) NOT NULL COMMENT '学校编码',
|
`school_code` varchar(32) NOT NULL COMMENT '学校编码',
|
||||||
`school_name` varchar(100) 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_id` bigint DEFAULT NULL COMMENT '所属区域ID',
|
||||||
`region_path` varchar(500) DEFAULT NULL COMMENT '区域路径',
|
`region_path` varchar(500) DEFAULT NULL COMMENT '区域路径',
|
||||||
`address` varchar(500) DEFAULT NULL COMMENT '详细地址',
|
`address` varchar(500) DEFAULT NULL COMMENT '详细地址',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- 生产环境数据迁移脚本
|
||||||
|
--
|
||||||
|
-- 功能:将 pg_school_class 表的 ID 从雪花算法改为数据库自增
|
||||||
|
--
|
||||||
|
-- 问题背景:
|
||||||
|
-- 雪花算法生成的 ID 是 19 位数字(如 2018604156050272257),
|
||||||
|
-- 超出了 JavaScript 安全整数范围(9007199254740991),
|
||||||
|
-- 导致前端在处理这些 ID 时会丢失精度。
|
||||||
|
--
|
||||||
|
-- 执行时机:部署新代码之前执行
|
||||||
|
-- 执行方式:在 MySQL 客户端中执行此脚本
|
||||||
|
--
|
||||||
|
-- 注意事项:
|
||||||
|
-- 1. 执行前请先备份数据库
|
||||||
|
-- 2. 建议在业务低峰期执行
|
||||||
|
-- 3. 执行后请验证数据完整性
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 设置变量
|
||||||
|
SET @JS_MAX_SAFE_INTEGER = 9007199254740991;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第一步:检查是否需要迁移
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第一步:检查迁移需求 ===' AS step;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS '需要迁移的记录数',
|
||||||
|
CASE WHEN COUNT(*) > 0 THEN '需要执行迁移' ELSE '无需迁移' END AS '状态'
|
||||||
|
FROM pg_school_class
|
||||||
|
WHERE id > @JS_MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第二步:备份原始数据
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第二步:备份原始数据 ===' AS step;
|
||||||
|
|
||||||
|
-- 备份 pg_school_class 表
|
||||||
|
DROP TABLE IF EXISTS pg_school_class_backup_prod;
|
||||||
|
CREATE TABLE pg_school_class_backup_prod AS SELECT * FROM pg_school_class;
|
||||||
|
SELECT CONCAT('已备份 pg_school_class 表,共 ', COUNT(*), ' 条记录') AS result FROM pg_school_class_backup_prod;
|
||||||
|
|
||||||
|
-- 备份 pg_student 表中的 school_class_id
|
||||||
|
DROP TABLE IF EXISTS pg_student_class_backup_prod;
|
||||||
|
CREATE TABLE pg_student_class_backup_prod AS
|
||||||
|
SELECT student_id, school_class_id FROM pg_student WHERE school_class_id IS NOT NULL;
|
||||||
|
SELECT CONCAT('已备份 pg_student.school_class_id,共 ', COUNT(*), ' 条记录') AS result FROM pg_student_class_backup_prod;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第三步:创建 ID 映射表
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第三步:创建 ID 映射表 ===' AS step;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS tmp_school_class_id_mapping;
|
||||||
|
CREATE TABLE tmp_school_class_id_mapping (
|
||||||
|
old_id BIGINT PRIMARY KEY COMMENT '原始大数字ID',
|
||||||
|
new_id BIGINT COMMENT '新的小数字ID'
|
||||||
|
) ENGINE=InnoDB COMMENT='学校班级ID迁移映射表';
|
||||||
|
|
||||||
|
-- 获取当前最大的安全范围内的 ID
|
||||||
|
SET @max_safe_id = (SELECT IFNULL(MAX(id), 0) FROM pg_school_class WHERE id <= @JS_MAX_SAFE_INTEGER);
|
||||||
|
SET @new_id_start = @max_safe_id + 1;
|
||||||
|
|
||||||
|
SELECT CONCAT('当前安全范围内最大ID: ', @max_safe_id, ', 新ID将从 ', @new_id_start, ' 开始') AS info;
|
||||||
|
|
||||||
|
-- 插入需要迁移的 ID 映射
|
||||||
|
INSERT INTO tmp_school_class_id_mapping (old_id, new_id)
|
||||||
|
SELECT
|
||||||
|
id AS old_id,
|
||||||
|
@max_safe_id := @max_safe_id + 1 AS new_id
|
||||||
|
FROM pg_school_class
|
||||||
|
WHERE id > @JS_MAX_SAFE_INTEGER
|
||||||
|
ORDER BY COALESCE(create_time, '1970-01-01'), id;
|
||||||
|
|
||||||
|
SELECT CONCAT('已创建 ', COUNT(*), ' 条 ID 映射') AS result FROM tmp_school_class_id_mapping;
|
||||||
|
|
||||||
|
-- 显示映射详情
|
||||||
|
SELECT '映射详情(旧ID -> 新ID):' AS info;
|
||||||
|
SELECT old_id, new_id FROM tmp_school_class_id_mapping ORDER BY new_id;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第四步:执行数据迁移
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第四步:执行数据迁移 ===' AS step;
|
||||||
|
|
||||||
|
-- 4.1 更新 pg_school_class 表的 ID
|
||||||
|
-- 注意:需要临时禁用外键检查(如果有的话)
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
UPDATE pg_school_class sc
|
||||||
|
INNER JOIN tmp_school_class_id_mapping m ON sc.id = m.old_id
|
||||||
|
SET sc.id = m.new_id;
|
||||||
|
|
||||||
|
SELECT CONCAT('已更新 pg_school_class 表 ', ROW_COUNT(), ' 条记录') AS result;
|
||||||
|
|
||||||
|
-- 4.2 更新 pg_student 表的 school_class_id 引用
|
||||||
|
UPDATE pg_student s
|
||||||
|
INNER JOIN tmp_school_class_id_mapping m ON s.school_class_id = m.old_id
|
||||||
|
SET s.school_class_id = m.new_id;
|
||||||
|
|
||||||
|
SELECT CONCAT('已更新 pg_student 表 ', ROW_COUNT(), ' 条记录') AS result;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第五步:修改表结构为自增
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第五步:修改表结构为自增 ===' AS step;
|
||||||
|
|
||||||
|
-- 获取迁移后的最大 ID
|
||||||
|
SET @new_max_id = (SELECT IFNULL(MAX(id), 0) + 1 FROM pg_school_class);
|
||||||
|
|
||||||
|
-- 修改 id 字段为自增
|
||||||
|
ALTER TABLE pg_school_class MODIFY COLUMN id BIGINT NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
-- 设置自增起始值
|
||||||
|
SET @alter_sql = CONCAT('ALTER TABLE pg_school_class AUTO_INCREMENT = ', @new_max_id);
|
||||||
|
PREPARE stmt FROM @alter_sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SELECT CONCAT('已将 pg_school_class.id 修改为自增,起始值: ', @new_max_id) AS result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第六步:验证迁移结果
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第六步:验证迁移结果 ===' AS step;
|
||||||
|
|
||||||
|
-- 验证数据完整性
|
||||||
|
SELECT
|
||||||
|
'数据完整性检查' AS check_type,
|
||||||
|
(SELECT COUNT(*) FROM pg_school_class_backup_prod) AS '原班级记录数',
|
||||||
|
(SELECT COUNT(*) FROM pg_school_class) AS '现班级记录数',
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(*) FROM pg_school_class_backup_prod) = (SELECT COUNT(*) FROM pg_school_class)
|
||||||
|
THEN '✓ 通过'
|
||||||
|
ELSE '✗ 失败'
|
||||||
|
END AS '状态';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'学生关联检查' AS check_type,
|
||||||
|
(SELECT COUNT(*) FROM pg_student_class_backup_prod) AS '原关联记录数',
|
||||||
|
(SELECT COUNT(*) FROM pg_student WHERE school_class_id IS NOT NULL) AS '现关联记录数',
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(*) FROM pg_student_class_backup_prod) = (SELECT COUNT(*) FROM pg_student WHERE school_class_id IS NOT NULL)
|
||||||
|
THEN '✓ 通过'
|
||||||
|
ELSE '✗ 失败'
|
||||||
|
END AS '状态';
|
||||||
|
|
||||||
|
-- 验证是否还有大数字 ID
|
||||||
|
SELECT
|
||||||
|
'ID范围检查' AS check_type,
|
||||||
|
(SELECT COUNT(*) FROM pg_school_class WHERE id > @JS_MAX_SAFE_INTEGER) AS '超出安全范围的ID数',
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(*) FROM pg_school_class WHERE id > @JS_MAX_SAFE_INTEGER) = 0
|
||||||
|
THEN '✓ 通过'
|
||||||
|
ELSE '✗ 失败'
|
||||||
|
END AS '状态';
|
||||||
|
|
||||||
|
-- 显示新的 ID 范围
|
||||||
|
SELECT
|
||||||
|
'新ID范围' AS info,
|
||||||
|
MIN(id) AS '最小ID',
|
||||||
|
MAX(id) AS '最大ID'
|
||||||
|
FROM pg_school_class;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第七步:清理临时表(确认无误后执行)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第七步:清理说明 ===' AS step;
|
||||||
|
SELECT '请在确认数据无误后,手动执行以下命令清理临时表:' AS info;
|
||||||
|
SELECT 'DROP TABLE IF EXISTS tmp_school_class_id_mapping;' AS cleanup_sql_1;
|
||||||
|
SELECT 'DROP TABLE IF EXISTS pg_school_class_backup_prod;' AS cleanup_sql_2;
|
||||||
|
SELECT 'DROP TABLE IF EXISTS pg_student_class_backup_prod;' AS cleanup_sql_3;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 完成
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 迁移完成 ===' AS step;
|
||||||
|
SELECT '请验证以下内容:' AS info;
|
||||||
|
SELECT '1. 登录系统,检查学生管理功能是否正常' AS check_1;
|
||||||
|
SELECT '2. 编辑学生,选择班级后保存,确认数据正确' AS check_2;
|
||||||
|
SELECT '3. 新增班级,确认 ID 是小的自增数字' AS check_3;
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- 生产环境回滚脚本
|
||||||
|
--
|
||||||
|
-- 功能:回滚 pg_school_class ID 迁移
|
||||||
|
--
|
||||||
|
-- 执行时机:仅在迁移出现问题时执行
|
||||||
|
-- 前提条件:备份表(pg_school_class_backup_prod, pg_student_class_backup_prod)必须存在
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SELECT '=== 开始回滚 ===' AS step;
|
||||||
|
|
||||||
|
-- 检查备份表是否存在
|
||||||
|
SELECT
|
||||||
|
'备份表检查' AS check_type,
|
||||||
|
(SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'pg_school_class_backup_prod') AS 'pg_school_class_backup_prod',
|
||||||
|
(SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'pg_student_class_backup_prod') AS 'pg_student_class_backup_prod';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第一步:恢复 pg_school_class 表
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第一步:恢复 pg_school_class ===' AS step;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- 清空当前表
|
||||||
|
TRUNCATE TABLE pg_school_class;
|
||||||
|
|
||||||
|
-- 从备份恢复
|
||||||
|
INSERT INTO pg_school_class SELECT * FROM pg_school_class_backup_prod;
|
||||||
|
|
||||||
|
SELECT CONCAT('已恢复 pg_school_class 表,共 ', ROW_COUNT(), ' 条记录') AS result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第二步:恢复 pg_student 表的 school_class_id
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第二步:恢复 pg_student.school_class_id ===' AS step;
|
||||||
|
|
||||||
|
UPDATE pg_student s
|
||||||
|
INNER JOIN pg_student_class_backup_prod b ON s.student_id = b.student_id
|
||||||
|
SET s.school_class_id = b.school_class_id;
|
||||||
|
|
||||||
|
SELECT CONCAT('已恢复 pg_student.school_class_id,共 ', ROW_COUNT(), ' 条记录') AS result;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第三步:恢复表结构(移除自增)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 第三步:恢复表结构 ===' AS step;
|
||||||
|
|
||||||
|
-- 移除自增属性
|
||||||
|
ALTER TABLE pg_school_class MODIFY COLUMN id BIGINT NOT NULL;
|
||||||
|
|
||||||
|
SELECT '已移除 pg_school_class.id 的自增属性' AS result;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 验证回滚结果
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '=== 验证回滚结果 ===' AS step;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'数据完整性检查' AS check_type,
|
||||||
|
(SELECT COUNT(*) FROM pg_school_class_backup_prod) AS '备份记录数',
|
||||||
|
(SELECT COUNT(*) FROM pg_school_class) AS '当前记录数',
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT COUNT(*) FROM pg_school_class_backup_prod) = (SELECT COUNT(*) FROM pg_school_class)
|
||||||
|
THEN '✓ 通过'
|
||||||
|
ELSE '✗ 失败'
|
||||||
|
END AS '状态';
|
||||||
|
|
||||||
|
SELECT '=== 回滚完成 ===' AS step;
|
||||||
|
SELECT '注意:回滚后需要部署旧版本代码(使用雪花算法ID的版本)' AS warning;
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.dromara.pangu.common;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.pangu.school.mapper.PgSchoolMapper;
|
||||||
|
import org.dromara.system.mapper.SysDeptMapper;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域数据权限工具类
|
||||||
|
* <p>
|
||||||
|
* 用于获取当前登录用户可访问的区域ID列表
|
||||||
|
* 业务规则:
|
||||||
|
* - 超级管理员:不限制,返回 null
|
||||||
|
* - 分公司/学校用户:返回所管理学校所在区域的ID列表
|
||||||
|
*
|
||||||
|
* @author pangu
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegionPermissionHelper {
|
||||||
|
|
||||||
|
private final PgSchoolMapper schoolMapper;
|
||||||
|
private final SysDeptMapper deptMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户可访问的区域ID列表
|
||||||
|
*
|
||||||
|
* @return 区域ID列表,null 表示不限制(超管),空列表表示无权限
|
||||||
|
*/
|
||||||
|
public List<Long> getAccessibleRegionIds() {
|
||||||
|
// 超级管理员不限制
|
||||||
|
if (LoginHelper.isSuperAdmin()) {
|
||||||
|
log.debug("超级管理员,不限制区域");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deptId = LoginHelper.getDeptId();
|
||||||
|
if (deptId == null) {
|
||||||
|
log.warn("用户部门ID为空,无法获取区域权限");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户部门及下级部门ID列表
|
||||||
|
List<Long> deptIds = deptMapper.selectDeptAndChildById(deptId);
|
||||||
|
if (deptIds.isEmpty()) {
|
||||||
|
log.warn("未找到部门及下级部门: deptId={}", deptId);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询这些部门关联的学校的区域ID
|
||||||
|
List<Long> regionIds = schoolMapper.selectRegionIdsByDeptIds(deptIds);
|
||||||
|
|
||||||
|
log.debug("用户区域权限: userId={}, deptId={}, deptIds={}, regionIds={}",
|
||||||
|
LoginHelper.getUserId(), deptId, deptIds, regionIds);
|
||||||
|
|
||||||
|
return regionIds.isEmpty() ? Collections.emptyList() : regionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否需要区域过滤
|
||||||
|
*
|
||||||
|
* @return true 需要过滤,false 不需要(超管)
|
||||||
|
*/
|
||||||
|
public boolean needRegionFilter() {
|
||||||
|
return !LoginHelper.isSuperAdmin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -216,27 +216,33 @@ public class H5AuthServiceImpl implements H5AuthService {
|
||||||
|
|
||||||
// 发送短信
|
// 发送短信
|
||||||
if (smsProperties.isEnabled()) {
|
if (smsProperties.isEnabled()) {
|
||||||
try {
|
SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName());
|
||||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
if (smsBlend == null) {
|
||||||
map.put("code", code);
|
// 配置了启用但未配置短信实现(如生产未配 sms4j)时降级为测试模式,避免 NPE,验证码已写入 Redis 可正常登录
|
||||||
SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName());
|
log.warn("短信服务未配置(smsBlend=null),降级为测试模式,验证码仅写入Redis并打印日志");
|
||||||
String templateId = "login".equals(type) ? smsProperties.getLoginTemplateId() : smsProperties.getRegisterTemplateId();
|
log.info("【测试模式】短信验证码: phone={}, code={}, type={}", phone, code, type);
|
||||||
SmsResponse smsResponse;
|
} else {
|
||||||
if (StringUtils.isNotBlank(templateId)) {
|
try {
|
||||||
smsResponse = smsBlend.sendMessage(phone, templateId, map);
|
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||||
} else {
|
map.put("code", code);
|
||||||
smsResponse = smsBlend.sendMessage(phone, map);
|
String templateId = "login".equals(type) ? smsProperties.getLoginTemplateId() : smsProperties.getRegisterTemplateId();
|
||||||
}
|
SmsResponse smsResponse;
|
||||||
if (!smsResponse.isSuccess()) {
|
if (StringUtils.isNotBlank(templateId)) {
|
||||||
log.error("短信发送失败: phone={}, response={}", phone, smsResponse);
|
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("短信发送失败,请稍后重试");
|
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 {
|
} else {
|
||||||
// 测试模式,打印验证码到日志
|
// 测试模式,打印验证码到日志
|
||||||
|
|
@ -823,27 +829,32 @@ public class H5AuthServiceImpl implements H5AuthService {
|
||||||
incrementCounter(dailyPhoneKey, Duration.ofDays(1));
|
incrementCounter(dailyPhoneKey, Duration.ofDays(1));
|
||||||
|
|
||||||
if (smsProperties.isEnabled()) {
|
if (smsProperties.isEnabled()) {
|
||||||
try {
|
SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName());
|
||||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
if (smsBlend == null) {
|
||||||
map.put("code", code);
|
log.warn("短信服务未配置(smsBlend=null),降级为测试模式");
|
||||||
SmsBlend smsBlend = SmsFactory.getSmsBlend(smsProperties.getSmsConfigName());
|
log.info("【测试模式】微信绑定短信验证码: phone={}, code={}", phone, code);
|
||||||
String templateId = smsProperties.getLoginTemplateId();
|
} else {
|
||||||
SmsResponse smsResponse;
|
try {
|
||||||
if (StringUtils.isNotBlank(templateId)) {
|
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||||
smsResponse = smsBlend.sendMessage(phone, templateId, map);
|
map.put("code", code);
|
||||||
} else {
|
String templateId = smsProperties.getLoginTemplateId();
|
||||||
smsResponse = smsBlend.sendMessage(phone, map);
|
SmsResponse smsResponse;
|
||||||
}
|
if (StringUtils.isNotBlank(templateId)) {
|
||||||
if (!smsResponse.isSuccess()) {
|
smsResponse = smsBlend.sendMessage(phone, templateId, map);
|
||||||
log.error("短信发送失败: phone={}, response={}", phone, smsResponse);
|
} 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("短信发送失败,请稍后重试");
|
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 {
|
} else {
|
||||||
log.info("【测试模式】微信绑定短信验证码: phone={}, code={}", phone, code);
|
log.info("【测试模式】微信绑定短信验证码: phone={}, code={}", phone, code);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import org.dromara.pangu.member.domain.PgMember;
|
||||||
import org.dromara.pangu.member.domain.PgMemberStudent;
|
import org.dromara.pangu.member.domain.PgMemberStudent;
|
||||||
import org.dromara.pangu.member.domain.dto.EducationDto;
|
import org.dromara.pangu.member.domain.dto.EducationDto;
|
||||||
import org.dromara.pangu.member.domain.dto.MemberSaveDto;
|
import org.dromara.pangu.member.domain.dto.MemberSaveDto;
|
||||||
|
import org.dromara.pangu.common.RegionPermissionHelper;
|
||||||
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
import org.dromara.pangu.member.mapper.PgMemberMapper;
|
||||||
import org.dromara.pangu.member.mapper.PgMemberStudentMapper;
|
import org.dromara.pangu.member.mapper.PgMemberStudentMapper;
|
||||||
import org.dromara.pangu.member.service.IPgEducationService;
|
import org.dromara.pangu.member.service.IPgEducationService;
|
||||||
|
|
@ -24,6 +25,7 @@ import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -42,19 +44,44 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
||||||
private final PgMemberStudentMapper memberStudentMapper;
|
private final PgMemberStudentMapper memberStudentMapper;
|
||||||
private final IPgEducationService educationService;
|
private final IPgEducationService educationService;
|
||||||
private final IPgStudentService studentService;
|
private final IPgStudentService studentService;
|
||||||
|
private final RegionPermissionHelper regionPermissionHelper;
|
||||||
|
|
||||||
private static final String DEFAULT_PASSWORD = "123456";
|
private static final String DEFAULT_PASSWORD = "123456";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TableDataInfo<PgMember> selectPageList(PgMember member, PageQuery pageQuery) {
|
public TableDataInfo<PgMember> selectPageList(PgMember member, PageQuery pageQuery) {
|
||||||
LambdaQueryWrapper<PgMember> lqw = buildQueryWrapper(member);
|
LambdaQueryWrapper<PgMember> lqw = buildQueryWrapper(member);
|
||||||
|
|
||||||
|
// 添加区域数据权限过滤
|
||||||
|
List<Long> regionIds = regionPermissionHelper.getAccessibleRegionIds();
|
||||||
|
if (regionIds != null) {
|
||||||
|
// regionIds 为 null 表示超管,不限制
|
||||||
|
// regionIds 为空列表表示无权限
|
||||||
|
if (regionIds.isEmpty()) {
|
||||||
|
log.info("用户无区域权限,返回空列表");
|
||||||
|
return TableDataInfo.build(new Page<>());
|
||||||
|
}
|
||||||
|
lqw.in(PgMember::getRegionId, regionIds);
|
||||||
|
}
|
||||||
|
|
||||||
Page<PgMember> page = baseMapper.selectPage(pageQuery.build(), lqw);
|
Page<PgMember> page = baseMapper.selectPage(pageQuery.build(), lqw);
|
||||||
return TableDataInfo.build(page);
|
return TableDataInfo.build(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PgMember> selectList(PgMember member) {
|
public List<PgMember> selectList(PgMember member) {
|
||||||
return baseMapper.selectList(buildQueryWrapper(member));
|
LambdaQueryWrapper<PgMember> lqw = buildQueryWrapper(member);
|
||||||
|
|
||||||
|
// 添加区域数据权限过滤
|
||||||
|
List<Long> regionIds = regionPermissionHelper.getAccessibleRegionIds();
|
||||||
|
if (regionIds != null) {
|
||||||
|
if (regionIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
lqw.in(PgMember::getRegionId, regionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseMapper.selectList(lqw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public class OpenSchoolVo implements Serializable {
|
||||||
@Schema(description = "学校名称")
|
@Schema(description = "学校名称")
|
||||||
private String schoolName;
|
private String schoolName;
|
||||||
|
|
||||||
@Schema(description = "学校类型(1小学 2初中 3高中 4完全中学)")
|
@Schema(description = "学校类型(1小学 2初中 3高中 4中专 5大学)")
|
||||||
private String schoolType;
|
private String schoolType;
|
||||||
|
|
||||||
@Schema(description = "区域ID")
|
@Schema(description = "区域ID")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class PgSchool extends BaseEntity {
|
||||||
private String schoolName;
|
private String schoolName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学校类型(1小学 2初中 3高中 4完全中学)
|
* 学校类型(1小学 2初中 3高中 4中专 5大学)
|
||||||
*/
|
*/
|
||||||
private String schoolType;
|
private String schoolType;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 学校班级表
|
* 学校班级表
|
||||||
|
*
|
||||||
|
* 注意:ID 使用数据库自增策略(AUTO),避免雪花算法生成的大数字超出 JavaScript 安全整数范围
|
||||||
*
|
*
|
||||||
* @author pangu
|
* @author pangu
|
||||||
*/
|
*/
|
||||||
|
|
@ -18,7 +20,7 @@ import java.util.Date;
|
||||||
@TableName("pg_school_class")
|
@TableName("pg_school_class")
|
||||||
public class PgSchoolClass implements Serializable {
|
public class PgSchoolClass implements Serializable {
|
||||||
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private Long schoolId;
|
private Long schoolId;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ public class SchoolTreeNode {
|
||||||
*/
|
*/
|
||||||
private Long regionId;
|
private Long regionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门ID(仅学校节点有值)
|
||||||
|
*/
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态(0正常 1停用)
|
* 状态(0正常 1停用)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package org.dromara.pangu.school.mapper;
|
package org.dromara.pangu.school.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.dromara.common.mybatis.annotation.DataColumn;
|
import org.dromara.common.mybatis.annotation.DataColumn;
|
||||||
import org.dromara.common.mybatis.annotation.DataPermission;
|
import org.dromara.common.mybatis.annotation.DataPermission;
|
||||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||||
import org.dromara.pangu.school.domain.PgSchool;
|
import org.dromara.pangu.school.domain.PgSchool;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,4 +46,24 @@ public interface PgSchoolMapper extends BaseMapperPlus<PgSchool, PgSchool> {
|
||||||
default Page<PgSchool> selectPageSchoolList(Page<PgSchool> page, Wrapper<PgSchool> queryWrapper) {
|
default Page<PgSchool> selectPageSchoolList(Page<PgSchool> page, Wrapper<PgSchool> queryWrapper) {
|
||||||
return this.selectPage(page, queryWrapper);
|
return this.selectPage(page, queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据部门ID列表查询关联学校的区域ID
|
||||||
|
* <p>
|
||||||
|
* 用于区域数据权限过滤:获取用户所管理学校的区域ID列表
|
||||||
|
*
|
||||||
|
* @param deptIds 部门ID列表
|
||||||
|
* @return 区域ID列表(去重)
|
||||||
|
*/
|
||||||
|
default List<Long> selectRegionIdsByDeptIds(@Param("deptIds") List<Long> deptIds) {
|
||||||
|
if (deptIds == null || deptIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<PgSchool> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.select(PgSchool::getRegionId)
|
||||||
|
.in(PgSchool::getDeptId, deptIds)
|
||||||
|
.isNotNull(PgSchool::getRegionId)
|
||||||
|
.groupBy(PgSchool::getRegionId);
|
||||||
|
return this.selectObjs(wrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,7 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
|
||||||
schoolNode.setSchoolType(s.getSchoolType());
|
schoolNode.setSchoolType(s.getSchoolType());
|
||||||
schoolNode.setRegionName(s.getRegionName());
|
schoolNode.setRegionName(s.getRegionName());
|
||||||
schoolNode.setRegionId(s.getRegionId());
|
schoolNode.setRegionId(s.getRegionId());
|
||||||
|
schoolNode.setDeptId(s.getDeptId());
|
||||||
schoolNode.setStatus(s.getStatus());
|
schoolNode.setStatus(s.getStatus());
|
||||||
schoolNode.setCreateTime(s.getCreateTime() != null ? sdf.format(s.getCreateTime()) : null);
|
schoolNode.setCreateTime(s.getCreateTime() != null ? sdf.format(s.getCreateTime()) : null);
|
||||||
// 使用昵称而不是ID
|
// 使用昵称而不是ID
|
||||||
|
|
@ -433,7 +434,15 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
|
||||||
.map(sc -> {
|
.map(sc -> {
|
||||||
SchoolTreeNode classNode = new SchoolTreeNode();
|
SchoolTreeNode classNode = new SchoolTreeNode();
|
||||||
classNode.setId("class_" + sc.getId()); // 添加类型前缀确保唯一
|
classNode.setId("class_" + sc.getId()); // 添加类型前缀确保唯一
|
||||||
classNode.setName(finalClassNameMap.getOrDefault(sc.getClassId(), ""));
|
// 优先使用基础班级名称,如果为空则使用班级别名,最后使用默认值
|
||||||
|
String className = finalClassNameMap.getOrDefault(sc.getClassId(), null);
|
||||||
|
if (className == null || className.isEmpty()) {
|
||||||
|
className = sc.getClassAlias();
|
||||||
|
}
|
||||||
|
if (className == null || className.isEmpty()) {
|
||||||
|
className = "未设置班级";
|
||||||
|
}
|
||||||
|
classNode.setName(className);
|
||||||
classNode.setType("class");
|
classNode.setType("class");
|
||||||
classNode.setSchoolId(sc.getSchoolId());
|
classNode.setSchoolId(sc.getSchoolId());
|
||||||
classNode.setSchoolGradeId(sc.getSchoolGradeId());
|
classNode.setSchoolGradeId(sc.getSchoolGradeId());
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,31 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int insert(PgStudent student) {
|
public int insert(PgStudent student) {
|
||||||
|
// 根据学生所属学校设置部门ID(用于数据权限)
|
||||||
|
if (student.getSchoolId() != null) {
|
||||||
|
PgSchool school = schoolMapper.selectById(student.getSchoolId());
|
||||||
|
if (school != null && school.getDeptId() != null) {
|
||||||
|
student.setDeptId(school.getDeptId());
|
||||||
|
log.info("设置学生部门ID: studentName={}, schoolId={}, schoolName={}, deptId={}",
|
||||||
|
student.getStudentName(), student.getSchoolId(), school.getSchoolName(), school.getDeptId());
|
||||||
|
} else {
|
||||||
|
log.warn("学校部门ID为空: schoolId={}", student.getSchoolId());
|
||||||
|
}
|
||||||
|
}
|
||||||
return baseMapper.insert(student);
|
return baseMapper.insert(student);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int update(PgStudent student) {
|
public int update(PgStudent student) {
|
||||||
|
// 如果学校变更,同步更新部门ID(用于数据权限)
|
||||||
|
if (student.getSchoolId() != null) {
|
||||||
|
PgSchool school = schoolMapper.selectById(student.getSchoolId());
|
||||||
|
if (school != null && school.getDeptId() != null) {
|
||||||
|
student.setDeptId(school.getDeptId());
|
||||||
|
log.info("更新学生部门ID: studentId={}, schoolId={}, schoolName={}, deptId={}",
|
||||||
|
student.getStudentId(), student.getSchoolId(), school.getSchoolName(), school.getDeptId());
|
||||||
|
}
|
||||||
|
}
|
||||||
return baseMapper.updateById(student);
|
return baseMapper.updateById(student);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,8 +224,19 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
||||||
|
|
||||||
// 填充班级名称
|
// 填充班级名称
|
||||||
PgSchoolClass schoolClass = schoolClassMap.get(s.getSchoolClassId());
|
PgSchoolClass schoolClass = schoolClassMap.get(s.getSchoolClassId());
|
||||||
if (schoolClass != null && schoolClass.getClassId() != null) {
|
if (schoolClass != null) {
|
||||||
vo.setClassName(classNameMap.get(schoolClass.getClassId()));
|
if (schoolClass.getClassId() != null) {
|
||||||
|
// 优先使用基础班级名称
|
||||||
|
vo.setClassName(classNameMap.get(schoolClass.getClassId()));
|
||||||
|
}
|
||||||
|
// 如果基础班级名称为空,使用班级别名
|
||||||
|
if (vo.getClassName() == null && schoolClass.getClassAlias() != null) {
|
||||||
|
vo.setClassName(schoolClass.getClassAlias());
|
||||||
|
}
|
||||||
|
// 如果仍然为空,设置默认值
|
||||||
|
if (vo.getClassName() == null) {
|
||||||
|
vo.setClassName("未设置班级");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填充会员信息(多对多)
|
// 填充会员信息(多对多)
|
||||||
|
|
|
||||||
|
|
@ -76,5 +76,9 @@
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
<artifactId>pangu-common-sse</artifactId>
|
<artifactId>pangu-common-sse</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.oshi</groupId>
|
||||||
|
<artifactId>oshi-core</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@
|
||||||
<artifactId>pangu-common-sse</artifactId>
|
<artifactId>pangu-common-sse</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OSHI 服务监控 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.oshi</groupId>
|
||||||
|
<artifactId>oshi-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -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<ServerVo> 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<ServerVo.SysFileVo> buildSysFiles(OperatingSystem os) {
|
||||||
|
List<ServerVo.SysFileVo> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SysFileVo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,8 @@
|
||||||
<anyline.version>8.7.3-20251210</anyline.version>
|
<anyline.version>8.7.3-20251210</anyline.version>
|
||||||
<!-- 工作流配置 -->
|
<!-- 工作流配置 -->
|
||||||
<warm-flow.version>1.8.4</warm-flow.version>
|
<warm-flow.version>1.8.4</warm-flow.version>
|
||||||
|
<!-- OSHI 服务监控 -->
|
||||||
|
<oshi.version>6.4.0</oshi.version>
|
||||||
|
|
||||||
<!-- 插件版本 -->
|
<!-- 插件版本 -->
|
||||||
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
||||||
|
|
@ -326,6 +328,13 @@
|
||||||
<version>${ip2region.version}</version>
|
<version>${ip2region.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OSHI 服务监控(CPU/内存/JVM/磁盘) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.oshi</groupId>
|
||||||
|
<artifactId>oshi-core</artifactId>
|
||||||
|
<version>${oshi.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>fastjson</artifactId>
|
<artifactId>fastjson</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
| **文档版本** | V1.0 |
|
| **文档版本** | V1.0 |
|
||||||
| **项目名称** | 盘古用户平台(Pangu User Platform) |
|
| **项目名称** | 盘古用户平台(Pangu User Platform) |
|
||||||
| **编写团队** | pangu |
|
| **编写团队** | pangu |
|
||||||
| **创建日期** | 2026-01-31 |
|
| **创建日期** | 2026-01-26 |
|
||||||
| **审核状态** | 待审核 |
|
| **审核状态** | 已审核 |
|
||||||
| **原型链接** | https://modao.cc/proto/atlwMul8t9pi6yxQ42W7up/sharing |
|
| **原型链接** | https://modao.cc/proto/atlwMul8t9pi6yxQ42W7up/sharing |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -17,8 +17,9 @@
|
||||||
|
|
||||||
| 版本 | 日期 | 修订人 | 修订内容 |
|
| 版本 | 日期 | 修订人 | 修订内容 |
|
||||||
| ---- | ---------- | ----- | ---- |
|
| ---- | ---------- | ----- | ---- |
|
||||||
| V1.0 | 2026-01-31 | pangu | 初稿 |
|
| V1.0 | 2026-01-26 | pangu | 初稿 |
|
||||||
| V1.1 | 2026-02-03 | pangu | 更新会员多教育身份模型、微信扫码登录流程 |
|
| V1.1 | 2026-02-03 | pangu | 更新会员多教育身份模型、微信扫码登录流程 |
|
||||||
|
| V1.2 | 2026-02-05 | pangu | 补充验收实现状态与系统监控(服务监控)说明 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -957,6 +958,8 @@
|
||||||
| 学生管理 | 批量导入功能正常,数据校验完整 |
|
| 学生管理 | 批量导入功能正常,数据校验完整 |
|
||||||
| 应用管理 | 接口授权可控制API访问权限 |
|
| 应用管理 | 接口授权可控制API访问权限 |
|
||||||
| 基础数据 | 年级、班级、学科、区域CRUD正常 |
|
| 基础数据 | 年级、班级、学科、区域CRUD正常 |
|
||||||
|
| 系统设置 | 用户、角色、菜单、部门、字典、参数 |
|
||||||
|
| 系统监控 | 在线用户、登录/操作日志、服务监控(CPU/内存/JVM/磁盘)、缓存监控 |
|
||||||
| 权限控制 | 三种角色权限划分符合需求 |
|
| 权限控制 | 三种角色权限划分符合需求 |
|
||||||
|
|
||||||
### 10.2 性能验收标准
|
### 10.2 性能验收标准
|
||||||
|
|
|
||||||
|
|
@ -61,33 +61,41 @@
|
||||||
| | Element Plus | 2.x | UI组件库 |
|
| | Element Plus | 2.x | UI组件库 |
|
||||||
| | Axios | 1.x | HTTP客户端 |
|
| | Axios | 1.x | HTTP客户端 |
|
||||||
| | Pinia | 2.x | 状态管理 |
|
| | Pinia | 2.x | 状态管理 |
|
||||||
| **后端** | Spring Boot | 3.3.x | 应用框架(LTS版本) |
|
| **后端** | Spring Boot | 3.5.x | 应用框架 |
|
||||||
| | Spring Security | 6.x | 安全框架 |
|
| | Sa-Token | 1.44.x | 认证与权限 |
|
||||||
| | MyBatis Plus | 3.5.x | ORM框架 |
|
| | MyBatis Plus | 3.5.x | ORM框架 |
|
||||||
| | JWT | 0.12.x | Token认证 |
|
|
||||||
| | Hutool | 5.x | 工具库 |
|
| | Hutool | 5.x | 工具库 |
|
||||||
| | JDK | 17+ | 运行环境(LTS) |
|
| | JDK | 17+ | 运行环境(LTS) |
|
||||||
| **数据库** | MySQL | 8.0 | 主数据库 |
|
| **数据库** | MySQL | 8.0 | 主数据库 |
|
||||||
| | Redis | 7.x | 缓存 |
|
| | Redis | 7.x | 缓存 |
|
||||||
| **中间件** | Nginx | 1.20+ | 反向代理 |
|
| **中间件** | Nginx | 1.20+ | 反向代理 |
|
||||||
| | MinIO | - | 文件存储(可选)|
|
| | MinIO | - | 文件存储(可选)|
|
||||||
| **基础框架** | RuoYi-Vue | 3.8.x | 快速开发框架 |
|
| **基础框架** | RuoYi-Vue-Plus | 5.5.x | 多租户快速开发框架(Dromara) |
|
||||||
|
|
||||||
### 1.3 模块划分
|
### 1.3 模块划分
|
||||||
|
|
||||||
```
|
```
|
||||||
pangu-user-platform/
|
pangu-user-platform/
|
||||||
├── pangu-admin/ # 后台管理模块
|
├── backend/
|
||||||
├── pangu-api/ # 移动端API模块
|
│ ├── pangu-admin/ # 启动与认证入口(Spring Boot 主模块)
|
||||||
├── pangu-open/ # 开放平台API模块
|
│ ├── pangu-common/ # 公共模块
|
||||||
├── pangu-common/ # 公共模块
|
│ │ ├── pangu-common-core/ # 核心工具、常量、异常
|
||||||
│ ├── pangu-common-core/ # 核心工具
|
│ │ ├── pangu-common-redis/ # Redis/Redisson
|
||||||
│ ├── pangu-common-redis/ # Redis工具
|
│ │ ├── pangu-common-security/# 安全与加解密
|
||||||
│ └── pangu-common-security/# 安全工具
|
│ │ ├── pangu-common-satoken/ # Sa-Token 认证
|
||||||
├── pangu-framework/ # 框架核心
|
│ │ ├── pangu-common-mybatis/# MyBatis Plus 扩展
|
||||||
├── pangu-system/ # 系统模块(复用RuoYi)
|
│ │ ├── pangu-common-web/ # Web 配置与工具
|
||||||
├── pangu-generator/ # 代码生成器
|
│ │ └── 其他 common 子模块 # 日志、OSS、短信、租户等
|
||||||
└── frontend/ # 前端工程
|
│ ├── pangu-modules/
|
||||||
|
│ │ ├── pangu-system/ # 系统模块(用户、角色、菜单、部门、字典、监控等)
|
||||||
|
│ │ ├── pangu-business/ # 业务模块(学校、会员、学生、应用、基础数据、H5、开放API)
|
||||||
|
│ │ ├── pangu-generator/ # 代码生成器
|
||||||
|
│ │ ├── pangu-job/ # 定时任务(SnailJob)
|
||||||
|
│ │ ├── pangu-demo/ # 示例模块
|
||||||
|
│ │ └── pangu-workflow/ # 工作流(可选)
|
||||||
|
│ ├── pangu-extend/ # 扩展(Monitor Admin、SnailJob Server 等)
|
||||||
|
│ └── pom.xml # 后端父 POM(RuoYi-Vue-Plus 5.5.x)
|
||||||
|
└── frontend/ # 管理后台前端(Vue 3 + Element Plus + Vite)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
| 项目 | 本地最新版本 | 服务器当前版本 | 待发布提交数 |
|
| 项目 | 本地最新版本 | 服务器当前版本 | 待发布提交数 |
|
||||||
|------|-------------|---------------|-------------|
|
|------|-------------|---------------|-------------|
|
||||||
| pangu-user-platform (后端+管理后台) | add00c9 | 2026-02-03 22:29 部署 | 5 个 |
|
| pangu-user-platform (后端+管理后台) | a9bbb30 | 2026-02-03 22:29 部署 | 6 个 |
|
||||||
| user_authentication_center_front (H5前端) | 70fc1ad | 2026-02-03 22:04 部署 | 2 个 |
|
| user_authentication_center_front (H5前端) | 76f27ce | 2026-02-03 22:04 部署 | 3 个 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -22,28 +22,33 @@
|
||||||
|
|
||||||
| 序号 | Commit ID | 提交说明 | 类型 |
|
| 序号 | Commit ID | 提交说明 | 类型 |
|
||||||
|------|----------|---------|------|
|
|------|----------|---------|------|
|
||||||
| 1 | add00c9 | 新增学校自动添加年级 + 修复区域层级查询 + 清理区域数据 | feat |
|
| 1 | a9bbb30 | 业务功能按钮权限统一(v-hasPermi,与角色配置一致) | fix |
|
||||||
| 2 | 72cb666 | 年级管理增加学段字段(小学/初中/高中/中专/大学) | feat |
|
| 2 | add00c9 | 新增学校自动添加年级 + 修复区域层级查询 + 清理区域数据 | feat |
|
||||||
| 3 | 80dd406 | 新增OpenApi基础数据接口 + 学生完整数据接口 + UI文案优化 | feat |
|
| 3 | 72cb666 | 年级管理增加学段字段(小学/初中/高中/中专/大学) | feat |
|
||||||
| 4 | 1a0b75e | 同步需求与技术方案文档 | docs |
|
| 4 | 80dd406 | 新增OpenApi基础数据接口 + 学生完整数据接口 + UI文案优化 | feat |
|
||||||
| 5 | 6027a8c | 修改后端欢迎语为盘古后台管理系统 | refactor |
|
| 5 | 1a0b75e | 同步需求与技术方案文档 | docs |
|
||||||
|
| 6 | 6027a8c | 修改后端欢迎语为盘古后台管理系统 | refactor |
|
||||||
|
|
||||||
### 2.2 功能变更摘要
|
### 2.2 功能变更摘要
|
||||||
|
|
||||||
1. **学校管理优化**
|
1. **业务功能按钮权限统一(本次新增)**
|
||||||
|
- 学生、会员、学校、应用管理、接口字典、年级、班级、学科、区域等列表页的「新增」「编辑」「删除」「导入」「导出」等按钮增加 `v-hasPermi` 控制
|
||||||
|
- 与角色管理中配置的菜单权限一致,无权限用户不显示对应按钮
|
||||||
|
|
||||||
|
2. **学校管理优化**
|
||||||
- 新增学校时自动添加对应学段的年级
|
- 新增学校时自动添加对应学段的年级
|
||||||
- 修复选择省/市时无法显示学校的Bug(支持区域层级查询)
|
- 修复选择省/市时无法显示学校的Bug(支持区域层级查询)
|
||||||
- 区域树默认只展开湖北省,平行显示市级
|
- 区域树默认只展开湖北省,平行显示市级
|
||||||
|
|
||||||
2. **年级管理增强**
|
3. **年级管理增强**
|
||||||
- 新增学段字段(小学/初中/高中/中专/大学)
|
- 新增学段字段(小学/初中/高中/中专/大学)
|
||||||
- 支持按学段筛选年级
|
- 支持按学段筛选年级
|
||||||
|
|
||||||
3. **OpenAPI接口扩展**
|
4. **OpenAPI接口扩展**
|
||||||
- 新增学校/年级/班级基础数据查询接口
|
- 新增学校/年级/班级基础数据查询接口
|
||||||
- 新增学生完整数据接口(不脱敏,需特殊授权)
|
- 新增学生完整数据接口(不脱敏,需特殊授权)
|
||||||
|
|
||||||
4. **UI文案优化**
|
5. **UI文案优化**
|
||||||
- "教育身份" 改为 "任教信息"
|
- "教育身份" 改为 "任教信息"
|
||||||
- 后端欢迎语改为 "盘古后台管理系统"
|
- 后端欢迎语改为 "盘古后台管理系统"
|
||||||
|
|
||||||
|
|
@ -91,16 +96,22 @@
|
||||||
|
|
||||||
| 序号 | Commit ID | 提交说明 | 类型 |
|
| 序号 | Commit ID | 提交说明 | 类型 |
|
||||||
|------|----------|---------|------|
|
|------|----------|---------|------|
|
||||||
| 1 | 70fc1ad | 优化登录注册页面交互和弹窗内容 | feat |
|
| 1 | 76f27ce | 会员支持所在区域:注册和个人中心可填可改,教育身份和绑定学生表单默认带出区域 | feat |
|
||||||
| 2 | 842d64f | 允许通过IP地址访问H5前端 | fix |
|
| 2 | 70fc1ad | 优化登录注册页面交互和弹窗内容 | feat |
|
||||||
|
| 3 | 842d64f | 允许通过IP地址访问H5前端 | fix |
|
||||||
|
|
||||||
### 3.2 功能变更摘要
|
### 3.2 功能变更摘要
|
||||||
|
|
||||||
1. **登录注册优化**
|
1. **会员所在区域(本次新增)**
|
||||||
|
- 注册时增加所在区域选择(级联)
|
||||||
|
- 个人中心可查看、修改所在区域
|
||||||
|
- 添加教育身份、绑定学生时区域默认带出会员区域,可修改
|
||||||
|
|
||||||
|
2. **登录注册优化**
|
||||||
- 优化登录注册页面交互体验
|
- 优化登录注册页面交互体验
|
||||||
- 改进弹窗内容展示
|
- 改进弹窗内容展示
|
||||||
|
|
||||||
2. **访问限制修复**
|
3. **访问限制修复**
|
||||||
- 允许通过IP地址直接访问H5前端
|
- 允许通过IP地址直接访问H5前端
|
||||||
|
|
||||||
### 3.3 服务器部署路径
|
### 3.3 服务器部署路径
|
||||||
|
|
@ -195,11 +206,13 @@ cd /opt/pangu-user-platform/scripts
|
||||||
|--------|---------|---------|
|
|--------|---------|---------|
|
||||||
| 后端服务 | `curl http://localhost:9083/actuator/health` | {"status":"UP"} |
|
| 后端服务 | `curl http://localhost:9083/actuator/health` | {"status":"UP"} |
|
||||||
| 管理后台登录 | 浏览器访问管理后台 | 正常登录 |
|
| 管理后台登录 | 浏览器访问管理后台 | 正常登录 |
|
||||||
|
| 权限按钮显隐 | 用无增删改权限账号登录,进入学生/会员等列表 | 仅显示查询,不显示新增/编辑/删除等按钮 |
|
||||||
| 学校管理-区域树 | 查看区域树展开状态 | 默认展开湖北省 |
|
| 学校管理-区域树 | 查看区域树展开状态 | 默认展开湖北省 |
|
||||||
| 学校管理-层级查询 | 选择武汉市查看学校 | 显示武汉市下所有学校 |
|
| 学校管理-层级查询 | 选择武汉市查看学校 | 显示武汉市下所有学校 |
|
||||||
| 年级管理-学段 | 查看年级列表 | 显示学段列 |
|
| 年级管理-学段 | 查看年级列表 | 显示学段列 |
|
||||||
| OpenAPI接口 | 调用新增的接口 | 正常返回数据 |
|
| OpenAPI接口 | 调用新增的接口 | 正常返回数据 |
|
||||||
| H5前端登录 | 浏览器访问H5 | 正常登录 |
|
| 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)两项提交。*
|
||||||
|
|
|
||||||
|
|
@ -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 <PROD库> -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 时执行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*检查说明:按上述顺序与条件执行,可避免重复加列报错与误删区域数据。*
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2018 RuoYi
|
Copyright (c) 2018 PanGu
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "ruoyi",
|
"name": "pangu",
|
||||||
"version": "3.9.1",
|
"version": "3.9.1",
|
||||||
"description": "盘古管理系统",
|
"description": "盘古管理系统",
|
||||||
"author": "盘古",
|
"author": "盘古",
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
|
"url": "https://gitlab.dodoedu.com/root/pangu-user-platform.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 登录方法 (RuoYi-Vue-Plus)
|
// 登录方法
|
||||||
export function login(username, password, code, uuid) {
|
export function login(username, password, code, uuid) {
|
||||||
const data = {
|
const data = {
|
||||||
username,
|
username,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 获取路由 (RuoYi-Vue-Plus)
|
// 获取路由
|
||||||
export const getRouters = () => {
|
export const getRouters = () => {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/menu/getRouters',
|
url: '/system/menu/getRouters',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* 通用css样式布局处理
|
* 通用css样式布局处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 pangu
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 基础通用 **/
|
/** 基础通用 **/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* v-copyText 复制文本内容
|
* v-copyText 复制文本内容
|
||||||
* Copyright (c) 2022 ruoyi
|
* Copyright (c) 2022 pangu
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
beforeMount(el, { value, arg }) {
|
beforeMount(el, { value, arg }) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* v-hasPermi 操作权限处理
|
* v-hasPermi 操作权限处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 pangu
|
||||||
*/
|
*/
|
||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* v-hasRole 角色权限处理
|
* v-hasRole 角色权限处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 pangu
|
||||||
*/
|
*/
|
||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const useUserStore = defineStore(
|
||||||
permissions: []
|
permissions: []
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
// 登录 (RuoYi-Vue-Plus)
|
// 登录
|
||||||
login(userInfo) {
|
login(userInfo) {
|
||||||
const username = userInfo.username.trim()
|
const username = userInfo.username.trim()
|
||||||
const password = userInfo.password
|
const password = userInfo.password
|
||||||
|
|
@ -26,7 +26,7 @@ const useUserStore = defineStore(
|
||||||
const uuid = userInfo.uuid
|
const uuid = userInfo.uuid
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
login(username, password, code, uuid).then(res => {
|
login(username, password, code, uuid).then(res => {
|
||||||
// RuoYi-Vue-Plus 返回格式: { code, msg, data: { access_token, ... } }
|
// 返回格式: { code, msg, data: { access_token, ... } }
|
||||||
const data = res.data || res
|
const data = res.data || res
|
||||||
const token = data.access_token || data.token
|
const token = data.access_token || data.token
|
||||||
setToken(token)
|
setToken(token)
|
||||||
|
|
@ -37,11 +37,11 @@ const useUserStore = defineStore(
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 获取用户信息 (RuoYi-Vue-Plus)
|
// 获取用户信息
|
||||||
getInfo() {
|
getInfo() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
getInfo().then(res => {
|
getInfo().then(res => {
|
||||||
// RuoYi-Vue-Plus 返回格式: { code, msg, data: { user, permissions, roles } }
|
// 返回格式: { code, msg, data: { user, permissions, roles } }
|
||||||
const data = res.data || res
|
const data = res.data || res
|
||||||
const user = data.user
|
const user = data.user
|
||||||
let avatar = user.avatar || ""
|
let avatar = user.avatar || ""
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ service.interceptors.request.use(config => {
|
||||||
if (getToken() && !isToken) {
|
if (getToken() && !isToken) {
|
||||||
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
}
|
}
|
||||||
// RuoYi-Vue-Plus 需要 clientid header
|
// 需要 clientid header
|
||||||
config.headers['clientid'] = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
config.headers['clientid'] = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||||
// get请求映射params参数
|
// get请求映射params参数
|
||||||
if (config.method === 'get' && config.params) {
|
if (config.method === 'get' && config.params) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* 通用js方法封装处理
|
* 通用js方法封装处理
|
||||||
* Copyright (c) 2019 ruoyi
|
* Copyright (c) 2019 pangu
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 日期格式化
|
// 日期格式化
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
v-if="refreshTable"
|
v-if="refreshTable"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
|
|
@ -119,6 +120,7 @@ const tableData = ref([])
|
||||||
const regionOptions = ref([])
|
const regionOptions = ref([])
|
||||||
const refreshTable = ref(true)
|
const refreshTable = ref(true)
|
||||||
const isExpandAll = ref(false) // 默认折叠,避免渲染大量节点
|
const isExpandAll = ref(false) // 默认折叠,避免渲染大量节点
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
regionName: '',
|
regionName: '',
|
||||||
|
|
@ -141,18 +143,71 @@ const rules = {
|
||||||
regionName: [{ required: true, message: '请输入区域名称', trigger: 'blur' }]
|
regionName: [{ required: true, message: '请输入区域名称', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取区域列表(树形)- 使用 Store 缓存
|
// 按区域名称过滤树:保留名称包含 keyword 的节点及其祖先,递归过滤子节点
|
||||||
|
const filterTreeByKeyword = (list, keyword, status) => {
|
||||||
|
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 () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 使用 Store,自动缓存
|
const hasQuery = (queryParams.value.regionName || '').trim() || (queryParams.value.status !== '' && queryParams.value.status !== undefined)
|
||||||
const data = await baseDataStore.fetchRegionTree()
|
const data = await baseDataStore.fetchRegionTree(!!hasQuery)
|
||||||
tableData.value = data || []
|
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 {
|
} finally {
|
||||||
loading.value = false
|
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 getRegionOptions = async () => {
|
||||||
const data = await baseDataStore.fetchRegionTree()
|
const data = await baseDataStore.fetchRegionTree()
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
<el-option label="小学" value="1" />
|
<el-option label="小学" value="1" />
|
||||||
<el-option label="初中" value="2" />
|
<el-option label="初中" value="2" />
|
||||||
<el-option label="高中" value="3" />
|
<el-option label="高中" value="3" />
|
||||||
<el-option label="九年一贯制" value="4" />
|
<el-option label="中专" value="4" />
|
||||||
<el-option label="完全中学" value="5" />
|
<el-option label="大学" value="5" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上级部门" prop="deptId">
|
<el-form-item label="上级部门" prop="deptId">
|
||||||
|
|
@ -184,14 +184,14 @@ const handleRegionChange = (value) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 学校类型对应学段映射
|
// 学校类型对应学段映射(与 pg_grade.stage 一致:1小学 2初中 3高中 4中专 5大学)
|
||||||
const getStagesBySchoolType = (schoolType) => {
|
const getStagesBySchoolType = (schoolType) => {
|
||||||
const map = {
|
const map = {
|
||||||
'1': ['1'], // 小学
|
'1': ['1'], // 小学
|
||||||
'2': ['2'], // 初中
|
'2': ['2'], // 初中
|
||||||
'3': ['3'], // 高中
|
'3': ['3'], // 高中
|
||||||
'4': ['1', '2'], // 九年一贯制
|
'4': ['4'], // 中专
|
||||||
'5': ['2', '3'] // 完全中学
|
'5': ['5'] // 大学
|
||||||
}
|
}
|
||||||
return map[schoolType] || []
|
return map[schoolType] || []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,10 +242,11 @@ const handleAdd = () => {
|
||||||
// 编辑学校
|
// 编辑学校
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
const schoolData = {
|
const schoolData = {
|
||||||
schoolId: row.id,
|
schoolId: row.schoolId, // 使用 schoolId 而不是 id(id 是 "school_1" 格式的字符串)
|
||||||
schoolCode: row.schoolCode,
|
schoolCode: row.schoolCode,
|
||||||
schoolName: row.name,
|
schoolName: row.name,
|
||||||
schoolType: row.schoolType,
|
schoolType: row.schoolType,
|
||||||
|
deptId: row.deptId,
|
||||||
regionId: row.regionId,
|
regionId: row.regionId,
|
||||||
regionName: row.regionName,
|
regionName: row.regionName,
|
||||||
status: row.status
|
status: row.status
|
||||||
|
|
@ -261,7 +262,7 @@ const handleAddGrade = (row) => {
|
||||||
.map(child => child.gradeId)
|
.map(child => child.gradeId)
|
||||||
|
|
||||||
const schoolData = {
|
const schoolData = {
|
||||||
schoolId: row.id,
|
schoolId: row.schoolId, // 使用 schoolId 而不是 id
|
||||||
schoolName: row.name,
|
schoolName: row.name,
|
||||||
existingGradeIds: existingGradeIds
|
existingGradeIds: existingGradeIds
|
||||||
}
|
}
|
||||||
|
|
@ -307,10 +308,18 @@ const countChildren = (node) => {
|
||||||
return { gradeCount, classCount }
|
return { gradeCount, classCount }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 row.id 中提取真正的数字 ID(row.id 格式为 "school_1", "grade_2", "class_3")
|
||||||
|
const extractId = (rowId) => {
|
||||||
|
if (!rowId) return null
|
||||||
|
const parts = rowId.split('_')
|
||||||
|
return parts.length > 1 ? parts[1] : rowId
|
||||||
|
}
|
||||||
|
|
||||||
// 删除操作 - 删除时提示是否有子级(有子级给出提示,确认后级联删除)
|
// 删除操作 - 删除时提示是否有子级(有子级给出提示,确认后级联删除)
|
||||||
const handleDelete = (row, type) => {
|
const handleDelete = (row, type) => {
|
||||||
let message = ''
|
let message = ''
|
||||||
let url = ''
|
let url = ''
|
||||||
|
const realId = extractId(row.id)
|
||||||
|
|
||||||
if (type === 'school') {
|
if (type === 'school') {
|
||||||
const { gradeCount, classCount } = countChildren(row)
|
const { gradeCount, classCount } = countChildren(row)
|
||||||
|
|
@ -323,7 +332,7 @@ const handleDelete = (row, type) => {
|
||||||
} else {
|
} else {
|
||||||
message = `确定要删除学校"${row.name}"吗?`
|
message = `确定要删除学校"${row.name}"吗?`
|
||||||
}
|
}
|
||||||
url = `/business/school/${row.id}`
|
url = `/business/school/${realId}`
|
||||||
} else if (type === 'grade') {
|
} else if (type === 'grade') {
|
||||||
const classCount = (row.children || []).filter(c => c.type === 'class').length
|
const classCount = (row.children || []).filter(c => c.type === 'class').length
|
||||||
if (classCount > 0) {
|
if (classCount > 0) {
|
||||||
|
|
@ -332,10 +341,10 @@ const handleDelete = (row, type) => {
|
||||||
} else {
|
} else {
|
||||||
message = `确定要删除年级"${row.name}"吗?`
|
message = `确定要删除年级"${row.name}"吗?`
|
||||||
}
|
}
|
||||||
url = `/business/school/grade/${row.id}`
|
url = `/business/school/grade/${realId}`
|
||||||
} else {
|
} else {
|
||||||
message = `确定要删除班级"${row.name}"吗?`
|
message = `确定要删除班级"${row.name}"吗?`
|
||||||
url = `/business/school/class/${row.id}`
|
url = `/business/school/class/${realId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessageBox.confirm(message, '提示', {
|
ElMessageBox.confirm(message, '提示', {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ function handleLogin() {
|
||||||
|
|
||||||
function getCode() {
|
function getCode() {
|
||||||
getCodeImg().then(res => {
|
getCodeImg().then(res => {
|
||||||
// RuoYi-Vue-Plus 返回格式: { code, msg, data: { captchaEnabled, uuid, img } }
|
// 返回格式: { code, msg, data: { captchaEnabled, uuid, img } }
|
||||||
const data = res.data || res
|
const data = res.data || res
|
||||||
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled
|
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled
|
||||||
if (captchaEnabled.value) {
|
if (captchaEnabled.value) {
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,8 @@
|
||||||
<el-tooltip placement="top">
|
<el-tooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>
|
<div>
|
||||||
Bean调用示例:ryTask.ryParams('ry')
|
Bean调用示例:pgTask.pgParams('pg')
|
||||||
<br />Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
|
<br />Class类调用示例:org.dromara.pangu.quartz.task.PgTask.pgParams('pg')
|
||||||
<br />参数说明:支持字符串,布尔类型,长整型,浮点型,整型
|
<br />参数说明:支持字符串,布尔类型,长整型,浮点型,整型
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,9 @@ function getList() {
|
||||||
getServer().then(response => {
|
getServer().then(response => {
|
||||||
server.value = response.data
|
server.value = response.data
|
||||||
proxy.$modal.closeLoading()
|
proxy.$modal.closeLoading()
|
||||||
|
}).catch(() => {
|
||||||
|
proxy.$modal.closeLoading()
|
||||||
|
proxy.$modal.msgError("加载服务监控数据失败")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ const open = (school) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
currentSchool.value = school
|
currentSchool.value = school
|
||||||
form.value = {
|
form.value = {
|
||||||
schoolId: school.id,
|
schoolId: school.schoolId,
|
||||||
gradeId: '',
|
gradeId: '',
|
||||||
classIds: []
|
classIds: []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ const open = (school) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
currentSchool.value = school
|
currentSchool.value = school
|
||||||
form.value = {
|
form.value = {
|
||||||
schoolId: school.id,
|
schoolId: school.schoolId,
|
||||||
gradeIds: []
|
gradeIds: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const isEdit = ref(false)
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
const form = ref({
|
||||||
id: null,
|
schoolId: null,
|
||||||
schoolName: '',
|
schoolName: '',
|
||||||
schoolType: '',
|
schoolType: '',
|
||||||
regionId: null,
|
regionId: null,
|
||||||
|
|
@ -144,7 +144,7 @@ const open = (row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
// 编辑模式:回显数据
|
// 编辑模式:回显数据
|
||||||
form.value = {
|
form.value = {
|
||||||
id: row.id,
|
schoolId: row.schoolId,
|
||||||
schoolName: row.schoolName,
|
schoolName: row.schoolName,
|
||||||
schoolType: row.schoolType,
|
schoolType: row.schoolType,
|
||||||
regionId: row.regionId,
|
regionId: row.regionId,
|
||||||
|
|
@ -155,7 +155,7 @@ const open = (row) => {
|
||||||
} else {
|
} else {
|
||||||
// 新增模式:重置表单
|
// 新增模式:重置表单
|
||||||
form.value = {
|
form.value = {
|
||||||
id: null,
|
schoolId: null,
|
||||||
schoolName: '',
|
schoolName: '',
|
||||||
schoolType: '',
|
schoolType: '',
|
||||||
regionId: null,
|
regionId: null,
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ const handleDelete = (row) => {
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
const res = await request.delete(`/api/school/${row.id}`)
|
const res = await request.delete(`/api/school/${row.schoolId}`)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
getList()
|
getList()
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,19 @@
|
||||||
width="600px"
|
width="600px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
@open="handleOpen"
|
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="form"
|
:model="form"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="100px"
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
>
|
>
|
||||||
<el-form-item label="姓名" prop="name">
|
<el-form-item label="姓名" prop="studentName">
|
||||||
<el-input v-model="form.name" placeholder="请输入学生姓名" maxlength="20" />
|
<el-input v-model="form.studentName" placeholder="请输入学生姓名" maxlength="50" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="学号" prop="studentNo">
|
<el-form-item label="学号" prop="studentNo">
|
||||||
<el-input v-model="form.studentNo" placeholder="请输入学号" maxlength="30" />
|
<el-input v-model="form.studentNo" placeholder="请输入学号(选填)" maxlength="30" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="性别" prop="gender">
|
<el-form-item label="性别" prop="gender">
|
||||||
<el-radio-group v-model="form.gender">
|
<el-radio-group v-model="form.gender">
|
||||||
|
|
@ -29,39 +29,42 @@
|
||||||
<el-form-item label="出生日期" prop="birthday">
|
<el-form-item label="出生日期" prop="birthday">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="form.birthday"
|
v-model="form.birthday"
|
||||||
type="month"
|
type="date"
|
||||||
placeholder="请选择出生年月"
|
placeholder="请选择出生日期"
|
||||||
format="YYYY-MM"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM"
|
value-format="YYYY-MM-DD"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="学校信息" prop="schoolPath" required>
|
<el-form-item label="学校信息" prop="schoolClassId" required>
|
||||||
<el-cascader
|
<el-cascader
|
||||||
v-model="form.schoolPath"
|
v-model="form.schoolPath"
|
||||||
:options="schoolTree"
|
:options="schoolTreeData"
|
||||||
:props="{
|
:props="{
|
||||||
value: 'id',
|
value: 'id',
|
||||||
label: 'label',
|
label: 'name',
|
||||||
children: 'children',
|
children: 'children',
|
||||||
checkStrictly: false
|
checkStrictly: false
|
||||||
}"
|
}"
|
||||||
placeholder="请选择学校/年级/班级"
|
placeholder="请选择学校/年级/班级"
|
||||||
clearable
|
clearable
|
||||||
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
@change="handleSchoolChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="学科" prop="subject">
|
<el-form-item label="归属用户">
|
||||||
<el-select v-model="form.subject" placeholder="请选择学科" clearable style="width: 100%">
|
<template v-if="form.members && form.members.length > 0">
|
||||||
<el-option v-for="item in subjectList" :key="item.id" :label="item.name" :value="item.name" />
|
<el-tag
|
||||||
</el-select>
|
v-for="(m, idx) in form.members"
|
||||||
</el-form-item>
|
:key="idx"
|
||||||
<el-form-item label="归属用户" prop="userId">
|
size="small"
|
||||||
<el-input v-model="form.userNickname" placeholder="请输入用户昵称搜索" readonly>
|
style="margin-right: 6px; margin-bottom: 4px;"
|
||||||
<template #append>
|
>
|
||||||
<el-button @click="handleSelectUser">选择</el-button>
|
{{ m.nickname || m.phone }}{{ m.relation ? `(${m.relation})` : '' }}
|
||||||
</template>
|
</el-tag>
|
||||||
</el-input>
|
</template>
|
||||||
|
<span v-else style="color: #909399;">暂无归属用户</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
@ -76,95 +79,140 @@
|
||||||
* 学生新增/编辑弹窗
|
* 学生新增/编辑弹窗
|
||||||
* @author pangu
|
* @author pangu
|
||||||
*/
|
*/
|
||||||
import { addStudent, getStudent, updateStudent } from '@/api/pangu/student'
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed, reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
const props = defineProps({
|
const emit = defineEmits(['success'])
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
studentId: {
|
|
||||||
type: [Number, null],
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
schoolTree: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
subjectList: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'success'])
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => emit('update:modelValue', val)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isEdit = computed(() => !!props.studentId)
|
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
|
const formLoading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
|
// 学校树数据
|
||||||
|
const schoolTreeData = ref([])
|
||||||
|
|
||||||
const initialForm = {
|
const initialForm = {
|
||||||
id: null,
|
studentId: null,
|
||||||
name: '',
|
studentName: '',
|
||||||
studentNo: '',
|
studentNo: '',
|
||||||
gender: '1',
|
gender: '1',
|
||||||
birthday: '',
|
birthday: '',
|
||||||
schoolPath: [],
|
schoolPath: [],
|
||||||
subject: '',
|
schoolId: null,
|
||||||
userId: null,
|
schoolGradeId: null,
|
||||||
userNickname: ''
|
schoolClassId: null,
|
||||||
|
members: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = reactive({ ...initialForm })
|
const form = reactive({ ...initialForm })
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: [
|
studentName: [
|
||||||
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
|
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
schoolPath: [
|
schoolClassId: [
|
||||||
{ required: true, message: '请选择学校/年级/班级', trigger: 'change' }
|
{ required: true, message: '请选择学校/年级/班级', trigger: 'change' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹窗打开时
|
// 获取学校树数据
|
||||||
const handleOpen = async () => {
|
const getSchoolTree = async () => {
|
||||||
Object.assign(form, initialForm)
|
try {
|
||||||
formRef.value?.clearValidate()
|
const res = await request.get('/business/student/schoolTree')
|
||||||
|
if (res.code === 200) {
|
||||||
|
schoolTreeData.value = res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取学校树失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (props.studentId) {
|
// 格式化出生日期
|
||||||
|
const formatBirthday = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
const d = new Date(date)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 学校选择变化
|
||||||
|
// 级联选择器的 value 格式是 type_id(如 school_1, grade_2, class_3)
|
||||||
|
const handleSchoolChange = (value) => {
|
||||||
|
if (value && value.length === 3) {
|
||||||
|
// 从 type_id 格式中提取数字 ID
|
||||||
|
form.schoolId = parseInt(value[0].split('_')[1])
|
||||||
|
form.schoolGradeId = parseInt(value[1].split('_')[1])
|
||||||
|
form.schoolClassId = parseInt(value[2].split('_')[1])
|
||||||
|
} else {
|
||||||
|
form.schoolId = null
|
||||||
|
form.schoolGradeId = null
|
||||||
|
form.schoolClassId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const open = async (row = null) => {
|
||||||
|
visible.value = true
|
||||||
|
isEdit.value = !!row
|
||||||
|
formLoading.value = true
|
||||||
|
|
||||||
|
// 重置表单 - 使用逐个字段赋值保持响应式
|
||||||
|
form.studentId = null
|
||||||
|
form.studentName = ''
|
||||||
|
form.studentNo = ''
|
||||||
|
form.gender = '1'
|
||||||
|
form.birthday = ''
|
||||||
|
form.schoolPath.length = 0 // 清空数组但保持引用
|
||||||
|
form.schoolId = null
|
||||||
|
form.schoolGradeId = null
|
||||||
|
form.schoolClassId = null
|
||||||
|
form.members.length = 0
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
|
||||||
|
// 获取基础数据
|
||||||
|
await getSchoolTree()
|
||||||
|
|
||||||
|
// 编辑模式:加载学生详情
|
||||||
|
if (row) {
|
||||||
try {
|
try {
|
||||||
const res = await getStudent(props.studentId)
|
const res = await request.get(`/business/student/${row.studentId}`)
|
||||||
if (res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
const data = res.data
|
const data = res.data
|
||||||
form.id = data.id
|
form.studentId = data.studentId
|
||||||
form.name = data.name
|
form.studentName = data.studentName
|
||||||
form.studentNo = data.studentNo
|
form.studentNo = data.studentNo
|
||||||
form.gender = data.gender
|
form.gender = data.gender || '0'
|
||||||
form.birthday = data.birthday
|
form.birthday = data.birthday ? formatBirthday(data.birthday) : ''
|
||||||
form.subject = data.subject
|
if (data.members) {
|
||||||
form.userId = data.userId
|
form.members.push(...data.members)
|
||||||
form.userNickname = data.userNickname
|
}
|
||||||
// 需要根据实际数据结构构建 schoolPath
|
|
||||||
form.schoolPath = [data.schoolId]
|
// 构建学校路径用于级联选择器回显
|
||||||
|
// 注意:级联选择器的 id 格式是 type_id(如 school_1, grade_2, class_3)
|
||||||
|
if (data.schoolId && data.schoolGradeId && data.schoolClassId) {
|
||||||
|
// 使用 nextTick 确保树数据已渲染
|
||||||
|
const path = [
|
||||||
|
`school_${data.schoolId}`,
|
||||||
|
`grade_${data.schoolGradeId}`,
|
||||||
|
`class_${data.schoolClassId}`
|
||||||
|
]
|
||||||
|
form.schoolPath.push(...path)
|
||||||
|
form.schoolId = data.schoolId
|
||||||
|
form.schoolGradeId = data.schoolGradeId
|
||||||
|
form.schoolClassId = data.schoolClassId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取学生详情失败:', e)
|
console.error('获取学生详情失败:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
formLoading.value = false
|
||||||
// 选择用户
|
|
||||||
const handleSelectUser = () => {
|
|
||||||
// 简化处理,实际应该弹出用户选择器
|
|
||||||
ElMessage.info('用户选择功能需要对接会员管理模块')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交
|
// 提交
|
||||||
|
|
@ -174,22 +222,31 @@ const handleSubmit = async () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验学校信息
|
||||||
|
if (!form.schoolClassId) {
|
||||||
|
ElMessage.warning('请选择完整的学校/年级/班级信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...form,
|
studentId: form.studentId,
|
||||||
schoolId: form.schoolPath[0],
|
studentName: form.studentName,
|
||||||
gradeId: form.schoolPath[1],
|
studentNo: form.studentNo,
|
||||||
classId: form.schoolPath[2]
|
gender: form.gender,
|
||||||
|
birthday: form.birthday,
|
||||||
|
schoolId: form.schoolId,
|
||||||
|
schoolGradeId: form.schoolGradeId,
|
||||||
|
schoolClassId: form.schoolClassId
|
||||||
}
|
}
|
||||||
delete submitData.schoolPath
|
|
||||||
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateStudent(submitData)
|
await request.put('/business/student', submitData)
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
} else {
|
} else {
|
||||||
await addStudent(submitData)
|
await request.post('/business/student', submitData)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
}
|
}
|
||||||
visible.value = false
|
visible.value = false
|
||||||
|
|
@ -200,4 +257,6 @@ const handleSubmit = async () => {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
:data="schoolTree"
|
:data="schoolTree"
|
||||||
:props="{ label: 'label', children: 'children' }"
|
:props="{ label: 'name', children: 'children' }"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
highlight-current
|
highlight-current
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<el-card shadow="never" class="search-wrapper">
|
<el-card shadow="never" class="search-wrapper">
|
||||||
<el-form :model="queryParams" :inline="true">
|
<el-form :model="queryParams" :inline="true">
|
||||||
<el-form-item label="学生姓名">
|
<el-form-item label="学生姓名">
|
||||||
<el-input v-model="queryParams.name" placeholder="请输入学生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="学号">
|
<el-form-item label="学号">
|
||||||
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable style="width: 150px" @keyup.enter="handleQuery" />
|
||||||
|
|
@ -59,19 +59,33 @@
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
|
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
|
||||||
<el-table-column prop="studentNo" label="学号" width="130" />
|
<el-table-column prop="studentNo" label="学号" width="130" />
|
||||||
<el-table-column prop="name" label="姓名" width="100" />
|
<el-table-column prop="studentName" label="姓名" width="100" />
|
||||||
<el-table-column prop="gender" label="性别" width="60" align="center">
|
<el-table-column prop="gender" label="性别" width="60" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="birthday" label="出生年月" width="100" />
|
<el-table-column prop="birthday" label="出生日期" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.birthday ? row.birthday.substring(0, 10) : '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="gradeName" label="年级" width="80" />
|
<el-table-column prop="gradeName" label="年级" width="80" />
|
||||||
<el-table-column prop="className" label="班级" width="80" />
|
<el-table-column prop="className" label="班级" width="80" />
|
||||||
<el-table-column prop="subject" label="学科" width="80" />
|
<el-table-column label="归属用户" width="120" show-overflow-tooltip>
|
||||||
<el-table-column prop="userNickname" label="归属用户" width="100" show-overflow-tooltip />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
<template v-if="row.members && row.members.length > 0">
|
||||||
|
{{ row.members.map(m => m.nickname || m.phone).join(', ') }}
|
||||||
|
</template>
|
||||||
|
<span v-else style="color: #909399;">未绑定</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.createTime ? row.createTime.substring(0, 19).replace('T', ' ') : '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
|
@ -129,12 +143,12 @@ const total = ref(0)
|
||||||
const queryParams = ref({
|
const queryParams = ref({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
name: '',
|
studentName: '',
|
||||||
studentNo: '',
|
studentNo: '',
|
||||||
gender: '',
|
gender: '',
|
||||||
schoolId: '',
|
schoolId: '',
|
||||||
gradeId: '',
|
schoolGradeId: '',
|
||||||
classId: ''
|
schoolClassId: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 弹窗引用
|
// 弹窗引用
|
||||||
|
|
@ -149,12 +163,12 @@ watch(treeFilterText, (val) => {
|
||||||
// 树节点过滤
|
// 树节点过滤
|
||||||
const filterNode = (value, data) => {
|
const filterNode = (value, data) => {
|
||||||
if (!value) return true
|
if (!value) return true
|
||||||
return data.label.includes(value)
|
return data.name.includes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取学校树
|
// 获取学校树
|
||||||
const getSchoolTree = async () => {
|
const getSchoolTree = async () => {
|
||||||
const res = await request.get('/api/student/schoolTree')
|
const res = await request.get('/business/student/schoolTree')
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
schoolTree.value = res.data
|
schoolTree.value = res.data
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +178,7 @@ const getSchoolTree = async () => {
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await request.get('/api/student/list', { params: queryParams.value })
|
const res = await request.get('/business/student/list', { params: queryParams.value })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
tableData.value = res.rows
|
tableData.value = res.rows
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
|
|
@ -174,18 +188,28 @@ const getList = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从节点 id 中提取数字 ID(格式为 "school_1", "grade_2", "class_3")
|
||||||
|
const extractId = (nodeId) => {
|
||||||
|
if (!nodeId) return ''
|
||||||
|
const parts = nodeId.split('_')
|
||||||
|
return parts.length > 1 ? parts[1] : nodeId
|
||||||
|
}
|
||||||
|
|
||||||
// 树节点点击
|
// 树节点点击
|
||||||
const handleNodeClick = (data) => {
|
const handleNodeClick = (data) => {
|
||||||
// 根据节点层级设置筛选条件
|
// 根据节点层级设置筛选条件,提取真正的数字 ID
|
||||||
if (data.type === 'school') {
|
if (data.type === 'school') {
|
||||||
queryParams.value.schoolId = data.id
|
queryParams.value.schoolId = extractId(data.id)
|
||||||
queryParams.value.gradeId = ''
|
queryParams.value.schoolGradeId = ''
|
||||||
queryParams.value.classId = ''
|
queryParams.value.schoolClassId = ''
|
||||||
} else if (data.type === 'grade') {
|
} else if (data.type === 'grade') {
|
||||||
queryParams.value.gradeId = data.id
|
queryParams.value.schoolId = data.schoolId // 年级节点有 schoolId 字段
|
||||||
queryParams.value.classId = ''
|
queryParams.value.schoolGradeId = extractId(data.id)
|
||||||
|
queryParams.value.schoolClassId = ''
|
||||||
} else if (data.type === 'class') {
|
} else if (data.type === 'class') {
|
||||||
queryParams.value.classId = data.id
|
queryParams.value.schoolId = data.schoolId // 班级节点有 schoolId 字段
|
||||||
|
queryParams.value.schoolGradeId = data.schoolGradeId // 班级节点有 schoolGradeId 字段
|
||||||
|
queryParams.value.schoolClassId = extractId(data.id)
|
||||||
}
|
}
|
||||||
queryParams.value.pageNum = 1
|
queryParams.value.pageNum = 1
|
||||||
getList()
|
getList()
|
||||||
|
|
@ -202,12 +226,12 @@ const resetQuery = () => {
|
||||||
queryParams.value = {
|
queryParams.value = {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
name: '',
|
studentName: '',
|
||||||
studentNo: '',
|
studentNo: '',
|
||||||
gender: '',
|
gender: '',
|
||||||
schoolId: '',
|
schoolId: '',
|
||||||
gradeId: '',
|
schoolGradeId: '',
|
||||||
classId: ''
|
schoolClassId: ''
|
||||||
}
|
}
|
||||||
treeRef.value?.setCurrentKey(null)
|
treeRef.value?.setCurrentKey(null)
|
||||||
getList()
|
getList()
|
||||||
|
|
@ -230,12 +254,12 @@ const handleImport = () => {
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
ElMessageBox.confirm(`确定要删除学生"${row.name}"吗?`, '提示', {
|
ElMessageBox.confirm(`确定要删除学生"${row.studentName}"吗?`, '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
const res = await request.delete(`/api/student/${row.id}`)
|
const res = await request.delete(`/business/student/${row.studentId}`)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
getList()
|
getList()
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<el-form-item prop="packageName">
|
<el-form-item prop="packageName">
|
||||||
<template #label>
|
<template #label>
|
||||||
生成包路径
|
生成包路径
|
||||||
<el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">
|
<el-tooltip content="生成在哪个java包下,例如 org.dromara.pangu" placement="top">
|
||||||
<el-icon><question-filled /></el-icon>
|
<el-icon><question-filled /></el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ function handleGenTable(row) {
|
||||||
proxy.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
|
proxy.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const zipName = Array.isArray(tbNames) ? "ruoyi.zip" : tbNames + ".zip"
|
const zipName = Array.isArray(tbNames) ? "pangu.zip" : tbNames + ".zip"
|
||||||
proxy.$download.zip("/tool/gen/batchGenCode?tables=" + tbNames, zipName)
|
proxy.$download.zip("/tool/gen/batchGenCode?tables=" + tbNames, zipName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default defineConfig(({ mode, command }) => {
|
||||||
return {
|
return {
|
||||||
// 部署生产环境和开发环境下的URL。
|
// 部署生产环境和开发环境下的URL。
|
||||||
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
||||||
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
|
// 例如 https://www.pangu.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.pangu.vip/admin/,则设置 baseUrl 为 /admin/。
|
||||||
base: VITE_APP_ENV === 'production' ? '/' : '/',
|
base: VITE_APP_ENV === 'production' ? '/' : '/',
|
||||||
plugins: createVitePlugins(env, command === 'build'),
|
plugins: createVitePlugins(env, command === 'build'),
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
@ -42,7 +42,7 @@ export default defineConfig(({ mode, command }) => {
|
||||||
},
|
},
|
||||||
// vite 相关配置
|
// vite 相关配置
|
||||||
server: {
|
server: {
|
||||||
port: 80,
|
port: 3000,
|
||||||
host: true,
|
host: true,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export default function createCompression(env) {
|
||||||
if (VITE_BUILD_COMPRESS) {
|
if (VITE_BUILD_COMPRESS) {
|
||||||
const compressList = VITE_BUILD_COMPRESS.split(',')
|
const compressList = VITE_BUILD_COMPRESS.split(',')
|
||||||
if (compressList.includes('gzip')) {
|
if (compressList.includes('gzip')) {
|
||||||
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
|
// 使用gzip解压缩静态文件
|
||||||
plugin.push(
|
plugin.push(
|
||||||
compression({
|
compression({
|
||||||
ext: '.gz',
|
ext: '.gz',
|
||||||
|
|
|
||||||
|
|
@ -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 "清理完成。"
|
||||||
|
|
@ -1,21 +1,41 @@
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- 年级表增加学段字段 - V1.0.4
|
-- 年级表增加学段字段 - V1.0.4(幂等,可重复执行)
|
||||||
-- 执行时间: 2026-02-04
|
-- 执行时间: 2026-02-04
|
||||||
-- 说明: 年级管理增加学段(小学/初中/高中/中专/大学)
|
-- 说明: 年级管理增加学段(小学/初中/高中/中专/大学)
|
||||||
|
-- PROD 对比: 若 pg_grade 已有 stage 列则仅做空值回填,否则加列并初始化
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
-- 1. 新增学段字段
|
DELIMITER //
|
||||||
ALTER TABLE pg_grade ADD COLUMN stage CHAR(1) NULL COMMENT '学段(1小学 2初中 3高中 4中专 5大学)' AFTER grade_name;
|
DROP PROCEDURE IF EXISTS proc_grade_add_stage//
|
||||||
|
CREATE PROCEDURE proc_grade_add_stage()
|
||||||
|
BEGIN
|
||||||
|
DECLARE col_exists INT DEFAULT 0;
|
||||||
|
|
||||||
-- 2. 初始化现有数据(根据年级名称自动匹配学段)
|
SELECT COUNT(*) INTO col_exists
|
||||||
-- 小学
|
FROM information_schema.COLUMNS
|
||||||
UPDATE pg_grade SET stage = '1' WHERE grade_name IN ('一年级', '二年级', '三年级', '四年级', '五年级', '六年级');
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
-- 初中
|
AND TABLE_NAME = 'pg_grade'
|
||||||
UPDATE pg_grade SET stage = '2' WHERE grade_name IN ('七年级', '八年级', '九年级');
|
AND COLUMN_NAME = 'stage';
|
||||||
-- 高中
|
|
||||||
UPDATE pg_grade SET stage = '3' WHERE grade_name IN ('高一', '高二', '高三');
|
|
||||||
-- 未匹配的默认设为小学
|
|
||||||
UPDATE pg_grade SET stage = '1' WHERE stage IS NULL;
|
|
||||||
|
|
||||||
-- 3. 设置为非空并添加默认值
|
IF col_exists = 0 THEN
|
||||||
ALTER TABLE pg_grade MODIFY COLUMN stage CHAR(1) NOT NULL DEFAULT '1' COMMENT '学段(1小学 2初中 3高中 4中专 5大学)';
|
-- 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;
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,26 @@
|
||||||
-- ================================================================
|
-- ================================================================
|
||||||
-- V1.0.5 清理非湖北省区域数据
|
-- V1.0.5 清理非湖北省区域数据(幂等,可重复执行)
|
||||||
-- 执行说明:先检查湖北省的region_id,确认为42后再执行删除
|
-- 执行说明:先确认湖北省 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 = '湖北省';
|
SELECT region_id, region_name, parent_id, ancestors FROM pg_region WHERE region_name = '湖北省';
|
||||||
-- 预期结果:region_id = 42
|
|
||||||
|
|
||||||
-- 1. 查看要删除的数据量(预检查,不会执行删除)
|
-- 1. 预检查:将要删除的行数(非湖北省及其下级的区域;湖北省 region_id=42,其下级 ancestors 以 0,42 开头)
|
||||||
SELECT COUNT(*) as '待删除的区域数量' FROM pg_region
|
SELECT COUNT(*) AS '待删除的区域数量(非湖北)' FROM pg_region
|
||||||
WHERE region_id != 42
|
WHERE region_id != 42
|
||||||
AND ancestors NOT LIKE '0,42%';
|
AND ancestors NOT LIKE '0,42%';
|
||||||
|
|
||||||
-- 2. 物理删除非湖北省的区域数据
|
-- 2. 物理删除:非湖北省及其下级(保留 region_id=42 及 ancestors 以 0,42 开头的行)
|
||||||
-- 删除所有ancestors不以"0,42"开头的区域(即非湖北省及其下级区域)
|
DELETE FROM pg_region
|
||||||
-- 同时排除湖北省本身(region_id=42)
|
WHERE region_id != 42
|
||||||
DELETE FROM pg_region
|
|
||||||
WHERE region_id != 42
|
|
||||||
AND ancestors NOT LIKE '0,42%';
|
AND ancestors NOT LIKE '0,42%';
|
||||||
|
|
||||||
-- 3. 删除其他省份的根节点(parent_id = 0 且 region_id != 42)
|
-- 3. 删除其他省份根节点(parent_id=0 且 region_id 非 42)
|
||||||
DELETE FROM pg_region
|
DELETE FROM pg_region
|
||||||
WHERE parent_id = 0
|
WHERE parent_id = 0
|
||||||
AND region_id != 42;
|
AND region_id != 42;
|
||||||
|
|
||||||
-- 4. 验证结果
|
-- 4. 验证结果
|
||||||
SELECT COUNT(*) as '剩余区域数量' FROM pg_region;
|
SELECT COUNT(*) AS '剩余区域数量' FROM pg_region;
|
||||||
SELECT region_name, level, COUNT(*) as '数量' FROM pg_region GROUP BY level, region_name LIMIT 20;
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
@ -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 -- 测试树表按钮
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue