diff --git a/backend/LICENSE b/backend/LICENSE index 32b3071..f71d84b 100644 --- a/backend/LICENSE +++ b/backend/LICENSE @@ -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 diff --git a/backend/pangu-modules/pangu-business/sql/DEPLOYMENT_GUIDE.md b/backend/pangu-modules/pangu-business/sql/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..64d2cd1 --- /dev/null +++ b/backend/pangu-modules/pangu-business/sql/DEPLOYMENT_GUIDE.md @@ -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 -u -p + +# 执行迁移脚本 +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 -u -p +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` | 本文档 | diff --git a/backend/pangu-modules/pangu-business/sql/README_导入说明.md b/backend/pangu-modules/pangu-business/sql/README_导入说明.md index d1418cb..f338fa0 100644 --- a/backend/pangu-modules/pangu-business/sql/README_导入说明.md +++ b/backend/pangu-modules/pangu-business/sql/README_导入说明.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) --- diff --git a/backend/pangu-modules/pangu-business/sql/fix_student_dept_id.sql b/backend/pangu-modules/pangu-business/sql/fix_student_dept_id.sql new file mode 100644 index 0000000..458d742 --- /dev/null +++ b/backend/pangu-modules/pangu-business/sql/fix_student_dept_id.sql @@ -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); diff --git a/backend/pangu-modules/pangu-business/sql/pangu_tables.sql b/backend/pangu-modules/pangu-business/sql/pangu_tables.sql index 0b8f678..cd981b4 100644 --- a/backend/pangu-modules/pangu-business/sql/pangu_tables.sql +++ b/backend/pangu-modules/pangu-business/sql/pangu_tables.sql @@ -1,5 +1,5 @@ -- ============================================================ --- 盘古用户平台 - 业务模块建表脚本(适配 RuoYi-Vue-Plus 5.x) +-- 盘古用户平台 - 业务模块建表脚本 -- 作者:pangu -- 创建时间:2026-02-02 -- ============================================================ diff --git a/backend/pangu-modules/pangu-business/sql/production_migrate_school_class_id.sql b/backend/pangu-modules/pangu-business/sql/production_migrate_school_class_id.sql new file mode 100644 index 0000000..b4e76ee --- /dev/null +++ b/backend/pangu-modules/pangu-business/sql/production_migrate_school_class_id.sql @@ -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; diff --git a/backend/pangu-modules/pangu-business/sql/production_rollback_school_class_id.sql b/backend/pangu-modules/pangu-business/sql/production_rollback_school_class_id.sql new file mode 100644 index 0000000..b0c96f1 --- /dev/null +++ b/backend/pangu-modules/pangu-business/sql/production_rollback_school_class_id.sql @@ -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; diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/common/RegionPermissionHelper.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/common/RegionPermissionHelper.java new file mode 100644 index 0000000..4403dae --- /dev/null +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/common/RegionPermissionHelper.java @@ -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; + +/** + * 区域数据权限工具类 + *

+ * 用于获取当前登录用户可访问的区域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 getAccessibleRegionIds() { + // 超级管理员不限制 + if (LoginHelper.isSuperAdmin()) { + log.debug("超级管理员,不限制区域"); + return null; + } + + Long deptId = LoginHelper.getDeptId(); + if (deptId == null) { + log.warn("用户部门ID为空,无法获取区域权限"); + return Collections.emptyList(); + } + + // 获取用户部门及下级部门ID列表 + List deptIds = deptMapper.selectDeptAndChildById(deptId); + if (deptIds.isEmpty()) { + log.warn("未找到部门及下级部门: deptId={}", deptId); + return Collections.emptyList(); + } + + // 查询这些部门关联的学校的区域ID + List 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(); + } +} diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java index 32c8047..b02fb34 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java @@ -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 selectPageList(PgMember member, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(member); + + // 添加区域数据权限过滤 + List 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 page = baseMapper.selectPage(pageQuery.build(), lqw); return TableDataInfo.build(page); } @Override public List selectList(PgMember member) { - return baseMapper.selectList(buildQueryWrapper(member)); + LambdaQueryWrapper lqw = buildQueryWrapper(member); + + // 添加区域数据权限过滤 + List regionIds = regionPermissionHelper.getAccessibleRegionIds(); + if (regionIds != null) { + if (regionIds.isEmpty()) { + return Collections.emptyList(); + } + lqw.in(PgMember::getRegionId, regionIds); + } + + return baseMapper.selectList(lqw); } @Override diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java index 83a3613..8893527 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java @@ -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; diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/vo/SchoolTreeNode.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/vo/SchoolTreeNode.java index 74e4952..a6533f7 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/vo/SchoolTreeNode.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/vo/SchoolTreeNode.java @@ -47,6 +47,11 @@ public class SchoolTreeNode { */ private Long regionId; + /** + * 部门ID(仅学校节点有值) + */ + private Long deptId; + /** * 状态(0正常 1停用) */ diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/mapper/PgSchoolMapper.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/mapper/PgSchoolMapper.java index 16fda13..24e272c 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/mapper/PgSchoolMapper.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/mapper/PgSchoolMapper.java @@ -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 { default Page selectPageSchoolList(Page page, Wrapper queryWrapper) { return this.selectPage(page, queryWrapper); } + + /** + * 根据部门ID列表查询关联学校的区域ID + *

+ * 用于区域数据权限过滤:获取用户所管理学校的区域ID列表 + * + * @param deptIds 部门ID列表 + * @return 区域ID列表(去重) + */ + default List selectRegionIdsByDeptIds(@Param("deptIds") List deptIds) { + if (deptIds == null || deptIds.isEmpty()) { + return Collections.emptyList(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(PgSchool::getRegionId) + .in(PgSchool::getDeptId, deptIds) + .isNotNull(PgSchool::getRegionId) + .groupBy(PgSchool::getRegionId); + return this.selectObjs(wrapper); + } } diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java index 20e2e15..11cf869 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java @@ -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()); diff --git a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java index 143ab11..5d582a7 100644 --- a/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java +++ b/backend/pangu-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java @@ -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("未设置班级"); + } } // 填充会员信息(多对多) diff --git a/docs/01-需求文档/需求规格说明书_v1.0.md b/docs/01-需求文档/需求规格说明书_v1.0.md index 8ef9517..c9f2944 100644 --- a/docs/01-需求文档/需求规格说明书_v1.0.md +++ b/docs/01-需求文档/需求规格说明书_v1.0.md @@ -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 | 补充验收实现状态与系统监控(服务监控)说明 | diff --git a/frontend/LICENSE b/frontend/LICENSE index 8564f29..89ae929 100644 --- a/frontend/LICENSE +++ b/frontend/LICENSE @@ -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 diff --git a/frontend/package.json b/frontend/package.json index 7f52110..ab73882 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/api/login.js b/frontend/src/api/login.js index c55138d..6ec069d 100644 --- a/frontend/src/api/login.js +++ b/frontend/src/api/login.js @@ -1,6 +1,6 @@ import request from '@/utils/request' -// 登录方法 (RuoYi-Vue-Plus) +// 登录方法 export function login(username, password, code, uuid) { const data = { username, diff --git a/frontend/src/api/menu.js b/frontend/src/api/menu.js index 6637c42..845efd7 100644 --- a/frontend/src/api/menu.js +++ b/frontend/src/api/menu.js @@ -1,6 +1,6 @@ import request from '@/utils/request' -// 获取路由 (RuoYi-Vue-Plus) +// 获取路由 export const getRouters = () => { return request({ url: '/system/menu/getRouters', diff --git a/frontend/src/assets/styles/ruoyi.scss b/frontend/src/assets/styles/ruoyi.scss index 6d96a35..3064a48 100644 --- a/frontend/src/assets/styles/ruoyi.scss +++ b/frontend/src/assets/styles/ruoyi.scss @@ -1,6 +1,6 @@ /** * 通用css样式布局处理 - * Copyright (c) 2019 ruoyi + * Copyright (c) 2019 pangu */ /** 基础通用 **/ diff --git a/frontend/src/directive/common/copyText.js b/frontend/src/directive/common/copyText.js index b5f844d..ca510b1 100644 --- a/frontend/src/directive/common/copyText.js +++ b/frontend/src/directive/common/copyText.js @@ -1,6 +1,6 @@ /** * v-copyText 复制文本内容 -* Copyright (c) 2022 ruoyi +* Copyright (c) 2022 pangu */ export default { beforeMount(el, { value, arg }) { diff --git a/frontend/src/directive/permission/hasPermi.js b/frontend/src/directive/permission/hasPermi.js index 87ed5a6..476d8ce 100644 --- a/frontend/src/directive/permission/hasPermi.js +++ b/frontend/src/directive/permission/hasPermi.js @@ -1,6 +1,6 @@ /** * v-hasPermi 操作权限处理 - * Copyright (c) 2019 ruoyi + * Copyright (c) 2019 pangu */ import useUserStore from '@/store/modules/user' diff --git a/frontend/src/directive/permission/hasRole.js b/frontend/src/directive/permission/hasRole.js index 8d57159..8481e46 100644 --- a/frontend/src/directive/permission/hasRole.js +++ b/frontend/src/directive/permission/hasRole.js @@ -1,6 +1,6 @@ /** * v-hasRole 角色权限处理 - * Copyright (c) 2019 ruoyi + * Copyright (c) 2019 pangu */ import useUserStore from '@/store/modules/user' diff --git a/frontend/src/store/modules/user.js b/frontend/src/store/modules/user.js index 304f695..5d8a780 100644 --- a/frontend/src/store/modules/user.js +++ b/frontend/src/store/modules/user.js @@ -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 || "" diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js index 11a056e..b0bf874 100644 --- a/frontend/src/utils/request.js +++ b/frontend/src/utils/request.js @@ -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) { diff --git a/frontend/src/utils/ruoyi.js b/frontend/src/utils/ruoyi.js index a7bfbdf..a776b48 100644 --- a/frontend/src/utils/ruoyi.js +++ b/frontend/src/utils/ruoyi.js @@ -1,6 +1,6 @@ /** * 通用js方法封装处理 - * Copyright (c) 2019 ruoyi + * Copyright (c) 2019 pangu */ // 日期格式化 diff --git a/frontend/src/views/business/school/index.vue b/frontend/src/views/business/school/index.vue index 7623e4c..59f16f5 100644 --- a/frontend/src/views/business/school/index.vue +++ b/frontend/src/views/business/school/index.vue @@ -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, '提示', { diff --git a/frontend/src/views/login.vue b/frontend/src/views/login.vue index 0ed6317..d9aa041 100644 --- a/frontend/src/views/login.vue +++ b/frontend/src/views/login.vue @@ -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) { diff --git a/frontend/src/views/monitor/job/index.vue b/frontend/src/views/monitor/job/index.vue index 89a6911..188549f 100644 --- a/frontend/src/views/monitor/job/index.vue +++ b/frontend/src/views/monitor/job/index.vue @@ -166,8 +166,8 @@ diff --git a/frontend/src/views/school/components/ClassDialog.vue b/frontend/src/views/school/components/ClassDialog.vue index aa53569..701ded3 100644 --- a/frontend/src/views/school/components/ClassDialog.vue +++ b/frontend/src/views/school/components/ClassDialog.vue @@ -94,7 +94,7 @@ const open = (school) => { dialogVisible.value = true currentSchool.value = school form.value = { - schoolId: school.id, + schoolId: school.schoolId, gradeId: '', classIds: [] } diff --git a/frontend/src/views/school/components/GradeDialog.vue b/frontend/src/views/school/components/GradeDialog.vue index 4a69dd9..cd83bf6 100644 --- a/frontend/src/views/school/components/GradeDialog.vue +++ b/frontend/src/views/school/components/GradeDialog.vue @@ -72,7 +72,7 @@ const open = (school) => { dialogVisible.value = true currentSchool.value = school form.value = { - schoolId: school.id, + schoolId: school.schoolId, gradeIds: [] } } diff --git a/frontend/src/views/school/components/SchoolDialog.vue b/frontend/src/views/school/components/SchoolDialog.vue index bb046c8..45ee5c3 100644 --- a/frontend/src/views/school/components/SchoolDialog.vue +++ b/frontend/src/views/school/components/SchoolDialog.vue @@ -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, diff --git a/frontend/src/views/school/index.vue b/frontend/src/views/school/index.vue index 2c6a727..f616a31 100644 --- a/frontend/src/views/school/index.vue +++ b/frontend/src/views/school/index.vue @@ -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() diff --git a/frontend/src/views/student/components/StudentDialog.vue b/frontend/src/views/student/components/StudentDialog.vue index 14869d4..c8e968e 100644 --- a/frontend/src/views/student/components/StudentDialog.vue +++ b/frontend/src/views/student/components/StudentDialog.vue @@ -5,19 +5,19 @@ width="600px" :close-on-click-modal="false" destroy-on-close - @open="handleOpen" > - - + + - + @@ -29,39 +29,42 @@ - + - - - - - - - - - + + + 暂无归属用户