feat: 完善学校管理模块(根据需求文档)

后端修改:
1. PgRegionController: 新增 /tree 接口用于获取区域树
2. PgRegion: 添加 children 字段支持树形结构
3. PgRegionService: 实现 selectTree 方法构建树形数据
4. PgSchoolController: 新增学校年级/班级管理接口
   - GET /{schoolId}/grades: 获取学校的年级列表
   - POST /grade: 为学校批量添加年级
   - POST /class: 为年级批量添加班级
5. PgSchoolService: 实现年级班级挂载逻辑
6. PgSchoolGrade: 添加 gradeName 字段用于显示

前端修改:
1. school.js API: 修正路径 /api → /business
2. index.vue: 修正删除接口的 ID 字段(id → schoolId)
3. SchoolDialog.vue: 修正表单字段名
4. GradeDialog.vue: 修正 API 调用和数据格式
5. ClassDialog.vue: 重写支持选择学校已挂载的年级

@author pangu
This commit is contained in:
神码-方晓辉 2026-02-02 16:32:33 +08:00
parent 82a0cb7f07
commit bd14bb36c4
13 changed files with 230 additions and 38 deletions

View File

@ -32,6 +32,14 @@ public class PgRegionController extends BaseController {
return R.ok(regionService.selectList(region));
}
/**
* 获取区域树用于学校管理左侧筛选
*/
@GetMapping("/tree")
public R<List<PgRegion>> tree() {
return R.ok(regionService.selectTree());
}
@GetMapping("/children/{parentId}")
public R<List<PgRegion>> children(@PathVariable Long parentId) {
return R.ok(regionService.selectByParentId(parentId));

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.base.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
@ -8,6 +9,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.util.List;
/**
* 区域表
*
@ -39,4 +42,10 @@ public class PgRegion extends BaseEntity {
@TableLogic
private String delFlag;
/**
* 子区域列表非数据库字段
*/
@TableField(exist = false)
private List<PgRegion> children;
}

View File

@ -11,6 +11,7 @@ import java.util.List;
*/
public interface IPgRegionService {
List<PgRegion> selectList(PgRegion region);
List<PgRegion> selectTree();
PgRegion selectById(Long regionId);
List<PgRegion> selectByParentId(Long parentId);
int insert(PgRegion region);

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.base.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.pangu.base.domain.PgRegion;
@ -8,8 +9,10 @@ import org.dromara.pangu.base.mapper.PgRegionMapper;
import org.dromara.pangu.base.service.IPgRegionService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 区域 Service 实现
@ -32,6 +35,28 @@ public class PgRegionServiceImpl implements IPgRegionService {
return baseMapper.selectList(lqw);
}
@Override
public List<PgRegion> selectTree() {
// 查询所有启用的区域
List<PgRegion> allRegions = baseMapper.selectList(
new LambdaQueryWrapper<PgRegion>()
.eq(PgRegion::getStatus, "0")
.orderByAsc(PgRegion::getOrderNum)
);
// 构建树形结构
return buildTree(allRegions, 0L);
}
/**
* 构建区域树
*/
private List<PgRegion> buildTree(List<PgRegion> regions, Long parentId) {
return regions.stream()
.filter(r -> parentId.equals(r.getParentId()))
.peek(r -> r.setChildren(buildTree(regions, r.getRegionId())))
.collect(Collectors.toList());
}
@Override
public PgRegion selectById(Long regionId) {
return baseMapper.selectById(regionId);

View File

@ -9,11 +9,14 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import org.dromara.pangu.school.service.IPgSchoolService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 学校管理
@ -65,4 +68,39 @@ public class PgSchoolController extends BaseController {
public R<Void> remove(@PathVariable Long[] schoolIds) {
return toAjax(schoolService.deleteByIds(schoolIds));
}
/**
* 获取学校的年级列表
*/
@GetMapping("/{schoolId}/grades")
public R<List<PgSchoolGrade>> getSchoolGrades(@PathVariable Long schoolId) {
return R.ok(schoolService.selectGradesBySchoolId(schoolId));
}
/**
* 为学校添加年级批量挂载
*/
@SaCheckPermission("business:school:edit")
@Log(title = "学校年级管理", businessType = BusinessType.INSERT)
@PostMapping("/grade")
public R<Void> addSchoolGrade(@RequestBody Map<String, Object> params) {
Long schoolId = Long.valueOf(params.get("schoolId").toString());
@SuppressWarnings("unchecked")
List<Long> gradeIds = (List<Long>) params.get("gradeIds");
return toAjax(schoolService.addSchoolGrades(schoolId, gradeIds));
}
/**
* 为年级添加班级批量挂载
*/
@SaCheckPermission("business:school:edit")
@Log(title = "学校班级管理", businessType = BusinessType.INSERT)
@PostMapping("/class")
public R<Void> addGradeClass(@RequestBody Map<String, Object> params) {
Long schoolId = Long.valueOf(params.get("schoolId").toString());
Long schoolGradeId = Long.valueOf(params.get("schoolGradeId").toString());
@SuppressWarnings("unchecked")
List<Long> classIds = (List<Long>) params.get("classIds");
return toAjax(schoolService.addGradeClasses(schoolId, schoolGradeId, classIds));
}
}

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.school.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -29,4 +30,10 @@ public class PgSchoolGrade implements Serializable {
private Long createBy;
private Date createTime;
/**
* 年级名称非数据库字段用于前端显示
*/
@TableField(exist = false)
private String gradeName;
}

View File

@ -3,6 +3,7 @@ package org.dromara.pangu.school.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import java.util.List;
@ -18,4 +19,19 @@ public interface IPgSchoolService {
int insert(PgSchool school);
int update(PgSchool school);
int deleteByIds(Long[] schoolIds);
/**
* 获取学校的年级列表
*/
List<PgSchoolGrade> selectGradesBySchoolId(Long schoolId);
/**
* 为学校添加年级批量挂载
*/
int addSchoolGrades(Long schoolId, List<Long> gradeIds);
/**
* 为年级添加班级批量挂载
*/
int addGradeClasses(Long schoolId, Long schoolGradeId, List<Long> classIds);
}

View File

@ -6,10 +6,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.base.domain.PgGrade;
import org.dromara.pangu.base.mapper.PgGradeMapper;
import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import org.dromara.pangu.school.mapper.PgSchoolClassMapper;
import org.dromara.pangu.school.mapper.PgSchoolGradeMapper;
import org.dromara.pangu.school.mapper.PgSchoolMapper;
import org.dromara.pangu.school.service.IPgSchoolService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
@ -24,6 +31,9 @@ import java.util.List;
public class PgSchoolServiceImpl implements IPgSchoolService {
private final PgSchoolMapper baseMapper;
private final PgSchoolGradeMapper schoolGradeMapper;
private final PgSchoolClassMapper schoolClassMapper;
private final PgGradeMapper gradeMapper;
@Override
public TableDataInfo<PgSchool> selectPageList(PgSchool school, PageQuery pageQuery) {
@ -57,6 +67,66 @@ public class PgSchoolServiceImpl implements IPgSchoolService {
return baseMapper.deleteByIds(Arrays.asList(schoolIds));
}
@Override
public List<PgSchoolGrade> selectGradesBySchoolId(Long schoolId) {
List<PgSchoolGrade> schoolGrades = schoolGradeMapper.selectList(
new LambdaQueryWrapper<PgSchoolGrade>()
.eq(PgSchoolGrade::getSchoolId, schoolId)
);
// 关联查询年级名称
for (PgSchoolGrade sg : schoolGrades) {
PgGrade grade = gradeMapper.selectById(sg.getGradeId());
if (grade != null) {
sg.setGradeName(grade.getGradeName());
}
}
return schoolGrades;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int addSchoolGrades(Long schoolId, List<Long> gradeIds) {
int count = 0;
for (Long gradeId : gradeIds) {
// 检查是否已存在
Long exists = schoolGradeMapper.selectCount(
new LambdaQueryWrapper<PgSchoolGrade>()
.eq(PgSchoolGrade::getSchoolId, schoolId)
.eq(PgSchoolGrade::getGradeId, gradeId)
);
if (exists == 0) {
PgSchoolGrade schoolGrade = new PgSchoolGrade();
schoolGrade.setSchoolId(schoolId);
schoolGrade.setGradeId(gradeId);
count += schoolGradeMapper.insert(schoolGrade);
}
}
return count;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int addGradeClasses(Long schoolId, Long schoolGradeId, List<Long> classIds) {
int count = 0;
for (Long classId : classIds) {
// 检查是否已存在
Long exists = schoolClassMapper.selectCount(
new LambdaQueryWrapper<PgSchoolClass>()
.eq(PgSchoolClass::getSchoolId, schoolId)
.eq(PgSchoolClass::getSchoolGradeId, schoolGradeId)
.eq(PgSchoolClass::getClassId, classId)
);
if (exists == 0) {
PgSchoolClass schoolClass = new PgSchoolClass();
schoolClass.setSchoolId(schoolId);
schoolClass.setSchoolGradeId(schoolGradeId);
schoolClass.setClassId(classId);
count += schoolClassMapper.insert(schoolClass);
}
}
return count;
}
private LambdaQueryWrapper<PgSchool> buildQueryWrapper(PgSchool school) {
LambdaQueryWrapper<PgSchool> lqw = new LambdaQueryWrapper<>();
lqw.like(StrUtil.isNotBlank(school.getSchoolName()), PgSchool::getSchoolName, school.getSchoolName());

View File

@ -7,7 +7,7 @@ import request from '@/utils/request'
// 获取区域树
export function getRegionTree() {
return request({
url: '/api/region/tree',
url: '/business/region/tree',
method: 'get'
})
}
@ -15,7 +15,7 @@ export function getRegionTree() {
// 获取学校列表
export function getSchoolList(params) {
return request({
url: '/api/school/list',
url: '/business/school/list',
method: 'get',
params
})
@ -24,7 +24,7 @@ export function getSchoolList(params) {
// 获取学校详情
export function getSchoolDetail(id) {
return request({
url: `/api/school/${id}`,
url: `/business/school/${id}`,
method: 'get'
})
}
@ -32,7 +32,7 @@ export function getSchoolDetail(id) {
// 新增学校
export function addSchool(data) {
return request({
url: '/api/school',
url: '/business/school',
method: 'post',
data
})
@ -41,7 +41,7 @@ export function addSchool(data) {
// 修改学校
export function updateSchool(data) {
return request({
url: '/api/school',
url: '/business/school',
method: 'put',
data
})
@ -50,15 +50,15 @@ export function updateSchool(data) {
// 删除学校
export function deleteSchool(id) {
return request({
url: `/api/school/${id}`,
url: `/business/school/${id}`,
method: 'delete'
})
}
// 获取年级列表(字典数据
// 获取年级选项(用于学校挂载年级
export function getGradeOptions() {
return request({
url: '/api/grade/options',
url: '/business/grade/listAll',
method: 'get'
})
}
@ -66,16 +66,16 @@ export function getGradeOptions() {
// 为学校添加年级
export function addSchoolGrade(data) {
return request({
url: '/api/school/grade',
url: '/business/school/grade',
method: 'post',
data
})
}
// 获取班级列表(字典数据
// 获取班级选项(用于年级挂载班级
export function getClassOptions() {
return request({
url: '/api/class/options',
url: '/business/class/listAll',
method: 'get'
})
}
@ -83,8 +83,16 @@ export function getClassOptions() {
// 为年级添加班级
export function addGradeClass(data) {
return request({
url: '/api/school/class',
url: '/business/school/class',
method: 'post',
data
})
}
// 获取学校年级树(用于新增班级时选择年级)
export function getSchoolGradeTree(schoolId) {
return request({
url: `/business/school/${schoolId}/grades`,
method: 'get'
})
}

View File

@ -12,13 +12,13 @@
</div>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="选择年级" prop="gradeId">
<el-select v-model="form.gradeId" placeholder="请先选择年级" style="width: 100%">
<el-form-item label="选择年级" prop="schoolGradeId">
<el-select v-model="form.schoolGradeId" placeholder="请先选择年级" style="width: 100%">
<el-option
v-for="item in gradeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in schoolGradeOptions"
:key="item.id"
:label="item.gradeName"
:value="item.id"
/>
</el-select>
</el-form-item>
@ -50,7 +50,8 @@
*/
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { addGradeClass, getClassOptions, getGradeOptions } from '@/api/pangu/school'
import request from '@/utils/request'
import { addGradeClass, getClassOptions, getSchoolGradeTree } from '@/api/pangu/school'
const emit = defineEmits(['success'])
@ -58,25 +59,25 @@ const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const currentSchool = ref(null)
const gradeOptions = ref([])
const schoolGradeOptions = ref([])
const classOptions = ref([])
//
const form = ref({
schoolId: null,
gradeId: '',
schoolGradeId: null,
classIds: []
})
//
const fetchGradeOptions = async () => {
//
const fetchSchoolGrades = async (schoolId) => {
try {
const res = await getGradeOptions()
const res = await getSchoolGradeTree(schoolId)
if (res.code === 200) {
gradeOptions.value = res.data
schoolGradeOptions.value = res.data || []
}
} catch (error) {
console.error('获取年级选项失败:', error)
console.error('获取学校年级失败:', error)
}
}
@ -85,7 +86,11 @@ const fetchClassOptions = async () => {
try {
const res = await getClassOptions()
if (res.code === 200) {
classOptions.value = res.data
//
classOptions.value = (res.data || []).map(item => ({
value: item.classId,
label: item.className
}))
}
} catch (error) {
console.error('获取班级选项失败:', error)
@ -97,15 +102,17 @@ const open = (school) => {
dialogVisible.value = true
currentSchool.value = school
form.value = {
schoolId: school.id,
gradeId: '',
schoolId: school.schoolId,
schoolGradeId: null,
classIds: []
}
//
fetchSchoolGrades(school.schoolId)
}
//
const handleSubmit = async () => {
if (!form.value.gradeId) {
if (!form.value.schoolGradeId) {
ElMessage.warning('请选择年级')
return
}
@ -131,7 +138,6 @@ const handleSubmit = async () => {
//
onMounted(() => {
fetchGradeOptions()
fetchClassOptions()
})

View File

@ -60,7 +60,11 @@ const fetchGradeOptions = async () => {
try {
const res = await getGradeOptions()
if (res.code === 200) {
gradeOptions.value = res.data
//
gradeOptions.value = (res.data || []).map(item => ({
value: item.gradeId,
label: item.gradeName
}))
}
} catch (error) {
console.error('获取年级选项失败:', error)
@ -72,7 +76,7 @@ const open = (school) => {
dialogVisible.value = true
currentSchool.value = school
form.value = {
schoolId: school.id,
schoolId: school.schoolId,
gradeIds: []
}
}

View File

@ -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,
@ -203,7 +203,7 @@ const handleSubmit = async () => {
regionIds: undefined //
}
const res = isEdit.value
const res = form.value.schoolId
? await updateSchool(submitData)
: await addSchool(submitData)

View File

@ -216,7 +216,7 @@ const handleDelete = (row) => {
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/business/school/${row.id}`)
const res = await request.delete(`/business/school/${row.schoolId}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()