sync: 同步prod仓库的最新更新
- 新增区域权限过滤功能 (RegionPermissionHelper) - 修复学生dept_id自动同步逻辑 - 修复学校班级ID精度丢失问题 (雪花ID改为自增) - 新增SQL迁移脚本 (fix_student_dept_id, production_migrate_school_class_id) - 更新前端组件 (学生、会员、学校管理) - 品牌更新: ruoyi -> pangu Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0b528e03d8
commit
f3b302c9fd
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
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/开放接口实现说明.md)
|
||||
- [PHP 客户端使用指南](/scripts/test/README.md)
|
||||
- [技术方案](/docs/2-技术方案.md)
|
||||
- [接口文档](/docs/3-接口文档.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);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
-- ============================================================
|
||||
-- 盘古用户平台 - 业务模块建表脚本(适配 RuoYi-Vue-Plus 5.x)
|
||||
-- 盘古用户平台 - 业务模块建表脚本
|
||||
-- 作者:pangu
|
||||
-- 创建时间:2026-02-02
|
||||
-- ============================================================
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import org.dromara.pangu.member.domain.PgMember;
|
|||
import org.dromara.pangu.member.domain.PgMemberStudent;
|
||||
import org.dromara.pangu.member.domain.dto.EducationDto;
|
||||
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.PgMemberStudentMapper;
|
||||
import org.dromara.pangu.member.service.IPgEducationService;
|
||||
|
|
@ -24,6 +25,7 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -42,19 +44,44 @@ public class PgMemberServiceImpl implements IPgMemberService {
|
|||
private final PgMemberStudentMapper memberStudentMapper;
|
||||
private final IPgEducationService educationService;
|
||||
private final IPgStudentService studentService;
|
||||
private final RegionPermissionHelper regionPermissionHelper;
|
||||
|
||||
private static final String DEFAULT_PASSWORD = "123456";
|
||||
|
||||
@Override
|
||||
public TableDataInfo<PgMember> selectPageList(PgMember member, PageQuery pageQuery) {
|
||||
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);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import java.util.Date;
|
|||
|
||||
/**
|
||||
* 学校班级表
|
||||
*
|
||||
* 注意:ID 使用数据库自增策略(AUTO),避免雪花算法生成的大数字超出 JavaScript 安全整数范围
|
||||
*
|
||||
* @author pangu
|
||||
*/
|
||||
|
|
@ -18,7 +20,7 @@ import java.util.Date;
|
|||
@TableName("pg_school_class")
|
||||
public class PgSchoolClass implements Serializable {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long schoolId;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ public class SchoolTreeNode {
|
|||
*/
|
||||
private Long regionId;
|
||||
|
||||
/**
|
||||
* 部门ID(仅学校节点有值)
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 状态(0正常 1停用)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package org.dromara.pangu.school.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.DataPermission;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.pangu.school.domain.PgSchool;
|
||||
|
||||
import java.util.Collections;
|
||||
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) {
|
||||
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.setRegionName(s.getRegionName());
|
||||
schoolNode.setRegionId(s.getRegionId());
|
||||
schoolNode.setDeptId(s.getDeptId());
|
||||
schoolNode.setStatus(s.getStatus());
|
||||
schoolNode.setCreateTime(s.getCreateTime() != null ? sdf.format(s.getCreateTime()) : null);
|
||||
// 使用昵称而不是ID
|
||||
|
|
@ -433,7 +434,15 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
|
|||
.map(sc -> {
|
||||
SchoolTreeNode classNode = new SchoolTreeNode();
|
||||
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.setSchoolId(sc.getSchoolId());
|
||||
classNode.setSchoolGradeId(sc.getSchoolGradeId());
|
||||
|
|
|
|||
|
|
@ -88,11 +88,31 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -204,8 +224,19 @@ public class PgStudentServiceImpl implements IPgStudentService {
|
|||
|
||||
// 填充班级名称
|
||||
PgSchoolClass schoolClass = schoolClassMap.get(s.getSchoolClassId());
|
||||
if (schoolClass != null && schoolClass.getClassId() != null) {
|
||||
vo.setClassName(classNameMap.get(schoolClass.getClassId()));
|
||||
if (schoolClass != null) {
|
||||
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("未设置班级");
|
||||
}
|
||||
}
|
||||
|
||||
// 填充会员信息(多对多)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
| **文档版本** | V1.0 |
|
||||
| **项目名称** | 盘古用户平台(Pangu User Platform) |
|
||||
| **编写团队** | pangu |
|
||||
| **创建日期** | 2026-01-31 |
|
||||
| **创建日期** | 2026-01-26 |
|
||||
| **审核状态** | 已审核 |
|
||||
| **原型链接** | https://modao.cc/proto/atlwMul8t9pi6yxQ42W7up/sharing |
|
||||
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
| 版本 | 日期 | 修订人 | 修订内容 |
|
||||
| ---- | ---------- | ----- | ---- |
|
||||
| V1.0 | 2026-01-31 | pangu | 初稿 |
|
||||
| V1.0 | 2026-01-26 | pangu | 初稿 |
|
||||
| V1.1 | 2026-02-03 | pangu | 更新会员多教育身份模型、微信扫码登录流程 |
|
||||
| V1.2 | 2026-02-05 | pangu | 补充验收实现状态与系统监控(服务监控)说明 |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "ruoyi",
|
||||
"name": "pangu",
|
||||
"version": "3.9.1",
|
||||
"description": "盘古管理系统",
|
||||
"author": "盘古",
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
|
||||
"url": "https://gitlab.dodoedu.com/root/pangu-user-platform.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"mockjs": "^1.1.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 登录方法 (RuoYi-Vue-Plus)
|
||||
// 登录方法
|
||||
export function login(username, password, code, uuid) {
|
||||
const data = {
|
||||
username,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 获取路由 (RuoYi-Vue-Plus)
|
||||
// 获取路由
|
||||
export const getRouters = () => {
|
||||
return request({
|
||||
url: '/system/menu/getRouters',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 通用css样式布局处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
* Copyright (c) 2019 pangu
|
||||
*/
|
||||
|
||||
/** 基础通用 **/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* v-copyText 复制文本内容
|
||||
* Copyright (c) 2022 ruoyi
|
||||
* Copyright (c) 2022 pangu
|
||||
*/
|
||||
export default {
|
||||
beforeMount(el, { value, arg }) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* v-hasPermi 操作权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
* Copyright (c) 2019 pangu
|
||||
*/
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* v-hasRole 角色权限处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
* Copyright (c) 2019 pangu
|
||||
*/
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const useUserStore = defineStore(
|
|||
permissions: []
|
||||
}),
|
||||
actions: {
|
||||
// 登录 (RuoYi-Vue-Plus)
|
||||
// 登录
|
||||
login(userInfo) {
|
||||
const username = userInfo.username.trim()
|
||||
const password = userInfo.password
|
||||
|
|
@ -26,7 +26,7 @@ const useUserStore = defineStore(
|
|||
const uuid = userInfo.uuid
|
||||
return new Promise((resolve, reject) => {
|
||||
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 token = data.access_token || data.token
|
||||
setToken(token)
|
||||
|
|
@ -37,11 +37,11 @@ const useUserStore = defineStore(
|
|||
})
|
||||
})
|
||||
},
|
||||
// 获取用户信息 (RuoYi-Vue-Plus)
|
||||
// 获取用户信息
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInfo().then(res => {
|
||||
// RuoYi-Vue-Plus 返回格式: { code, msg, data: { user, permissions, roles } }
|
||||
// 返回格式: { code, msg, data: { user, permissions, roles } }
|
||||
const data = res.data || res
|
||||
const user = data.user
|
||||
let avatar = user.avatar || ""
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ service.interceptors.request.use(config => {
|
|||
if (getToken() && !isToken) {
|
||||
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
}
|
||||
// RuoYi-Vue-Plus 需要 clientid header
|
||||
// 需要 clientid header
|
||||
config.headers['clientid'] = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||
// get请求映射params参数
|
||||
if (config.method === 'get' && config.params) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 通用js方法封装处理
|
||||
* Copyright (c) 2019 ruoyi
|
||||
* Copyright (c) 2019 pangu
|
||||
*/
|
||||
|
||||
// 日期格式化
|
||||
|
|
|
|||
|
|
@ -242,10 +242,11 @@ const handleAdd = () => {
|
|||
// 编辑学校
|
||||
const handleEdit = (row) => {
|
||||
const schoolData = {
|
||||
schoolId: row.id,
|
||||
schoolId: row.schoolId, // 使用 schoolId 而不是 id(id 是 "school_1" 格式的字符串)
|
||||
schoolCode: row.schoolCode,
|
||||
schoolName: row.name,
|
||||
schoolType: row.schoolType,
|
||||
deptId: row.deptId,
|
||||
regionId: row.regionId,
|
||||
regionName: row.regionName,
|
||||
status: row.status
|
||||
|
|
@ -261,7 +262,7 @@ const handleAddGrade = (row) => {
|
|||
.map(child => child.gradeId)
|
||||
|
||||
const schoolData = {
|
||||
schoolId: row.id,
|
||||
schoolId: row.schoolId, // 使用 schoolId 而不是 id
|
||||
schoolName: row.name,
|
||||
existingGradeIds: existingGradeIds
|
||||
}
|
||||
|
|
@ -307,10 +308,18 @@ const countChildren = (node) => {
|
|||
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) => {
|
||||
let message = ''
|
||||
let url = ''
|
||||
const realId = extractId(row.id)
|
||||
|
||||
if (type === 'school') {
|
||||
const { gradeCount, classCount } = countChildren(row)
|
||||
|
|
@ -323,7 +332,7 @@ const handleDelete = (row, type) => {
|
|||
} else {
|
||||
message = `确定要删除学校"${row.name}"吗?`
|
||||
}
|
||||
url = `/business/school/${row.id}`
|
||||
url = `/business/school/${realId}`
|
||||
} else if (type === 'grade') {
|
||||
const classCount = (row.children || []).filter(c => c.type === 'class').length
|
||||
if (classCount > 0) {
|
||||
|
|
@ -332,10 +341,10 @@ const handleDelete = (row, type) => {
|
|||
} else {
|
||||
message = `确定要删除年级"${row.name}"吗?`
|
||||
}
|
||||
url = `/business/school/grade/${row.id}`
|
||||
url = `/business/school/grade/${realId}`
|
||||
} else {
|
||||
message = `确定要删除班级"${row.name}"吗?`
|
||||
url = `/business/school/class/${row.id}`
|
||||
url = `/business/school/class/${realId}`
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(message, '提示', {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ function handleLogin() {
|
|||
|
||||
function getCode() {
|
||||
getCodeImg().then(res => {
|
||||
// RuoYi-Vue-Plus 返回格式: { code, msg, data: { captchaEnabled, uuid, img } }
|
||||
// 返回格式: { code, msg, data: { captchaEnabled, uuid, img } }
|
||||
const data = res.data || res
|
||||
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled
|
||||
if (captchaEnabled.value) {
|
||||
|
|
|
|||
|
|
@ -166,8 +166,8 @@
|
|||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<div>
|
||||
Bean调用示例:ryTask.ryParams('ry')
|
||||
<br />Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
|
||||
Bean调用示例:pgTask.pgParams('pg')
|
||||
<br />Class类调用示例:org.dromara.pangu.quartz.task.PgTask.pgParams('pg')
|
||||
<br />参数说明:支持字符串,布尔类型,长整型,浮点型,整型
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const open = (school) => {
|
|||
dialogVisible.value = true
|
||||
currentSchool.value = school
|
||||
form.value = {
|
||||
schoolId: school.id,
|
||||
schoolId: school.schoolId,
|
||||
gradeId: '',
|
||||
classIds: []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const open = (school) => {
|
|||
dialogVisible.value = true
|
||||
currentSchool.value = school
|
||||
form.value = {
|
||||
schoolId: school.id,
|
||||
schoolId: school.schoolId,
|
||||
gradeIds: []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const isEdit = ref(false)
|
|||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
id: null,
|
||||
schoolId: null,
|
||||
schoolName: '',
|
||||
schoolType: '',
|
||||
regionId: null,
|
||||
|
|
@ -144,7 +144,7 @@ const open = (row) => {
|
|||
if (row) {
|
||||
// 编辑模式:回显数据
|
||||
form.value = {
|
||||
id: row.id,
|
||||
schoolId: row.schoolId,
|
||||
schoolName: row.schoolName,
|
||||
schoolType: row.schoolType,
|
||||
regionId: row.regionId,
|
||||
|
|
@ -155,7 +155,7 @@ const open = (row) => {
|
|||
} else {
|
||||
// 新增模式:重置表单
|
||||
form.value = {
|
||||
id: null,
|
||||
schoolId: null,
|
||||
schoolName: '',
|
||||
schoolType: '',
|
||||
regionId: null,
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ const handleDelete = (row) => {
|
|||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await request.delete(`/api/school/${row.id}`)
|
||||
const res = await request.delete(`/api/school/${row.schoolId}`)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
|
|
|
|||
|
|
@ -5,19 +5,19 @@
|
|||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@open="handleOpen"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入学生姓名" maxlength="20" />
|
||||
<el-form-item label="姓名" prop="studentName">
|
||||
<el-input v-model="form.studentName" placeholder="请输入学生姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
<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 label="性别" prop="gender">
|
||||
<el-radio-group v-model="form.gender">
|
||||
|
|
@ -29,39 +29,42 @@
|
|||
<el-form-item label="出生日期" prop="birthday">
|
||||
<el-date-picker
|
||||
v-model="form.birthday"
|
||||
type="month"
|
||||
placeholder="请选择出生年月"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
type="date"
|
||||
placeholder="请选择出生日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="学校信息" prop="schoolPath" required>
|
||||
<el-form-item label="学校信息" prop="schoolClassId" required>
|
||||
<el-cascader
|
||||
v-model="form.schoolPath"
|
||||
:options="schoolTree"
|
||||
:options="schoolTreeData"
|
||||
:props="{
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
checkStrictly: false
|
||||
}"
|
||||
placeholder="请选择学校/年级/班级"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleSchoolChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="学科" prop="subject">
|
||||
<el-select v-model="form.subject" placeholder="请选择学科" clearable style="width: 100%">
|
||||
<el-option v-for="item in subjectList" :key="item.id" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="归属用户" prop="userId">
|
||||
<el-input v-model="form.userNickname" placeholder="请输入用户昵称搜索" readonly>
|
||||
<template #append>
|
||||
<el-button @click="handleSelectUser">选择</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-form-item label="归属用户">
|
||||
<template v-if="form.members && form.members.length > 0">
|
||||
<el-tag
|
||||
v-for="(m, idx) in form.members"
|
||||
:key="idx"
|
||||
size="small"
|
||||
style="margin-right: 6px; margin-bottom: 4px;"
|
||||
>
|
||||
{{ m.nickname || m.phone }}{{ m.relation ? `(${m.relation})` : '' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else style="color: #909399;">暂无归属用户</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -76,95 +79,140 @@
|
|||
* 学生新增/编辑弹窗
|
||||
* @author pangu
|
||||
*/
|
||||
import { addStudent, getStudent, updateStudent } from '@/api/pangu/student'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps({
|
||||
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 emit = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 学校树数据
|
||||
const schoolTreeData = ref([])
|
||||
|
||||
const initialForm = {
|
||||
id: null,
|
||||
name: '',
|
||||
studentId: null,
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '1',
|
||||
birthday: '',
|
||||
schoolPath: [],
|
||||
subject: '',
|
||||
userId: null,
|
||||
userNickname: ''
|
||||
schoolId: null,
|
||||
schoolGradeId: null,
|
||||
schoolClassId: null,
|
||||
members: []
|
||||
}
|
||||
|
||||
const form = reactive({ ...initialForm })
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
studentName: [
|
||||
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
|
||||
],
|
||||
schoolPath: [
|
||||
schoolClassId: [
|
||||
{ required: true, message: '请选择学校/年级/班级', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 弹窗打开时
|
||||
const handleOpen = async () => {
|
||||
Object.assign(form, initialForm)
|
||||
formRef.value?.clearValidate()
|
||||
// 获取学校树数据
|
||||
const getSchoolTree = async () => {
|
||||
try {
|
||||
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 {
|
||||
const res = await getStudent(props.studentId)
|
||||
if (res.data) {
|
||||
const res = await request.get(`/business/student/${row.studentId}`)
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data
|
||||
form.id = data.id
|
||||
form.name = data.name
|
||||
form.studentId = data.studentId
|
||||
form.studentName = data.studentName
|
||||
form.studentNo = data.studentNo
|
||||
form.gender = data.gender
|
||||
form.birthday = data.birthday
|
||||
form.subject = data.subject
|
||||
form.userId = data.userId
|
||||
form.userNickname = data.userNickname
|
||||
// 需要根据实际数据结构构建 schoolPath
|
||||
form.schoolPath = [data.schoolId]
|
||||
form.gender = data.gender || '0'
|
||||
form.birthday = data.birthday ? formatBirthday(data.birthday) : ''
|
||||
if (data.members) {
|
||||
form.members.push(...data.members)
|
||||
}
|
||||
|
||||
// 构建学校路径用于级联选择器回显
|
||||
// 注意:级联选择器的 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) {
|
||||
console.error('获取学生详情失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择用户
|
||||
const handleSelectUser = () => {
|
||||
// 简化处理,实际应该弹出用户选择器
|
||||
ElMessage.info('用户选择功能需要对接会员管理模块')
|
||||
|
||||
formLoading.value = false
|
||||
}
|
||||
|
||||
// 提交
|
||||
|
|
@ -174,22 +222,31 @@ const handleSubmit = async () => {
|
|||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验学校信息
|
||||
if (!form.schoolClassId) {
|
||||
ElMessage.warning('请选择完整的学校/年级/班级信息')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData = {
|
||||
...form,
|
||||
schoolId: form.schoolPath[0],
|
||||
gradeId: form.schoolPath[1],
|
||||
classId: form.schoolPath[2]
|
||||
studentId: form.studentId,
|
||||
studentName: form.studentName,
|
||||
studentNo: form.studentNo,
|
||||
gender: form.gender,
|
||||
birthday: form.birthday,
|
||||
schoolId: form.schoolId,
|
||||
schoolGradeId: form.schoolGradeId,
|
||||
schoolClassId: form.schoolClassId
|
||||
}
|
||||
delete submitData.schoolPath
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateStudent(submitData)
|
||||
await request.put('/business/student', submitData)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await addStudent(submitData)
|
||||
await request.post('/business/student', submitData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
visible.value = false
|
||||
|
|
@ -200,4 +257,6 @@ const handleSubmit = async () => {
|
|||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="schoolTree"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
node-key="id"
|
||||
highlight-current
|
||||
:filter-node-method="filterNode"
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<el-card shadow="never" class="search-wrapper">
|
||||
<el-form :model="queryParams" :inline="true">
|
||||
<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 label="学号">
|
||||
<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-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">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === '1' ? '男' : row.gender === '2' ? '女' : '未知' }}
|
||||
</template>
|
||||
</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="gradeName" label="年级" width="80" />
|
||||
<el-table-column prop="className" label="班级" width="80" />
|
||||
<el-table-column prop="subject" label="学科" width="80" />
|
||||
<el-table-column prop="userNickname" label="归属用户" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||
<el-table-column label="归属用户" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
||||
|
|
@ -129,12 +143,12 @@ const total = ref(0)
|
|||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '',
|
||||
schoolId: '',
|
||||
gradeId: '',
|
||||
classId: ''
|
||||
schoolGradeId: '',
|
||||
schoolClassId: ''
|
||||
})
|
||||
|
||||
// 弹窗引用
|
||||
|
|
@ -149,12 +163,12 @@ watch(treeFilterText, (val) => {
|
|||
// 树节点过滤
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true
|
||||
return data.label.includes(value)
|
||||
return data.name.includes(value)
|
||||
}
|
||||
|
||||
// 获取学校树
|
||||
const getSchoolTree = async () => {
|
||||
const res = await request.get('/api/student/schoolTree')
|
||||
const res = await request.get('/business/student/schoolTree')
|
||||
if (res.code === 200) {
|
||||
schoolTree.value = res.data
|
||||
}
|
||||
|
|
@ -164,7 +178,7 @@ const getSchoolTree = async () => {
|
|||
const getList = async () => {
|
||||
loading.value = true
|
||||
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) {
|
||||
tableData.value = res.rows
|
||||
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) => {
|
||||
// 根据节点层级设置筛选条件
|
||||
// 根据节点层级设置筛选条件,提取真正的数字 ID
|
||||
if (data.type === 'school') {
|
||||
queryParams.value.schoolId = data.id
|
||||
queryParams.value.gradeId = ''
|
||||
queryParams.value.classId = ''
|
||||
queryParams.value.schoolId = extractId(data.id)
|
||||
queryParams.value.schoolGradeId = ''
|
||||
queryParams.value.schoolClassId = ''
|
||||
} else if (data.type === 'grade') {
|
||||
queryParams.value.gradeId = data.id
|
||||
queryParams.value.classId = ''
|
||||
queryParams.value.schoolId = data.schoolId // 年级节点有 schoolId 字段
|
||||
queryParams.value.schoolGradeId = extractId(data.id)
|
||||
queryParams.value.schoolClassId = ''
|
||||
} 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
|
||||
getList()
|
||||
|
|
@ -202,12 +226,12 @@ const resetQuery = () => {
|
|||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
gender: '',
|
||||
schoolId: '',
|
||||
gradeId: '',
|
||||
classId: ''
|
||||
schoolGradeId: '',
|
||||
schoolClassId: ''
|
||||
}
|
||||
treeRef.value?.setCurrentKey(null)
|
||||
getList()
|
||||
|
|
@ -230,12 +254,12 @@ const handleImport = () => {
|
|||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确定要删除学生"${row.name}"吗?`, '提示', {
|
||||
ElMessageBox.confirm(`确定要删除学生"${row.studentName}"吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await request.delete(`/api/student/${row.id}`)
|
||||
const res = await request.delete(`/business/student/${row.studentId}`)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<el-form-item prop="packageName">
|
||||
<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-tooltip>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ function handleGenTable(row) {
|
|||
proxy.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
|
||||
})
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineConfig(({ mode, command }) => {
|
|||
return {
|
||||
// 部署生产环境和开发环境下的URL。
|
||||
// 默认情况下,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' ? '/' : '/',
|
||||
plugins: createVitePlugins(env, command === 'build'),
|
||||
resolve: {
|
||||
|
|
@ -42,7 +42,7 @@ export default defineConfig(({ mode, command }) => {
|
|||
},
|
||||
// vite 相关配置
|
||||
server: {
|
||||
port: 80,
|
||||
port: 3000,
|
||||
host: true,
|
||||
open: true,
|
||||
proxy: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function createCompression(env) {
|
|||
if (VITE_BUILD_COMPRESS) {
|
||||
const compressList = VITE_BUILD_COMPRESS.split(',')
|
||||
if (compressList.includes('gzip')) {
|
||||
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
|
||||
// 使用gzip解压缩静态文件
|
||||
plugin.push(
|
||||
compression({
|
||||
ext: '.gz',
|
||||
|
|
|
|||
Loading…
Reference in New Issue