pangu-user-platform/docs/05-技术方案/基础数据模块_后端开发文档.md

42 KiB
Raw Permalink Blame History

盘古用户平台 - 基础数据模块后端开发文档


文档信息 内容
文档版本 V1.0
项目名称 盘古用户平台Pangu User Platform
模块名称 基础数据管理-后端开发
编写团队 pangu
创建日期 2026-01-31

1. 开发环境要求

环境 版本要求 说明
JDK 17+ Java运行环境
Maven 3.8+ 项目构建工具
Spring Boot 3.3.x 应用框架
MyBatis Plus 3.5.x ORM框架
MySQL 8.0+ 数据库
Redis 7.x 缓存

2. 目录结构

pangu-admin/src/main/java/com/pangu/
├── base/                           # 基础数据模块
│   ├── controller/                 # 控制器层
│   │   ├── GradeController.java    # 年级管理
│   │   ├── PgClassController.java  # 班级管理
│   │   ├── SubjectController.java  # 学科管理
│   │   └── RegionController.java   # 区域管理
│   ├── service/                    # 服务层
│   │   ├── IGradeService.java
│   │   ├── IPgClassService.java
│   │   ├── ISubjectService.java
│   │   ├── IRegionService.java
│   │   └── impl/
│   │       ├── GradeServiceImpl.java
│   │       ├── PgClassServiceImpl.java
│   │       ├── SubjectServiceImpl.java
│   │       └── RegionServiceImpl.java
│   ├── mapper/                     # 数据访问层
│   │   ├── GradeMapper.java
│   │   ├── PgClassMapper.java
│   │   ├── SubjectMapper.java
│   │   └── RegionMapper.java
│   └── domain/                     # 实体类
│       ├── Grade.java
│       ├── PgClass.java
│       ├── Subject.java
│       └── Region.java
│
└── resources/mapper/base/          # MyBatis XML
    ├── GradeMapper.xml
    ├── PgClassMapper.xml
    ├── SubjectMapper.xml
    └── RegionMapper.xml

3. 实体类设计

3.1 年级实体Grade.java

package com.pangu.base.domain;

import com.baomidou.mybatisplus.annotation.*;
import com.pangu.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 年级实体类
 * @author pangu
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_grade")
public class Grade extends BaseEntity {
    
    private static final long serialVersionUID = 1L;
    
    /** 年级ID */
    @TableId(type = IdType.AUTO)
    private Long gradeId;
    
    /** 年级编码 */
    private String gradeCode;
    
    /** 年级名称 */
    @NotBlank(message = "年级名称不能为空")
    @Size(max = 50, message = "年级名称长度不能超过50个字符")
    private String gradeName;
    
    /** 显示顺序 */
    private Integer orderNum;
    
    /** 状态0正常 1停用*/
    private String status;
    
    /** 删除标志0存在 1删除*/
    @TableLogic
    private String delFlag;
}

3.2 班级实体PgClass.java

package com.pangu.base.domain;

import com.baomidou.mybatisplus.annotation.*;
import com.pangu.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 班级实体类
 * 注意类名使用PgClass避免与java.lang.Class冲突
 * @author pangu
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_class")
public class PgClass extends BaseEntity {
    
    private static final long serialVersionUID = 1L;
    
    /** 班级ID */
    @TableId(type = IdType.AUTO)
    private Long classId;
    
    /** 班级编码 */
    private String classCode;
    
    /** 班级名称 */
    @NotBlank(message = "班级名称不能为空")
    @Size(max = 50, message = "班级名称长度不能超过50个字符")
    private String className;
    
    /** 显示顺序 */
    private Integer orderNum;
    
    /** 状态0正常 1停用*/
    private String status;
    
    /** 删除标志0存在 1删除*/
    @TableLogic
    private String delFlag;
}

3.3 学科实体Subject.java

package com.pangu.base.domain;

import com.baomidou.mybatisplus.annotation.*;
import com.pangu.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 学科实体类
 * @author pangu
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_subject")
public class Subject extends BaseEntity {
    
    private static final long serialVersionUID = 1L;
    
    /** 学科ID */
    @TableId(type = IdType.AUTO)
    private Long subjectId;
    
    /** 学科编码 */
    private String subjectCode;
    
    /** 学科名称 */
    @NotBlank(message = "学科名称不能为空")
    @Size(max = 50, message = "学科名称长度不能超过50个字符")
    private String subjectName;
    
    /** 显示顺序 */
    private Integer orderNum;
    
    /** 状态0正常 1停用*/
    private String status;
    
    /** 删除标志0存在 1删除*/
    @TableLogic
    private String delFlag;
}

3.4 区域实体Region.java

package com.pangu.base.domain;

import com.baomidou.mybatisplus.annotation.*;
import com.pangu.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.List;

/**
 * 区域实体类
 * @author pangu
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_region")
public class Region extends BaseEntity {
    
    private static final long serialVersionUID = 1L;
    
    /** 区域ID */
    @TableId(type = IdType.AUTO)
    private Long regionId;
    
    /** 父区域ID */
    private Long parentId;
    
    /** 区域名称 */
    @NotBlank(message = "区域名称不能为空")
    @Size(max = 100, message = "区域名称长度不能超过100个字符")
    private String regionName;
    
    /** 区域编码 */
    private String regionCode;
    
    /** 层级1省 2市 3区*/
    private Integer level;
    
    /** 祖级列表 */
    private String ancestors;
    
    /** 显示顺序 */
    private Integer orderNum;
    
    /** 状态0正常 1停用*/
    private String status;
    
    /** 删除标志0存在 1删除*/
    @TableLogic
    private String delFlag;
    
    /** 子区域(非数据库字段)*/
    @TableField(exist = false)
    private List<Region> children;
}

4. Mapper层设计

4.1 年级Mapper接口

package com.pangu.base.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pangu.base.domain.Grade;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 年级Mapper接口
 * @author pangu
 */
@Mapper
public interface GradeMapper extends BaseMapper<Grade> {
    
    /**
     * 查询年级列表
     * @param grade 查询条件
     * @return 年级列表
     */
    List<Grade> selectGradeList(Grade grade);
    
    /**
     * 根据ID查询年级
     * @param gradeId 年级ID
     * @return 年级信息
     */
    Grade selectGradeById(Long gradeId);
    
    /**
     * 新增年级
     * @param grade 年级信息
     * @return 影响行数
     */
    int insertGrade(Grade grade);
    
    /**
     * 修改年级
     * @param grade 年级信息
     * @return 影响行数
     */
    int updateGrade(Grade grade);
    
    /**
     * 删除年级(软删除)
     * @param gradeId 年级ID
     * @return 影响行数
     */
    int deleteGradeById(Long gradeId);
    
    /**
     * 校验年级名称唯一
     * @param gradeName 年级名称
     * @return 年级信息
     */
    Grade checkGradeNameUnique(@Param("gradeName") String gradeName);
    
    /**
     * 查询最大编码
     * @return 最大编码
     */
    String selectMaxGradeCode();
}

4.2 年级Mapper XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pangu.base.mapper.GradeMapper">

    <resultMap id="GradeResult" type="Grade">
        <id property="gradeId" column="grade_id"/>
        <result property="gradeCode" column="grade_code"/>
        <result property="gradeName" column="grade_name"/>
        <result property="orderNum" column="order_num"/>
        <result property="status" column="status"/>
        <result property="delFlag" column="del_flag"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
        <result property="remark" column="remark"/>
    </resultMap>

    <sql id="selectGradeVo">
        select grade_id, grade_code, grade_name, order_num, status, del_flag,
               create_by, create_time, update_by, update_time, remark
        from pg_grade
        where del_flag = '0'
    </sql>

    <!-- 查询年级列表 -->
    <select id="selectGradeList" parameterType="Grade" resultMap="GradeResult">
        <include refid="selectGradeVo"/>
        <if test="gradeName != null and gradeName != ''">
            AND grade_name like concat('%', #{gradeName}, '%')
        </if>
        <if test="gradeCode != null and gradeCode != ''">
            AND grade_code = #{gradeCode}
        </if>
        <if test="status != null and status != ''">
            AND status = #{status}
        </if>
        order by order_num
    </select>

    <!-- 根据ID查询年级 -->
    <select id="selectGradeById" parameterType="Long" resultMap="GradeResult">
        <include refid="selectGradeVo"/>
        AND grade_id = #{gradeId}
    </select>

    <!-- 新增年级 -->
    <insert id="insertGrade" parameterType="Grade" useGeneratedKeys="true" keyProperty="gradeId">
        insert into pg_grade (
            grade_code,
            grade_name,
            order_num,
            status,
            del_flag,
            create_by,
            create_time,
            remark
        ) values (
            #{gradeCode},
            #{gradeName},
            #{orderNum},
            #{status},
            '0',
            #{createBy},
            #{createTime},
            #{remark}
        )
    </insert>

    <!-- 修改年级 -->
    <update id="updateGrade" parameterType="Grade">
        update pg_grade
        <set>
            <if test="gradeName != null and gradeName != ''">grade_name = #{gradeName},</if>
            <if test="orderNum != null">order_num = #{orderNum},</if>
            <if test="status != null">status = #{status},</if>
            <if test="remark != null">remark = #{remark},</if>
            update_by = #{updateBy},
            update_time = #{updateTime}
        </set>
        where grade_id = #{gradeId}
    </update>

    <!-- 删除年级(软删除)-->
    <update id="deleteGradeById" parameterType="Long">
        update pg_grade set del_flag = '1' where grade_id = #{gradeId}
    </update>

    <!-- 校验年级名称唯一 -->
    <select id="checkGradeNameUnique" parameterType="String" resultMap="GradeResult">
        <include refid="selectGradeVo"/>
        AND grade_name = #{gradeName}
        limit 1
    </select>

    <!-- 查询最大编码 -->
    <select id="selectMaxGradeCode" resultType="String">
        select max(grade_code) from pg_grade where del_flag = '0'
    </select>

</mapper>

4.3 区域Mapper接口

package com.pangu.base.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pangu.base.domain.Region;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 区域Mapper接口
 * @author pangu
 */
@Mapper
public interface RegionMapper extends BaseMapper<Region> {
    
    /**
     * 查询区域列表
     * @param region 查询条件
     * @return 区域列表
     */
    List<Region> selectRegionList(Region region);
    
    /**
     * 根据ID查询区域
     * @param regionId 区域ID
     * @return 区域信息
     */
    Region selectRegionById(Long regionId);
    
    /**
     * 新增区域
     * @param region 区域信息
     * @return 影响行数
     */
    int insertRegion(Region region);
    
    /**
     * 修改区域
     * @param region 区域信息
     * @return 影响行数
     */
    int updateRegion(Region region);
    
    /**
     * 删除区域(软删除)
     * @param regionId 区域ID
     * @return 影响行数
     */
    int deleteRegionById(Long regionId);
    
    /**
     * 查询子区域数量
     * @param parentId 父区域ID
     * @return 子区域数量
     */
    int countChildByParentId(@Param("parentId") Long parentId);
    
    /**
     * 查询指定层级的最大编码
     * @param level 层级
     * @return 最大编码
     */
    String selectMaxCodeByLevel(@Param("level") Integer level);
    
    /**
     * 查询指定父级下的最大编码
     * @param parentId 父区域ID
     * @return 最大编码
     */
    String selectMaxCodeByParent(@Param("parentId") Long parentId);
}

4.4 区域Mapper XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pangu.base.mapper.RegionMapper">

    <resultMap id="RegionResult" type="Region">
        <id property="regionId" column="region_id"/>
        <result property="parentId" column="parent_id"/>
        <result property="regionName" column="region_name"/>
        <result property="regionCode" column="region_code"/>
        <result property="level" column="level"/>
        <result property="ancestors" column="ancestors"/>
        <result property="orderNum" column="order_num"/>
        <result property="status" column="status"/>
        <result property="delFlag" column="del_flag"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
    </resultMap>

    <sql id="selectRegionVo">
        select region_id, parent_id, region_name, region_code, level, ancestors,
               order_num, status, del_flag, create_by, create_time, update_by, update_time
        from pg_region
        where del_flag = '0'
    </sql>

    <!-- 查询区域列表 -->
    <select id="selectRegionList" parameterType="Region" resultMap="RegionResult">
        <include refid="selectRegionVo"/>
        <if test="regionName != null and regionName != ''">
            AND region_name like concat('%', #{regionName}, '%')
        </if>
        <if test="status != null and status != ''">
            AND status = #{status}
        </if>
        <if test="parentId != null">
            AND parent_id = #{parentId}
        </if>
        order by order_num
    </select>

    <!-- 根据ID查询区域 -->
    <select id="selectRegionById" parameterType="Long" resultMap="RegionResult">
        <include refid="selectRegionVo"/>
        AND region_id = #{regionId}
    </select>

    <!-- 新增区域 -->
    <insert id="insertRegion" parameterType="Region" useGeneratedKeys="true" keyProperty="regionId">
        insert into pg_region (
            parent_id,
            region_name,
            region_code,
            level,
            ancestors,
            order_num,
            status,
            del_flag,
            create_by,
            create_time
        ) values (
            #{parentId},
            #{regionName},
            #{regionCode},
            #{level},
            #{ancestors},
            #{orderNum},
            #{status},
            '0',
            #{createBy},
            #{createTime}
        )
    </insert>

    <!-- 修改区域 -->
    <update id="updateRegion" parameterType="Region">
        update pg_region
        <set>
            <if test="regionName != null and regionName != ''">region_name = #{regionName},</if>
            <if test="orderNum != null">order_num = #{orderNum},</if>
            <if test="status != null">status = #{status},</if>
            update_by = #{updateBy},
            update_time = #{updateTime}
        </set>
        where region_id = #{regionId}
    </update>

    <!-- 删除区域(软删除)-->
    <update id="deleteRegionById" parameterType="Long">
        update pg_region set del_flag = '1' where region_id = #{regionId}
    </update>

    <!-- 查询子区域数量 -->
    <select id="countChildByParentId" parameterType="Long" resultType="int">
        select count(*) from pg_region where parent_id = #{parentId} and del_flag = '0'
    </select>

    <!-- 查询指定层级的最大编码 -->
    <select id="selectMaxCodeByLevel" parameterType="int" resultType="String">
        select max(region_code) from pg_region where level = #{level} and del_flag = '0'
    </select>

    <!-- 查询指定父级下的最大编码 -->
    <select id="selectMaxCodeByParent" parameterType="Long" resultType="String">
        select max(region_code) from pg_region where parent_id = #{parentId} and del_flag = '0'
    </select>

</mapper>

5. Service层设计

5.1 年级Service接口

package com.pangu.base.service;

import com.pangu.base.domain.Grade;

import java.util.List;

/**
 * 年级管理Service接口
 * @author pangu
 */
public interface IGradeService {
    
    /**
     * 查询年级分页列表
     * @param grade 查询条件
     * @return 年级列表
     */
    List<Grade> selectGradeList(Grade grade);
    
    /**
     * 查询年级选项列表(下拉用)
     * @return 启用状态的年级列表
     */
    List<Grade> selectGradeOptions();
    
    /**
     * 根据ID查询年级
     * @param gradeId 年级ID
     * @return 年级信息
     */
    Grade selectGradeById(Long gradeId);
    
    /**
     * 新增年级
     * @param grade 年级信息
     * @return 影响行数
     */
    int insertGrade(Grade grade);
    
    /**
     * 修改年级
     * @param grade 年级信息
     * @return 影响行数
     */
    int updateGrade(Grade grade);
    
    /**
     * 删除年级
     * @param gradeId 年级ID
     * @return 影响行数
     */
    int deleteGradeById(Long gradeId);
    
    /**
     * 校验年级名称是否唯一
     * @param grade 年级信息
     * @return true-唯一 false-不唯一
     */
    boolean checkGradeNameUnique(Grade grade);
    
    /**
     * 检查年级是否被学校使用
     * @param gradeId 年级ID
     * @return true-被使用 false-未使用
     */
    boolean checkGradeExistSchool(Long gradeId);
}

5.2 年级Service实现

package com.pangu.base.service.impl;

import com.pangu.base.domain.Grade;
import com.pangu.base.mapper.GradeMapper;
import com.pangu.base.mapper.SchoolGradeMapper;
import com.pangu.base.service.IGradeService;
import com.pangu.common.utils.DateUtils;
import com.pangu.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 年级管理Service实现
 * @author pangu
 */
@Service
public class GradeServiceImpl implements IGradeService {

    @Autowired
    private GradeMapper gradeMapper;
    
    @Autowired
    private SchoolGradeMapper schoolGradeMapper;

    @Override
    public List<Grade> selectGradeList(Grade grade) {
        return gradeMapper.selectGradeList(grade);
    }
    
    @Override
    public List<Grade> selectGradeOptions() {
        Grade grade = new Grade();
        grade.setStatus("0"); // 只查启用的
        return gradeMapper.selectGradeList(grade);
    }

    @Override
    public Grade selectGradeById(Long gradeId) {
        return gradeMapper.selectGradeById(gradeId);
    }

    @Override
    public int insertGrade(Grade grade) {
        // 生成年级编码
        grade.setGradeCode(generateGradeCode());
        grade.setCreateTime(DateUtils.getNowDate());
        return gradeMapper.insertGrade(grade);
    }
    
    /**
     * 生成年级编码
     * 规则GRD + 3位序号如GRD001
     */
    private String generateGradeCode() {
        String maxCode = gradeMapper.selectMaxGradeCode();
        if (StringUtils.isEmpty(maxCode)) {
            return "GRD001";
        }
        // 提取序号并+1
        int num = Integer.parseInt(maxCode.substring(3)) + 1;
        return String.format("GRD%03d", num);
    }

    @Override
    public int updateGrade(Grade grade) {
        grade.setUpdateTime(DateUtils.getNowDate());
        return gradeMapper.updateGrade(grade);
    }

    @Override
    public int deleteGradeById(Long gradeId) {
        return gradeMapper.deleteGradeById(gradeId);
    }

    @Override
    public boolean checkGradeNameUnique(Grade grade) {
        Long gradeId = grade.getGradeId() == null ? -1L : grade.getGradeId();
        Grade info = gradeMapper.checkGradeNameUnique(grade.getGradeName());
        // 名称不存在,或者是当前记录自己,则唯一
        return info == null || info.getGradeId().equals(gradeId);
    }

    @Override
    public boolean checkGradeExistSchool(Long gradeId) {
        int count = schoolGradeMapper.countByGradeId(gradeId);
        return count > 0;
    }
}

5.3 区域Service接口

package com.pangu.base.service;

import com.pangu.base.domain.Region;

import java.util.List;

/**
 * 区域管理Service接口
 * @author pangu
 */
public interface IRegionService {
    
    /**
     * 查询区域树
     * @return 树形结构的区域列表
     */
    List<Region> selectRegionTree();
    
    /**
     * 根据ID查询区域
     * @param regionId 区域ID
     * @return 区域信息
     */
    Region selectRegionById(Long regionId);
    
    /**
     * 新增区域
     * @param region 区域信息
     * @return 影响行数
     */
    int insertRegion(Region region);
    
    /**
     * 修改区域
     * @param region 区域信息
     * @return 影响行数
     */
    int updateRegion(Region region);
    
    /**
     * 删除区域
     * @param regionId 区域ID
     * @return 影响行数
     */
    int deleteRegionById(Long regionId);
    
    /**
     * 是否存在子区域
     * @param regionId 区域ID
     * @return true-存在 false-不存在
     */
    boolean hasChildRegion(Long regionId);
    
    /**
     * 检查区域是否被学校使用
     * @param regionId 区域ID
     * @return true-被使用 false-未使用
     */
    boolean checkRegionExistSchool(Long regionId);
    
    /**
     * 刷新区域缓存
     */
    void refreshRegionCache();
}

5.4 区域Service实现

package com.pangu.base.service.impl;

import com.pangu.base.domain.Region;
import com.pangu.base.mapper.MemberMapper;
import com.pangu.base.mapper.RegionMapper;
import com.pangu.base.mapper.SchoolMapper;
import com.pangu.base.service.IRegionService;
import com.pangu.common.core.redis.RedisCache;
import com.pangu.common.utils.DateUtils;
import com.pangu.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 区域管理Service实现
 * @author pangu
 */
@Service
public class RegionServiceImpl implements IRegionService {

    /** 区域树缓存Key */
    private static final String REGION_TREE_KEY = "base:region:tree";
    
    /** 缓存过期时间(小时)*/
    private static final int CACHE_EXPIRE_HOURS = 24;

    @Autowired
    private RegionMapper regionMapper;
    
    @Autowired
    private SchoolMapper schoolMapper;
    
    @Autowired
    private RedisCache redisCache;

    @Override
    public List<Region> selectRegionTree() {
        // 优先从缓存获取
        List<Region> cacheList = redisCache.getCacheObject(REGION_TREE_KEY);
        if (cacheList != null && !cacheList.isEmpty()) {
            return cacheList;
        }
        
        // 查询所有区域(未删除的)
        List<Region> regionList = regionMapper.selectRegionList(new Region());
        
        // 构建树形结构
        List<Region> tree = buildRegionTree(regionList);
        
        // 放入缓存
        redisCache.setCacheObject(REGION_TREE_KEY, tree, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
        
        return tree;
    }
    
    /**
     * 构建区域树形结构
     * @param regionList 区域列表
     * @return 树形结构
     */
    private List<Region> buildRegionTree(List<Region> regionList) {
        if (regionList == null || regionList.isEmpty()) {
            return new ArrayList<>();
        }
        
        // 构建ID到节点的映射
        Map<Long, Region> regionMap = regionList.stream()
            .collect(Collectors.toMap(Region::getRegionId, r -> r));
        
        List<Region> rootList = new ArrayList<>();
        
        for (Region region : regionList) {
            if (region.getParentId() == 0) {
                // 顶级节点
                rootList.add(region);
            } else {
                // 子节点,挂载到父节点下
                Region parent = regionMap.get(region.getParentId());
                if (parent != null) {
                    if (parent.getChildren() == null) {
                        parent.setChildren(new ArrayList<>());
                    }
                    parent.getChildren().add(region);
                }
            }
        }
        
        // 对每一层按orderNum排序
        sortRegionTree(rootList);
        
        return rootList;
    }
    
    /**
     * 递归排序区域树
     */
    private void sortRegionTree(List<Region> regions) {
        if (regions == null || regions.isEmpty()) {
            return;
        }
        regions.sort(Comparator.comparingInt(r -> r.getOrderNum() == null ? 0 : r.getOrderNum()));
        for (Region region : regions) {
            if (region.getChildren() != null) {
                sortRegionTree(region.getChildren());
            }
        }
    }

    @Override
    public Region selectRegionById(Long regionId) {
        return regionMapper.selectRegionById(regionId);
    }

    @Override
    public int insertRegion(Region region) {
        // 设置层级和祖级列表
        if (region.getParentId() == null || region.getParentId() == 0) {
            region.setParentId(0L);
            region.setLevel(1);
            region.setAncestors("0");
        } else {
            Region parent = regionMapper.selectRegionById(region.getParentId());
            if (parent == null) {
                throw new RuntimeException("父区域不存在");
            }
            region.setLevel(parent.getLevel() + 1);
            region.setAncestors(parent.getAncestors() + "," + parent.getRegionId());
        }
        
        // 生成区域编码
        region.setRegionCode(generateRegionCode(region));
        region.setCreateTime(DateUtils.getNowDate());
        
        // 默认状态为启用
        if (region.getStatus() == null) {
            region.setStatus("0");
        }
        
        int rows = regionMapper.insertRegion(region);
        
        // 清除缓存
        if (rows > 0) {
            refreshRegionCache();
        }
        
        return rows;
    }
    
    /**
     * 生成区域编码
     * 规则:
     * - 省级REG + 2位序号如REG01
     * - 市级:父编码 + 2位序号如REG0101
     * - 区级:父编码 + 2位序号如REG010101
     */
    private String generateRegionCode(Region region) {
        if (region.getParentId() == 0) {
            // 省级编码
            String maxCode = regionMapper.selectMaxCodeByLevel(1);
            if (StringUtils.isEmpty(maxCode)) {
                return "REG01";
            }
            int num = Integer.parseInt(maxCode.substring(3)) + 1;
            return String.format("REG%02d", num);
        } else {
            // 市/区级编码
            Region parent = regionMapper.selectRegionById(region.getParentId());
            String maxCode = regionMapper.selectMaxCodeByParent(region.getParentId());
            if (StringUtils.isEmpty(maxCode)) {
                return parent.getRegionCode() + "01";
            }
            String suffix = maxCode.substring(parent.getRegionCode().length());
            int num = Integer.parseInt(suffix) + 1;
            return parent.getRegionCode() + String.format("%02d", num);
        }
    }

    @Override
    public int updateRegion(Region region) {
        region.setUpdateTime(DateUtils.getNowDate());
        int rows = regionMapper.updateRegion(region);
        
        // 清除缓存
        if (rows > 0) {
            refreshRegionCache();
        }
        
        return rows;
    }

    @Override
    public int deleteRegionById(Long regionId) {
        int rows = regionMapper.deleteRegionById(regionId);
        
        // 清除缓存
        if (rows > 0) {
            refreshRegionCache();
        }
        
        return rows;
    }

    @Override
    public boolean hasChildRegion(Long regionId) {
        int count = regionMapper.countChildByParentId(regionId);
        return count > 0;
    }

    @Override
    public boolean checkRegionExistSchool(Long regionId) {
        int count = schoolMapper.countByRegionId(regionId);
        return count > 0;
    }
    
    @Override
    public void refreshRegionCache() {
        redisCache.deleteObject(REGION_TREE_KEY);
    }
}

6. Controller层设计

6.1 年级Controller

package com.pangu.base.controller;

import com.pangu.base.domain.Grade;
import com.pangu.base.service.IGradeService;
import com.pangu.common.annotation.Log;
import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.core.page.TableDataInfo;
import com.pangu.common.enums.BusinessType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 年级管理Controller
 * @author pangu
 */
@RestController
@RequestMapping("/api/grade")
public class GradeController extends BaseController {

    @Autowired
    private IGradeService gradeService;

    /**
     * 获取年级分页列表
     */
    @PreAuthorize("@ss.hasPermi('base:grade:list')")
    @GetMapping("/list")
    public TableDataInfo list(Grade grade) {
        startPage();
        List<Grade> list = gradeService.selectGradeList(grade);
        return getDataTable(list);
    }

    /**
     * 获取年级选项列表(下拉用)
     */
    @GetMapping("/options")
    public AjaxResult options() {
        List<Grade> list = gradeService.selectGradeOptions();
        return success(list);
    }

    /**
     * 获取年级详情
     */
    @PreAuthorize("@ss.hasPermi('base:grade:query')")
    @GetMapping("/{gradeId}")
    public AjaxResult getInfo(@PathVariable Long gradeId) {
        return success(gradeService.selectGradeById(gradeId));
    }

    /**
     * 新增年级
     */
    @PreAuthorize("@ss.hasPermi('base:grade:add')")
    @Log(title = "年级管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody Grade grade) {
        // 校验名称唯一
        if (!gradeService.checkGradeNameUnique(grade)) {
            return error("新增年级'" + grade.getGradeName() + "'失败,年级名称已存在");
        }
        grade.setCreateBy(getUsername());
        return toAjax(gradeService.insertGrade(grade));
    }

    /**
     * 修改年级
     */
    @PreAuthorize("@ss.hasPermi('base:grade:edit')")
    @Log(title = "年级管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody Grade grade) {
        // 校验名称唯一
        if (!gradeService.checkGradeNameUnique(grade)) {
            return error("修改年级'" + grade.getGradeName() + "'失败,年级名称已存在");
        }
        grade.setUpdateBy(getUsername());
        return toAjax(gradeService.updateGrade(grade));
    }

    /**
     * 删除年级
     */
    @PreAuthorize("@ss.hasPermi('base:grade:remove')")
    @Log(title = "年级管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{gradeId}")
    public AjaxResult remove(@PathVariable Long gradeId) {
        // 检查是否被学校引用
        if (gradeService.checkGradeExistSchool(gradeId)) {
            return error("该年级已被学校使用,不能删除");
        }
        return toAjax(gradeService.deleteGradeById(gradeId));
    }
}

6.2 区域Controller

package com.pangu.base.controller;

import com.pangu.base.domain.Region;
import com.pangu.base.service.IRegionService;
import com.pangu.common.annotation.Log;
import com.pangu.common.core.controller.BaseController;
import com.pangu.common.core.domain.AjaxResult;
import com.pangu.common.enums.BusinessType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 区域管理Controller
 * @author pangu
 */
@RestController
@RequestMapping("/api/region")
public class RegionController extends BaseController {

    @Autowired
    private IRegionService regionService;

    /**
     * 获取区域树
     */
    @GetMapping("/tree")
    public AjaxResult tree() {
        List<Region> tree = regionService.selectRegionTree();
        return success(tree);
    }

    /**
     * 获取区域详情
     */
    @PreAuthorize("@ss.hasPermi('base:region:query')")
    @GetMapping("/{regionId}")
    public AjaxResult getInfo(@PathVariable Long regionId) {
        return success(regionService.selectRegionById(regionId));
    }

    /**
     * 新增区域
     */
    @PreAuthorize("@ss.hasPermi('base:region:add')")
    @Log(title = "区域管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody Region region) {
        region.setCreateBy(getUsername());
        return toAjax(regionService.insertRegion(region));
    }

    /**
     * 修改区域
     */
    @PreAuthorize("@ss.hasPermi('base:region:edit')")
    @Log(title = "区域管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody Region region) {
        // 父级不能选择自己
        if (region.getRegionId().equals(region.getParentId())) {
            return error("修改区域'" + region.getRegionName() + "'失败,上级区域不能选择自己");
        }
        region.setUpdateBy(getUsername());
        return toAjax(regionService.updateRegion(region));
    }

    /**
     * 删除区域
     */
    @PreAuthorize("@ss.hasPermi('base:region:remove')")
    @Log(title = "区域管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{regionId}")
    public AjaxResult remove(@PathVariable Long regionId) {
        // 检查是否有子区域
        if (regionService.hasChildRegion(regionId)) {
            return error("存在下级区域,不能删除");
        }
        // 检查是否被学校引用
        if (regionService.checkRegionExistSchool(regionId)) {
            return error("该区域已被学校使用,不能删除");
        }
        return toAjax(regionService.deleteRegionById(regionId));
    }
    
    /**
     * 刷新区域缓存
     */
    @PreAuthorize("@ss.hasPermi('base:region:remove')")
    @Log(title = "区域管理", businessType = BusinessType.CLEAN)
    @DeleteMapping("/refreshCache")
    public AjaxResult refreshCache() {
        regionService.refreshRegionCache();
        return success();
    }
}

7. 权限配置

7.1 菜单权限

在系统菜单中添加以下菜单:

菜单名称 菜单路径 权限标识 菜单类型
基础数据 /base - 目录
年级管理 /base/grade base:grade:list 菜单
班级管理 /base/class base:class:list 菜单
学科管理 /base/subject base:subject:list 菜单
区域管理 /base/region base:region:list 菜单

7.2 按钮权限

功能 权限标识
年级查询 base:grade:query
年级新增 base:grade:add
年级修改 base:grade:edit
年级删除 base:grade:remove
班级查询 base:class:query
班级新增 base:class:add
班级修改 base:class:edit
班级删除 base:class:remove
学科查询 base:subject:query
学科新增 base:subject:add
学科修改 base:subject:edit
学科删除 base:subject:remove
区域查询 base:region:query
区域新增 base:region:add
区域修改 base:region:edit
区域删除 base:region:remove

8. 单元测试

8.1 年级Service测试

package com.pangu.base.service;

import com.pangu.base.domain.Grade;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 年级Service单元测试
 * @author pangu
 */
@SpringBootTest
public class GradeServiceTest {

    @Autowired
    private IGradeService gradeService;

    @Test
    public void testSelectGradeList() {
        Grade grade = new Grade();
        List<Grade> list = gradeService.selectGradeList(grade);
        assertNotNull(list);
        assertTrue(list.size() > 0);
    }

    @Test
    public void testSelectGradeOptions() {
        List<Grade> list = gradeService.selectGradeOptions();
        assertNotNull(list);
        // 选项列表应该只包含启用的
        for (Grade grade : list) {
            assertEquals("0", grade.getStatus());
        }
    }

    @Test
    public void testInsertGrade() {
        Grade grade = new Grade();
        grade.setGradeName("测试年级");
        grade.setOrderNum(99);
        grade.setStatus("0");
        grade.setCreateBy("test");
        
        int result = gradeService.insertGrade(grade);
        assertEquals(1, result);
        assertNotNull(grade.getGradeId());
        assertNotNull(grade.getGradeCode());
        assertTrue(grade.getGradeCode().startsWith("GRD"));
        
        // 清理测试数据
        gradeService.deleteGradeById(grade.getGradeId());
    }

    @Test
    public void testCheckGradeNameUnique() {
        // 新增时检查
        Grade grade = new Grade();
        grade.setGradeName("一年级"); // 假设已存在
        boolean unique = gradeService.checkGradeNameUnique(grade);
        assertFalse(unique);
        
        // 不存在的名称
        grade.setGradeName("不存在的年级");
        unique = gradeService.checkGradeNameUnique(grade);
        assertTrue(unique);
    }
}

8.2 区域Service测试

package com.pangu.base.service;

import com.pangu.base.domain.Region;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 区域Service单元测试
 * @author pangu
 */
@SpringBootTest
public class RegionServiceTest {

    @Autowired
    private IRegionService regionService;

    @Test
    public void testSelectRegionTree() {
        List<Region> tree = regionService.selectRegionTree();
        assertNotNull(tree);
        assertTrue(tree.size() > 0);
        
        // 验证树形结构
        Region root = tree.get(0);
        assertEquals(Long.valueOf(0), root.getParentId());
        assertEquals(Integer.valueOf(1), root.getLevel());
    }

    @Test
    public void testInsertRegion() {
        // 新增省级区域
        Region region = new Region();
        region.setParentId(0L);
        region.setRegionName("测试省");
        region.setOrderNum(99);
        region.setCreateBy("test");
        
        int result = regionService.insertRegion(region);
        assertEquals(1, result);
        assertNotNull(region.getRegionId());
        assertNotNull(region.getRegionCode());
        assertEquals(Integer.valueOf(1), region.getLevel());
        assertEquals("0", region.getAncestors());
        
        // 清理测试数据
        regionService.deleteRegionById(region.getRegionId());
    }

    @Test
    public void testHasChildRegion() {
        // 湖北省应该有子区域
        boolean hasChild = regionService.hasChildRegion(1L);
        assertTrue(hasChild);
    }

    @Test
    public void testRefreshRegionCache() {
        // 第一次查询,建立缓存
        List<Region> tree1 = regionService.selectRegionTree();
        
        // 刷新缓存
        regionService.refreshRegionCache();
        
        // 第二次查询,重新从数据库加载
        List<Region> tree2 = regionService.selectRegionTree();
        
        // 数据应该一致
        assertEquals(tree1.size(), tree2.size());
    }
}

9. 开发检查清单

9.1 年级管理

  • Grade 实体类
  • GradeMapper 接口
  • GradeMapper.xml
  • IGradeService 接口
  • GradeServiceImpl 实现
  • GradeController
  • 单元测试
  • 权限配置

9.2 班级管理

  • PgClass 实体类
  • PgClassMapper 接口
  • PgClassMapper.xml
  • IPgClassService 接口
  • PgClassServiceImpl 实现
  • PgClassController
  • 单元测试
  • 权限配置

9.3 学科管理

  • Subject 实体类
  • SubjectMapper 接口
  • SubjectMapper.xml
  • ISubjectService 接口
  • SubjectServiceImpl 实现
  • SubjectController
  • 单元测试
  • 权限配置

9.4 区域管理

  • Region 实体类
  • RegionMapper 接口
  • RegionMapper.xml
  • IRegionService 接口
  • RegionServiceImpl 实现(含缓存)
  • RegionController
  • 单元测试
  • 权限配置

文档结束