1885 lines
59 KiB
Markdown
1885 lines
59 KiB
Markdown
# 盘古用户平台 - 基础数据模块技术方案
|
||
|
||
---
|
||
|
||
| 文档信息 | 内容 |
|
||
| -------- | --------------------------------- |
|
||
| **文档版本** | V1.0 |
|
||
| **项目名称** | 盘古用户平台(Pangu User Platform) |
|
||
| **模块名称** | 基础数据管理(年级、班级、学科、区域) |
|
||
| **编写团队** | pangu |
|
||
| **创建日期** | 2026-01-31 |
|
||
| **审核状态** | 待评审 |
|
||
|
||
---
|
||
|
||
## 修订记录
|
||
|
||
| 版本 | 日期 | 修订人 | 修订内容 |
|
||
| ---- | ---------- | ----- | ---- |
|
||
| V1.0 | 2026-01-31 | pangu | 初稿 |
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [模块概述](#1-模块概述)
|
||
2. [需求分析](#2-需求分析)
|
||
3. [系统设计](#3-系统设计)
|
||
4. [前端技术方案](#4-前端技术方案)
|
||
5. [后端技术方案](#5-后端技术方案)
|
||
6. [数据库设计](#6-数据库设计)
|
||
7. [接口设计](#7-接口设计)
|
||
8. [开发计划](#8-开发计划)
|
||
9. [测试方案](#9-测试方案)
|
||
10. [风险与应对](#10-风险与应对)
|
||
|
||
---
|
||
|
||
## 1. 模块概述
|
||
|
||
### 1.1 模块定位
|
||
|
||
基础数据模块是盘古用户平台的核心支撑模块,负责管理系统运行所需的基础字典数据。这些数据被学校管理、学生管理等业务模块广泛引用,是整个系统数据完整性的基础保障。
|
||
|
||
### 1.2 功能范围
|
||
|
||
| 子模块 | 功能描述 | 数据特性 |
|
||
| ----- | ----------------- | -------- |
|
||
| 年级管理 | 管理年级字典数据(一年级~高三等) | 字典型、被学校引用 |
|
||
| 班级管理 | 管理班级字典数据(1班~10班等) | 字典型、被学校引用 |
|
||
| 学科管理 | 管理学科字典数据(语文、数学等) | 字典型、被学生引用 |
|
||
| 区域管理 | 管理省-市-区三级区域树 | 树形、被学校引用 |
|
||
|
||
> **说明**:区域管理的核心逻辑是管理每个区域下的学校信息,区域只被学校表直接引用,会员和学生通过学校间接关联区域。
|
||
|
||
### 1.3 关联关系
|
||
|
||
```
|
||
基础数据模块关联图:
|
||
|
||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||
│ 年级管理 │────▶│ 学校-年级 │────▶│ 学校管理 │
|
||
│ (pg_grade) │ │ 关联表 │ │ (pg_school) │
|
||
└─────────────┘ └─────────────┘ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌─────────────┐
|
||
│ 班级管理 │────▶│ 学校-班级 │
|
||
│ (pg_class) │ │ 关联表 │
|
||
└─────────────┘ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌─────────────┐
|
||
│ 学科管理 │────▶│ 学生管理 │
|
||
│ (pg_subject)│ │ (pg_student)│
|
||
└─────────────┘ └─────────────┘
|
||
|
||
┌─────────────┐
|
||
│ 区域管理 │────▶ 学校(区域ID)
|
||
│ (pg_region) │
|
||
└─────────────┘
|
||
|
||
> 区域通过学校间接关联会员和学生,删除区域时只需检查学校引用
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 需求分析
|
||
|
||
### 2.1 功能需求
|
||
|
||
#### 2.1.1 年级管理
|
||
|
||
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
||
| ------- | ------ | --------------- | :-: |
|
||
| GRD-001 | 年级列表查询 | 按名称、编码、状态筛选分页查询 | P0 |
|
||
| GRD-002 | 新增年级 | 创建年级,编码自动生成 | P0 |
|
||
| GRD-003 | 编辑年级 | 修改年级名称、排序、状态 | P0 |
|
||
| GRD-004 | 删除年级 | 软删除(需检查学校引用) | P1 |
|
||
| GRD-005 | 年级选项 | 供下拉选择使用的选项列表 | P0 |
|
||
|
||
**业务规则:**
|
||
- 年级编码自动生成,格式:GRD + 3位序号(如GRD001)
|
||
- 删除前检查是否被学校-年级关联表引用,有则不允许删除
|
||
- 所有删除为软删除(del_flag = 1)
|
||
|
||
#### 2.1.2 班级管理
|
||
|
||
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
||
| ------- | ------ | --------------- | :-: |
|
||
| CLS-001 | 班级列表查询 | 按名称、编码、状态筛选分页查询 | P0 |
|
||
| CLS-002 | 新增班级 | 创建班级,编码自动生成 | P0 |
|
||
| CLS-003 | 编辑班级 | 修改班级名称、排序、状态 | P0 |
|
||
| CLS-004 | 删除班级 | 软删除(需检查学校引用) | P1 |
|
||
| CLS-005 | 班级选项 | 供下拉选择使用的选项列表 | P0 |
|
||
|
||
**业务规则:**
|
||
- 班级编码自动生成,格式:CLS + 3位序号(如CLS001)
|
||
- 删除前检查是否被学校-班级关联表引用,有则不允许删除
|
||
|
||
#### 2.1.3 学科管理
|
||
|
||
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
||
| ------- | ------ | --------------- | :-: |
|
||
| SUB-001 | 学科列表查询 | 按名称、编码、状态筛选分页查询 | P0 |
|
||
| SUB-002 | 新增学科 | 创建学科信息 | P0 |
|
||
| SUB-003 | 编辑学科 | 修改学科名称、排序、状态 | P0 |
|
||
| SUB-004 | 删除学科 | 软删除(需检查学生引用) | P1 |
|
||
| SUB-005 | 学科选项 | 供下拉选择使用的选项列表 | P0 |
|
||
|
||
**业务规则:**
|
||
- 学科编码自动生成,格式:SUB + 3位序号(如SUB001)
|
||
- 删除前检查是否被学生表引用,有则不允许删除
|
||
|
||
#### 2.1.4 区域管理
|
||
|
||
| 功能编号 | 功能名称 | 功能描述 | 优先级 |
|
||
| ------- | ----- | ------------------- | :-: |
|
||
| REG-001 | 区域树查询 | 树形展示省-市-区层级结构 | P0 |
|
||
| REG-002 | 新增区域 | 在指定节点下新增区域 | P0 |
|
||
| REG-003 | 编辑区域 | 修改区域名称 | P0 |
|
||
| REG-004 | 删除区域 | 软删除(需检查子级和学校/会员引用) | P1 |
|
||
| REG-005 | 新增下级 | 在当前区域下新增子区域(自动带入父级) | P0 |
|
||
|
||
**业务规则:**
|
||
- 区域编码自动生成,格式:区分层级
|
||
- 省级:REG + 2位序号(如REG01)
|
||
- 市级:父编码 + 2位序号(如REG0101)
|
||
- 区级:父编码 + 2位序号(如REG010101)
|
||
- 新增下级时自动带入父级区域信息
|
||
- 删除前检查:
|
||
- 是否有子级区域,有则不允许删除
|
||
- 是否被学校引用,有则不允许删除
|
||
|
||
### 2.2 角色权限
|
||
|
||
| 功能 | 超级管理员 | 分公司用户 | 学校用户 |
|
||
| ------ | :---: | :---: | :--: |
|
||
| 年级管理 | ✓ | ✓ | ✗ |
|
||
| 班级管理 | ✓ | ✓ | ✗ |
|
||
| 学科管理 | ✓ | ✓ | ✗ |
|
||
| 区域管理 | ✓ | ✗ | ✗ |
|
||
|
||
### 2.3 非功能需求
|
||
|
||
| 需求类型 | 需求描述 |
|
||
| ------ | --------------- |
|
||
| 性能需求 | 列表查询响应时间 ≤ 200ms |
|
||
| 数据完整性 | 删除前必须检查引用关系 |
|
||
| 缓存需求 | 区域树数据需缓存,减少查询压力 |
|
||
| 日志需求 | 增删改操作需记录操作日志 |
|
||
|
||
---
|
||
|
||
## 3. 系统设计
|
||
|
||
### 3.1 模块架构
|
||
|
||
```
|
||
基础数据模块架构:
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 前端层 (Vue 3) │
|
||
├────────────────┬────────────────┬───────────────┬───────────────┤
|
||
│ GradeView │ ClassView │ SubjectView │ RegionView │
|
||
│ 年级管理页面 │ 班级管理页面 │ 学科管理页面 │ 区域管理页面 │
|
||
└───────┬────────┴───────┬────────┴───────┬───────┴───────┬───────┘
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ API层 (Axios) │
|
||
│ grade.js class.js subject.js region.js │
|
||
└───────────────────────────────┬─────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 后端层 (Spring Boot) │
|
||
├────────────────┬────────────────┬───────────────┬───────────────┤
|
||
│ GradeController│ ClassController│SubjectController│RegionController│
|
||
└───────┬────────┴───────┬────────┴───────┬───────┴───────┬───────┘
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Service层 │
|
||
│ GradeService ClassService SubjectService RegionService │
|
||
└───────────────────────────────┬─────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Mapper层 (MyBatis Plus) │
|
||
│ GradeMapper ClassMapper SubjectMapper RegionMapper │
|
||
└───────────────────────────────┬─────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ MySQL 8.0 │
|
||
│ pg_grade pg_class pg_subject pg_region │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 技术选型
|
||
|
||
| 层次 | 技术 | 版本 | 说明 |
|
||
| --- | ------------ | ----- | ------ |
|
||
| 前端 | Vue | 3.5.x | 视图框架 |
|
||
| 前端 | Element Plus | 2.x | UI组件库 |
|
||
| 前端 | Pinia | 3.x | 状态管理 |
|
||
| 后端 | Spring Boot | 3.3.x | 应用框架 |
|
||
| 后端 | MyBatis Plus | 3.5.x | ORM框架 |
|
||
| 数据库 | MySQL | 8.0 | 关系数据库 |
|
||
| 缓存 | Redis | 7.x | 区域树缓存 |
|
||
|
||
### 3.3 目录结构
|
||
|
||
**前端目录结构:**
|
||
|
||
```
|
||
pangu-ui/src/
|
||
├── api/
|
||
│ ├── grade.js # 年级API
|
||
│ ├── class.js # 班级API
|
||
│ ├── subject.js # 学科API
|
||
│ └── region.js # 区域API
|
||
├── mock/
|
||
│ ├── grade.js # 年级Mock
|
||
│ ├── class.js # 班级Mock
|
||
│ ├── subject.js # 学科Mock
|
||
│ └── region.js # 区域Mock
|
||
└── views/base/
|
||
├── grade/
|
||
│ └── index.vue # 年级管理页面
|
||
├── class/
|
||
│ └── index.vue # 班级管理页面
|
||
├── subject/
|
||
│ └── index.vue # 学科管理页面
|
||
└── region/
|
||
└── index.vue # 区域管理页面
|
||
```
|
||
|
||
**后端目录结构:**
|
||
|
||
```
|
||
pangu-admin/src/main/java/com/pangu/
|
||
├── base/
|
||
│ ├── controller/
|
||
│ │ ├── GradeController.java
|
||
│ │ ├── ClassController.java
|
||
│ │ ├── SubjectController.java
|
||
│ │ └── RegionController.java
|
||
│ ├── service/
|
||
│ │ ├── IGradeService.java
|
||
│ │ ├── IClassService.java
|
||
│ │ ├── ISubjectService.java
|
||
│ │ ├── IRegionService.java
|
||
│ │ └── impl/
|
||
│ │ ├── GradeServiceImpl.java
|
||
│ │ ├── ClassServiceImpl.java
|
||
│ │ ├── SubjectServiceImpl.java
|
||
│ │ └── RegionServiceImpl.java
|
||
│ ├── mapper/
|
||
│ │ ├── GradeMapper.java
|
||
│ │ ├── ClassMapper.java
|
||
│ │ ├── SubjectMapper.java
|
||
│ │ └── RegionMapper.java
|
||
│ └── domain/
|
||
│ ├── Grade.java
|
||
│ ├── PgClass.java
|
||
│ ├── Subject.java
|
||
│ └── Region.java
|
||
└── resources/mapper/base/
|
||
├── GradeMapper.xml
|
||
├── ClassMapper.xml
|
||
├── SubjectMapper.xml
|
||
└── RegionMapper.xml
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 前端技术方案
|
||
|
||
### 4.1 页面设计
|
||
|
||
#### 4.1.1 年级/班级/学科管理(列表页通用模式)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 搜索区域 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ [名称输入框] [状态下拉] [搜索按钮] [重置按钮] │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 操作区域 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ [新增按钮] │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 数据表格 │
|
||
├─────────┬─────────┬──────┬──────┬────────────┬─────────────────┤
|
||
│ 名称 │ 编码 │ 排序 │ 状态 │ 创建时间 │ 操作 │
|
||
├─────────┼─────────┼──────┼──────┼────────────┼─────────────────┤
|
||
│ 一年级 │ GRD001 │ 1 │ 正常 │ 2026-01-01 │ [编辑] [删除] │
|
||
│ 二年级 │ GRD002 │ 2 │ 正常 │ 2026-01-01 │ [编辑] [删除] │
|
||
└─────────┴─────────┴──────┴──────┴────────────┴─────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 分页区域 │
|
||
│ 共 12 条 < 1 2 > 10条/页 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 4.1.2 区域管理(树形表格模式)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 操作区域 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ [新增按钮] [展开全部] [折叠全部] │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 树形表格 │
|
||
├────────────────────┬─────────┬────────────┬────────────────────┤
|
||
│ 区域名称 │ 区域编码 │ 创建时间 │ 操作 │
|
||
├────────────────────┼─────────┼────────────┼────────────────────┤
|
||
│ ▼ 湖北 │ REG01 │ 2026-01-01 │ [新增下级][编辑][删除]│
|
||
│ ▼ 武汉 │ REG0101 │ 2026-01-01 │ [新增下级][编辑][删除]│
|
||
│ ├ 武昌区 │ REG010101│2026-01-01 │ [编辑][删除] │
|
||
│ ├ 汉口区 │ REG010102│2026-01-01 │ [编辑][删除] │
|
||
│ └ 汉阳区 │ REG010103│2026-01-01 │ [编辑][删除] │
|
||
│ └ 黄冈 │ REG0102 │ 2026-01-01 │ [新增下级][编辑][删除]│
|
||
│ ► 北京 │ REG02 │ 2026-01-01 │ [新增下级][编辑][删除]│
|
||
└────────────────────┴─────────┴────────────┴────────────────────┘
|
||
```
|
||
|
||
### 4.2 组件设计
|
||
|
||
#### 4.2.1 通用组件结构
|
||
|
||
```vue
|
||
<!-- views/base/grade/index.vue 示例结构 -->
|
||
<template>
|
||
<div class="app-container">
|
||
<!-- 搜索区域 -->
|
||
<el-card shadow="never" class="search-wrapper">
|
||
<el-form :model="queryParams" :inline="true">
|
||
<!-- 搜索表单项 -->
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- 表格区域 -->
|
||
<el-card shadow="never" style="margin-top: 12px">
|
||
<!-- 操作按钮 -->
|
||
<el-row :gutter="10" style="margin-bottom: 12px">
|
||
<el-col :span="1.5">
|
||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 数据表格 -->
|
||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||
<!-- 表格列定义 -->
|
||
</el-table>
|
||
|
||
<!-- 分页组件 -->
|
||
<el-pagination />
|
||
</el-card>
|
||
|
||
<!-- 新增/编辑弹窗 -->
|
||
<el-dialog v-model="dialogVisible" :title="dialogTitle">
|
||
<el-form ref="formRef" :model="form" :rules="rules">
|
||
<!-- 表单项 -->
|
||
</el-form>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
#### 4.2.2 年级管理组件
|
||
|
||
**核心功能实现:**
|
||
|
||
```javascript
|
||
// 年级管理核心逻辑
|
||
const queryParams = ref({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
gradeName: '',
|
||
status: ''
|
||
})
|
||
|
||
const form = ref({
|
||
gradeId: null,
|
||
gradeName: '',
|
||
gradeCode: '',
|
||
orderNum: 0,
|
||
status: '0'
|
||
})
|
||
|
||
const rules = {
|
||
gradeName: [
|
||
{ required: true, message: '请输入年级名称', trigger: 'blur' },
|
||
{ max: 50, message: '年级名称长度不能超过50个字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// 新增时自动生成编码
|
||
const handleAdd = () => {
|
||
form.value = {
|
||
gradeId: null,
|
||
gradeName: '',
|
||
gradeCode: '', // 由后端生成
|
||
orderNum: tableData.value.length + 1,
|
||
status: '0'
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 删除前检查引用
|
||
const handleDelete = async (row) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除年级"${row.gradeName}"吗?删除后不可恢复!`,
|
||
'删除确认',
|
||
{ type: 'warning' }
|
||
)
|
||
const res = await deleteGrade(row.gradeId)
|
||
if (res.code === 200) {
|
||
ElMessage.success('删除成功')
|
||
getList()
|
||
} else {
|
||
// 被引用时后端返回错误信息
|
||
ElMessage.error(res.msg || '删除失败')
|
||
}
|
||
} catch {
|
||
// 用户取消
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.2.3 区域管理组件
|
||
|
||
**树形表格核心逻辑:**
|
||
|
||
```javascript
|
||
// 区域管理核心逻辑
|
||
import { ref, onMounted } from 'vue'
|
||
import { getRegionTree, addRegion, updateRegion, deleteRegion } from '@/api/region'
|
||
|
||
const treeData = ref([])
|
||
const expandedRowKeys = ref([])
|
||
|
||
// 获取区域树
|
||
const getTree = async () => {
|
||
const res = await getRegionTree()
|
||
if (res.code === 200) {
|
||
treeData.value = res.data
|
||
}
|
||
}
|
||
|
||
// 新增下级
|
||
const handleAddChild = (row) => {
|
||
dialogTitle.value = '新增下级区域'
|
||
form.value = {
|
||
regionId: null,
|
||
parentId: row.regionId,
|
||
regionName: '',
|
||
level: row.level + 1,
|
||
parentName: row.regionName // 显示用
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 展开/折叠全部
|
||
const toggleExpandAll = (expand) => {
|
||
if (expand) {
|
||
// 收集所有非叶子节点的ID
|
||
const getAllKeys = (nodes) => {
|
||
let keys = []
|
||
nodes.forEach(node => {
|
||
if (node.children && node.children.length > 0) {
|
||
keys.push(node.regionId)
|
||
keys = keys.concat(getAllKeys(node.children))
|
||
}
|
||
})
|
||
return keys
|
||
}
|
||
expandedRowKeys.value = getAllKeys(treeData.value)
|
||
} else {
|
||
expandedRowKeys.value = []
|
||
}
|
||
}
|
||
|
||
// 删除区域(检查子级)
|
||
const handleDelete = async (row) => {
|
||
// 检查是否有子级
|
||
if (row.children && row.children.length > 0) {
|
||
ElMessage.warning('该区域下有子区域,不能删除')
|
||
return
|
||
}
|
||
// 执行删除(后端还会检查是否被学校引用)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 4.3 API接口封装
|
||
|
||
#### 4.3.1 年级管理API
|
||
|
||
```javascript
|
||
// api/grade.js
|
||
import request from '@/utils/request'
|
||
|
||
/**
|
||
* 获取年级分页列表
|
||
*/
|
||
export function getGradeList(params) {
|
||
return request({
|
||
url: '/api/grade/list',
|
||
method: 'get',
|
||
params
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 获取年级选项列表(下拉用)
|
||
*/
|
||
export function getGradeOptions() {
|
||
return request({
|
||
url: '/api/grade/options',
|
||
method: 'get'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 获取年级详情
|
||
*/
|
||
export function getGradeDetail(gradeId) {
|
||
return request({
|
||
url: `/api/grade/${gradeId}`,
|
||
method: 'get'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 新增年级
|
||
*/
|
||
export function addGrade(data) {
|
||
return request({
|
||
url: '/api/grade',
|
||
method: 'post',
|
||
data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 修改年级
|
||
*/
|
||
export function updateGrade(data) {
|
||
return request({
|
||
url: '/api/grade',
|
||
method: 'put',
|
||
data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 删除年级
|
||
*/
|
||
export function deleteGrade(gradeId) {
|
||
return request({
|
||
url: `/api/grade/${gradeId}`,
|
||
method: 'delete'
|
||
})
|
||
}
|
||
```
|
||
|
||
#### 4.3.2 区域管理API
|
||
|
||
```javascript
|
||
// api/region.js
|
||
import request from '@/utils/request'
|
||
|
||
/**
|
||
* 获取区域树
|
||
*/
|
||
export function getRegionTree() {
|
||
return request({
|
||
url: '/api/region/tree',
|
||
method: 'get'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 获取区域详情
|
||
*/
|
||
export function getRegionDetail(regionId) {
|
||
return request({
|
||
url: `/api/region/${regionId}`,
|
||
method: 'get'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 新增区域
|
||
*/
|
||
export function addRegion(data) {
|
||
return request({
|
||
url: '/api/region',
|
||
method: 'post',
|
||
data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 修改区域
|
||
*/
|
||
export function updateRegion(data) {
|
||
return request({
|
||
url: '/api/region',
|
||
method: 'put',
|
||
data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 删除区域
|
||
*/
|
||
export function deleteRegion(regionId) {
|
||
return request({
|
||
url: `/api/region/${regionId}`,
|
||
method: 'delete'
|
||
})
|
||
}
|
||
```
|
||
|
||
### 4.4 Mock数据
|
||
|
||
```javascript
|
||
// mock/grade.js 示例
|
||
import Mock from 'mockjs'
|
||
|
||
const gradeData = [
|
||
{ gradeId: 1, gradeName: '一年级', gradeCode: 'GRD001', orderNum: 1, status: '0', createTime: '2026-01-01 10:00:00' },
|
||
{ gradeId: 2, gradeName: '二年级', gradeCode: 'GRD002', orderNum: 2, status: '0', createTime: '2026-01-01 10:00:00' },
|
||
// ...更多数据
|
||
]
|
||
|
||
// 获取年级列表
|
||
Mock.mock(/\/api\/grade\/list/, 'get', (options) => {
|
||
const url = new URL('http://localhost' + options.url)
|
||
const gradeName = url.searchParams.get('gradeName') || ''
|
||
const status = url.searchParams.get('status')
|
||
const pageNum = parseInt(url.searchParams.get('pageNum')) || 1
|
||
const pageSize = parseInt(url.searchParams.get('pageSize')) || 10
|
||
|
||
let list = gradeData.filter(item => {
|
||
let match = true
|
||
if (gradeName) {
|
||
match = match && item.gradeName.includes(gradeName)
|
||
}
|
||
if (status !== null && status !== '') {
|
||
match = match && item.status === status
|
||
}
|
||
return match
|
||
})
|
||
|
||
const total = list.length
|
||
const start = (pageNum - 1) * pageSize
|
||
const rows = list.slice(start, start + pageSize)
|
||
|
||
return {
|
||
code: 200,
|
||
msg: '查询成功',
|
||
total,
|
||
rows
|
||
}
|
||
})
|
||
```
|
||
|
||
### 4.5 路由配置
|
||
|
||
```javascript
|
||
// router/index.js
|
||
{
|
||
path: '/base',
|
||
component: Layout,
|
||
redirect: '/base/grade',
|
||
name: 'Base',
|
||
meta: { title: '基础数据', icon: 'Setting' },
|
||
children: [
|
||
{
|
||
path: 'grade',
|
||
name: 'Grade',
|
||
component: () => import('@/views/base/grade/index.vue'),
|
||
meta: { title: '年级管理' }
|
||
},
|
||
{
|
||
path: 'class',
|
||
name: 'Class',
|
||
component: () => import('@/views/base/class/index.vue'),
|
||
meta: { title: '班级管理' }
|
||
},
|
||
{
|
||
path: 'subject',
|
||
name: 'Subject',
|
||
component: () => import('@/views/base/subject/index.vue'),
|
||
meta: { title: '学科管理' }
|
||
},
|
||
{
|
||
path: 'region',
|
||
name: 'Region',
|
||
component: () => import('@/views/base/region/index.vue'),
|
||
meta: { title: '区域管理' }
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 后端技术方案
|
||
|
||
### 5.1 Controller层设计
|
||
|
||
#### 5.1.1 年级管理Controller
|
||
|
||
```java
|
||
/**
|
||
* 年级管理Controller
|
||
* @author pangu
|
||
*/
|
||
@RestController
|
||
@RequestMapping("/api/grade")
|
||
public class GradeController extends BaseController {
|
||
|
||
@Autowired
|
||
private IGradeService gradeService;
|
||
|
||
/**
|
||
* 获取年级分页列表
|
||
*/
|
||
@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);
|
||
}
|
||
|
||
/**
|
||
* 获取年级详情
|
||
*/
|
||
@GetMapping("/{gradeId}")
|
||
public AjaxResult getInfo(@PathVariable Long gradeId) {
|
||
return success(gradeService.selectGradeById(gradeId));
|
||
}
|
||
|
||
/**
|
||
* 新增年级
|
||
*/
|
||
@PostMapping
|
||
@Log(title = "年级管理", businessType = BusinessType.INSERT)
|
||
public AjaxResult add(@Validated @RequestBody Grade grade) {
|
||
// 检查名称是否重复
|
||
if (!gradeService.checkGradeNameUnique(grade)) {
|
||
return error("年级名称已存在");
|
||
}
|
||
grade.setCreateBy(getUsername());
|
||
return toAjax(gradeService.insertGrade(grade));
|
||
}
|
||
|
||
/**
|
||
* 修改年级
|
||
*/
|
||
@PutMapping
|
||
@Log(title = "年级管理", businessType = BusinessType.UPDATE)
|
||
public AjaxResult edit(@Validated @RequestBody Grade grade) {
|
||
if (!gradeService.checkGradeNameUnique(grade)) {
|
||
return error("年级名称已存在");
|
||
}
|
||
grade.setUpdateBy(getUsername());
|
||
return toAjax(gradeService.updateGrade(grade));
|
||
}
|
||
|
||
/**
|
||
* 删除年级
|
||
*/
|
||
@DeleteMapping("/{gradeId}")
|
||
@Log(title = "年级管理", businessType = BusinessType.DELETE)
|
||
public AjaxResult remove(@PathVariable Long gradeId) {
|
||
// 检查是否被学校引用
|
||
if (gradeService.checkGradeExistSchool(gradeId)) {
|
||
return error("该年级已被学校使用,不能删除");
|
||
}
|
||
return toAjax(gradeService.deleteGradeById(gradeId));
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 5.1.2 区域管理Controller
|
||
|
||
```java
|
||
/**
|
||
* 区域管理Controller
|
||
* @author pangu
|
||
*/
|
||
@RestController
|
||
@RequestMapping("/api/region")
|
||
public class RegionController extends BaseController {
|
||
|
||
@Autowired
|
||
private IRegionService regionService;
|
||
|
||
/**
|
||
* 获取区域树
|
||
*/
|
||
@GetMapping("/tree")
|
||
public AjaxResult tree() {
|
||
List<Region> list = regionService.selectRegionTree();
|
||
return success(list);
|
||
}
|
||
|
||
/**
|
||
* 获取区域详情
|
||
*/
|
||
@GetMapping("/{regionId}")
|
||
public AjaxResult getInfo(@PathVariable Long regionId) {
|
||
return success(regionService.selectRegionById(regionId));
|
||
}
|
||
|
||
/**
|
||
* 新增区域
|
||
*/
|
||
@PostMapping
|
||
@Log(title = "区域管理", businessType = BusinessType.INSERT)
|
||
public AjaxResult add(@Validated @RequestBody Region region) {
|
||
region.setCreateBy(getUsername());
|
||
return toAjax(regionService.insertRegion(region));
|
||
}
|
||
|
||
/**
|
||
* 修改区域
|
||
*/
|
||
@PutMapping
|
||
@Log(title = "区域管理", businessType = BusinessType.UPDATE)
|
||
public AjaxResult edit(@Validated @RequestBody Region region) {
|
||
// 父级不能设为自己或自己的子级
|
||
if (region.getParentId().equals(region.getRegionId())) {
|
||
return error("父级区域不能选择自己");
|
||
}
|
||
region.setUpdateBy(getUsername());
|
||
return toAjax(regionService.updateRegion(region));
|
||
}
|
||
|
||
/**
|
||
* 删除区域
|
||
*/
|
||
@DeleteMapping("/{regionId}")
|
||
@Log(title = "区域管理", businessType = BusinessType.DELETE)
|
||
public AjaxResult remove(@PathVariable Long regionId) {
|
||
// 检查是否有子区域
|
||
if (regionService.hasChildRegion(regionId)) {
|
||
return error("存在下级区域,不能删除");
|
||
}
|
||
// 检查是否被学校/会员引用
|
||
if (regionService.checkRegionExistSchool(regionId)) {
|
||
return error("该区域已被学校使用,不能删除");
|
||
}
|
||
if (regionService.checkRegionExistMember(regionId)) {
|
||
return error("该区域已被会员使用,不能删除");
|
||
}
|
||
return toAjax(regionService.deleteRegionById(regionId));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 Service层设计
|
||
|
||
#### 5.2.1 年级管理Service
|
||
|
||
```java
|
||
/**
|
||
* 年级管理Service接口
|
||
* @author pangu
|
||
*/
|
||
public interface IGradeService {
|
||
|
||
/**
|
||
* 查询年级分页列表
|
||
*/
|
||
List<Grade> selectGradeList(Grade grade);
|
||
|
||
/**
|
||
* 查询年级选项列表
|
||
*/
|
||
List<Grade> selectGradeOptions();
|
||
|
||
/**
|
||
* 查询年级详情
|
||
*/
|
||
Grade selectGradeById(Long gradeId);
|
||
|
||
/**
|
||
* 新增年级
|
||
*/
|
||
int insertGrade(Grade grade);
|
||
|
||
/**
|
||
* 修改年级
|
||
*/
|
||
int updateGrade(Grade grade);
|
||
|
||
/**
|
||
* 删除年级
|
||
*/
|
||
int deleteGradeById(Long gradeId);
|
||
|
||
/**
|
||
* 校验年级名称是否唯一
|
||
*/
|
||
boolean checkGradeNameUnique(Grade grade);
|
||
|
||
/**
|
||
* 检查年级是否被学校使用
|
||
*/
|
||
boolean checkGradeExistSchool(Long gradeId);
|
||
}
|
||
```
|
||
|
||
```java
|
||
/**
|
||
* 年级管理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位序号
|
||
*/
|
||
private String generateGradeCode() {
|
||
String maxCode = gradeMapper.selectMaxGradeCode();
|
||
if (StringUtils.isEmpty(maxCode)) {
|
||
return "GRD001";
|
||
}
|
||
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.2.2 区域管理Service
|
||
|
||
```java
|
||
/**
|
||
* 区域管理Service接口
|
||
* @author pangu
|
||
*/
|
||
public interface IRegionService {
|
||
|
||
/**
|
||
* 查询区域树
|
||
*/
|
||
List<Region> selectRegionTree();
|
||
|
||
/**
|
||
* 查询区域详情
|
||
*/
|
||
Region selectRegionById(Long regionId);
|
||
|
||
/**
|
||
* 新增区域
|
||
*/
|
||
int insertRegion(Region region);
|
||
|
||
/**
|
||
* 修改区域
|
||
*/
|
||
int updateRegion(Region region);
|
||
|
||
/**
|
||
* 删除区域
|
||
*/
|
||
int deleteRegionById(Long regionId);
|
||
|
||
/**
|
||
* 是否存在子区域
|
||
*/
|
||
boolean hasChildRegion(Long regionId);
|
||
|
||
/**
|
||
* 检查区域是否被学校使用
|
||
*/
|
||
boolean checkRegionExistSchool(Long regionId);
|
||
}
|
||
```
|
||
|
||
```java
|
||
/**
|
||
* 区域管理Service实现
|
||
* @author pangu
|
||
*/
|
||
@Service
|
||
public class RegionServiceImpl implements IRegionService {
|
||
|
||
@Autowired
|
||
private RegionMapper regionMapper;
|
||
|
||
@Autowired
|
||
private SchoolMapper schoolMapper;
|
||
|
||
@Autowired
|
||
private RedisCache redisCache;
|
||
|
||
private static final String REGION_TREE_KEY = "region_tree";
|
||
|
||
@Override
|
||
public List<Region> selectRegionTree() {
|
||
// 优先从缓存获取
|
||
List<Region> cacheList = redisCache.getCacheObject(REGION_TREE_KEY);
|
||
if (cacheList != null) {
|
||
return cacheList;
|
||
}
|
||
|
||
// 查询所有区域
|
||
List<Region> regionList = regionMapper.selectRegionList(new Region());
|
||
// 构建树形结构
|
||
List<Region> tree = buildRegionTree(regionList);
|
||
|
||
// 放入缓存(24小时)
|
||
redisCache.setCacheObject(REGION_TREE_KEY, tree, 24, TimeUnit.HOURS);
|
||
|
||
return tree;
|
||
}
|
||
|
||
/**
|
||
* 构建区域树
|
||
*/
|
||
private List<Region> buildRegionTree(List<Region> regionList) {
|
||
List<Region> rootList = new ArrayList<>();
|
||
Map<Long, Region> regionMap = regionList.stream()
|
||
.collect(Collectors.toMap(Region::getRegionId, r -> r));
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
return rootList;
|
||
}
|
||
|
||
@Override
|
||
public int insertRegion(Region region) {
|
||
// 设置层级和祖级列表
|
||
if (region.getParentId() == 0) {
|
||
region.setLevel(1);
|
||
region.setAncestors("0");
|
||
} else {
|
||
Region parent = regionMapper.selectRegionById(region.getParentId());
|
||
region.setLevel(parent.getLevel() + 1);
|
||
region.setAncestors(parent.getAncestors() + "," + parent.getRegionId());
|
||
}
|
||
|
||
// 生成区域编码
|
||
region.setRegionCode(generateRegionCode(region));
|
||
region.setCreateTime(DateUtils.getNowDate());
|
||
|
||
int rows = regionMapper.insertRegion(region);
|
||
|
||
// 清除缓存
|
||
if (rows > 0) {
|
||
redisCache.deleteObject(REGION_TREE_KEY);
|
||
}
|
||
|
||
return rows;
|
||
}
|
||
|
||
/**
|
||
* 生成区域编码
|
||
*/
|
||
private String generateRegionCode(Region region) {
|
||
if (region.getParentId() == 0) {
|
||
// 省级:REG + 2位序号
|
||
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 {
|
||
// 市/区级:父编码 + 2位序号
|
||
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) {
|
||
redisCache.deleteObject(REGION_TREE_KEY);
|
||
}
|
||
|
||
return rows;
|
||
}
|
||
|
||
@Override
|
||
public int deleteRegionById(Long regionId) {
|
||
int rows = regionMapper.deleteRegionById(regionId);
|
||
|
||
// 清除缓存
|
||
if (rows > 0) {
|
||
redisCache.deleteObject(REGION_TREE_KEY);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 Mapper层设计
|
||
|
||
#### 5.3.1 年级Mapper
|
||
|
||
```java
|
||
/**
|
||
* 年级Mapper接口
|
||
* @author pangu
|
||
*/
|
||
public interface GradeMapper {
|
||
|
||
/**
|
||
* 查询年级列表
|
||
*/
|
||
List<Grade> selectGradeList(Grade grade);
|
||
|
||
/**
|
||
* 查询年级详情
|
||
*/
|
||
Grade selectGradeById(Long gradeId);
|
||
|
||
/**
|
||
* 新增年级
|
||
*/
|
||
int insertGrade(Grade grade);
|
||
|
||
/**
|
||
* 修改年级
|
||
*/
|
||
int updateGrade(Grade grade);
|
||
|
||
/**
|
||
* 删除年级(软删除)
|
||
*/
|
||
int deleteGradeById(Long gradeId);
|
||
|
||
/**
|
||
* 校验年级名称唯一
|
||
*/
|
||
Grade checkGradeNameUnique(String gradeName);
|
||
|
||
/**
|
||
* 查询最大编码
|
||
*/
|
||
String selectMaxGradeCode();
|
||
}
|
||
```
|
||
|
||
```xml
|
||
<!-- GradeMapper.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="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="selectGradeVo">
|
||
select grade_id, grade_code, grade_name, order_num, status,
|
||
create_by, create_time, update_by, update_time
|
||
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="status != null and status != ''">
|
||
AND status = #{status}
|
||
</if>
|
||
order by order_num
|
||
</select>
|
||
|
||
<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,
|
||
create_by, create_time, del_flag
|
||
) values (
|
||
#{gradeCode}, #{gradeName}, #{orderNum}, #{status},
|
||
#{createBy}, #{createTime}, '0'
|
||
)
|
||
</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>
|
||
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>
|
||
```
|
||
|
||
### 5.4 实体类设计
|
||
|
||
```java
|
||
/**
|
||
* 年级实体类
|
||
* @author pangu
|
||
*/
|
||
@Data
|
||
@TableName("pg_grade")
|
||
public class Grade extends BaseEntity {
|
||
|
||
/** 年级ID */
|
||
@TableId(type = IdType.AUTO)
|
||
private Long gradeId;
|
||
|
||
/** 年级编码 */
|
||
@NotBlank(message = "年级编码不能为空")
|
||
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;
|
||
}
|
||
```
|
||
|
||
```java
|
||
/**
|
||
* 区域实体类
|
||
* @author pangu
|
||
*/
|
||
@Data
|
||
@TableName("pg_region")
|
||
public class Region extends BaseEntity {
|
||
|
||
/** 区域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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 数据库设计
|
||
|
||
### 6.1 表结构
|
||
|
||
参考《数据库设计文档_v1.0.md》中的:
|
||
- `pg_grade` - 年级字典表
|
||
- `pg_class` - 班级字典表
|
||
- `pg_subject` - 学科表
|
||
- `pg_region` - 区域表
|
||
|
||
### 6.2 初始化数据
|
||
|
||
#### 6.2.1 年级数据
|
||
|
||
```sql
|
||
INSERT INTO pg_grade (grade_id, grade_code, grade_name, order_num, status, create_time, del_flag) VALUES
|
||
(1, 'GRD001', '一年级', 1, '0', NOW(), '0'),
|
||
(2, 'GRD002', '二年级', 2, '0', NOW(), '0'),
|
||
(3, 'GRD003', '三年级', 3, '0', NOW(), '0'),
|
||
(4, 'GRD004', '四年级', 4, '0', NOW(), '0'),
|
||
(5, 'GRD005', '五年级', 5, '0', NOW(), '0'),
|
||
(6, 'GRD006', '六年级', 6, '0', NOW(), '0'),
|
||
(7, 'GRD007', '七年级', 7, '0', NOW(), '0'),
|
||
(8, 'GRD008', '八年级', 8, '0', NOW(), '0'),
|
||
(9, 'GRD009', '九年级', 9, '0', NOW(), '0'),
|
||
(10, 'GRD010', '高一', 10, '0', NOW(), '0'),
|
||
(11, 'GRD011', '高二', 11, '0', NOW(), '0'),
|
||
(12, 'GRD012', '高三', 12, '0', NOW(), '0');
|
||
```
|
||
|
||
#### 6.2.2 班级数据
|
||
|
||
```sql
|
||
INSERT INTO pg_class (class_id, class_code, class_name, order_num, status, create_time, del_flag) VALUES
|
||
(1, 'CLS001', '1班', 1, '0', NOW(), '0'),
|
||
(2, 'CLS002', '2班', 2, '0', NOW(), '0'),
|
||
(3, 'CLS003', '3班', 3, '0', NOW(), '0'),
|
||
(4, 'CLS004', '4班', 4, '0', NOW(), '0'),
|
||
(5, 'CLS005', '5班', 5, '0', NOW(), '0'),
|
||
(6, 'CLS006', '6班', 6, '0', NOW(), '0'),
|
||
(7, 'CLS007', '7班', 7, '0', NOW(), '0'),
|
||
(8, 'CLS008', '8班', 8, '0', NOW(), '0'),
|
||
(9, 'CLS009', '9班', 9, '0', NOW(), '0'),
|
||
(10, 'CLS010', '10班', 10, '0', NOW(), '0');
|
||
```
|
||
|
||
#### 6.2.3 学科数据
|
||
|
||
```sql
|
||
INSERT INTO pg_subject (subject_id, subject_code, subject_name, order_num, status, create_time, del_flag) VALUES
|
||
(1, 'SUB001', '语文', 1, '0', NOW(), '0'),
|
||
(2, 'SUB002', '数学', 2, '0', NOW(), '0'),
|
||
(3, 'SUB003', '英语', 3, '0', NOW(), '0'),
|
||
(4, 'SUB004', '物理', 4, '0', NOW(), '0'),
|
||
(5, 'SUB005', '化学', 5, '0', NOW(), '0'),
|
||
(6, 'SUB006', '生物', 6, '0', NOW(), '0'),
|
||
(7, 'SUB007', '历史', 7, '0', NOW(), '0'),
|
||
(8, 'SUB008', '地理', 8, '0', NOW(), '0'),
|
||
(9, 'SUB009', '政治', 9, '0', NOW(), '0'),
|
||
(10, 'SUB010', '体育', 10, '0', NOW(), '0'),
|
||
(11, 'SUB011', '音乐', 11, '0', NOW(), '0'),
|
||
(12, 'SUB012', '美术', 12, '0', NOW(), '0');
|
||
```
|
||
|
||
#### 6.2.4 区域数据
|
||
|
||
```sql
|
||
-- 省级
|
||
INSERT INTO pg_region (region_id, parent_id, region_name, region_code, level, ancestors, order_num, status, create_time, del_flag) VALUES
|
||
(1, 0, '湖北', 'REG01', 1, '0', 1, '0', NOW(), '0'),
|
||
(2, 0, '北京', 'REG02', 1, '0', 2, '0', NOW(), '0'),
|
||
(3, 0, '香港', 'REG03', 1, '0', 3, '0', NOW(), '0'),
|
||
(4, 0, '吉宁', 'REG04', 1, '0', 4, '0', NOW(), '0');
|
||
|
||
-- 市级(湖北)
|
||
INSERT INTO pg_region (region_id, parent_id, region_name, region_code, level, ancestors, order_num, status, create_time, del_flag) VALUES
|
||
(11, 1, '武汉', 'REG0101', 2, '0,1', 1, '0', NOW(), '0'),
|
||
(12, 1, '黄冈', 'REG0102', 2, '0,1', 2, '0', NOW(), '0');
|
||
|
||
-- 区级(武汉)
|
||
INSERT INTO pg_region (region_id, parent_id, region_name, region_code, level, ancestors, order_num, status, create_time, del_flag) VALUES
|
||
(111, 11, '武昌区', 'REG010101', 3, '0,1,11', 1, '0', NOW(), '0'),
|
||
(112, 11, '汉口区', 'REG010102', 3, '0,1,11', 2, '0', NOW(), '0'),
|
||
(113, 11, '汉阳区', 'REG010103', 3, '0,1,11', 3, '0', NOW(), '0'),
|
||
(114, 11, '江夏区', 'REG010104', 3, '0,1,11', 4, '0', NOW(), '0'),
|
||
(115, 11, '新洲区', 'REG010105', 3, '0,1,11', 5, '0', NOW(), '0'),
|
||
(116, 11, '黄陂区', 'REG010106', 3, '0,1,11', 6, '0', NOW(), '0');
|
||
|
||
-- 区级(黄冈)
|
||
INSERT INTO pg_region (region_id, parent_id, region_name, region_code, level, ancestors, order_num, status, create_time, del_flag) VALUES
|
||
(121, 12, '黄州区', 'REG010201', 3, '0,1,12', 1, '0', NOW(), '0'),
|
||
(122, 12, '红安县', 'REG010202', 3, '0,1,12', 2, '0', NOW(), '0'),
|
||
(123, 12, '麻城市', 'REG010203', 3, '0,1,12', 3, '0', NOW(), '0');
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 接口设计
|
||
|
||
### 7.1 接口清单
|
||
|
||
#### 7.1.1 年级管理接口
|
||
|
||
| 接口地址 | 方法 | 说明 |
|
||
| -------------------- | ------ | -------- |
|
||
| GET /api/grade/list | GET | 获取年级分页列表 |
|
||
| GET /api/grade/options | GET | 获取年级选项列表 |
|
||
| GET /api/grade/{id} | GET | 获取年级详情 |
|
||
| POST /api/grade | POST | 新增年级 |
|
||
| PUT /api/grade | PUT | 修改年级 |
|
||
| DELETE /api/grade/{id} | DELETE | 删除年级 |
|
||
|
||
#### 7.1.2 班级管理接口
|
||
|
||
| 接口地址 | 方法 | 说明 |
|
||
| -------------------- | ------ | -------- |
|
||
| GET /api/class/list | GET | 获取班级分页列表 |
|
||
| GET /api/class/options | GET | 获取班级选项列表 |
|
||
| GET /api/class/{id} | GET | 获取班级详情 |
|
||
| POST /api/class | POST | 新增班级 |
|
||
| PUT /api/class | PUT | 修改班级 |
|
||
| DELETE /api/class/{id} | DELETE | 删除班级 |
|
||
|
||
#### 7.1.3 学科管理接口
|
||
|
||
| 接口地址 | 方法 | 说明 |
|
||
| ---------------------- | ------ | -------- |
|
||
| GET /api/subject/list | GET | 获取学科分页列表 |
|
||
| GET /api/subject/options | GET | 获取学科选项列表 |
|
||
| GET /api/subject/{id} | GET | 获取学科详情 |
|
||
| POST /api/subject | POST | 新增学科 |
|
||
| PUT /api/subject | PUT | 修改学科 |
|
||
| DELETE /api/subject/{id} | DELETE | 删除学科 |
|
||
|
||
#### 7.1.4 区域管理接口
|
||
|
||
| 接口地址 | 方法 | 说明 |
|
||
| --------------------- | ------ | ------ |
|
||
| GET /api/region/tree | GET | 获取区域树 |
|
||
| GET /api/region/{id} | GET | 获取区域详情 |
|
||
| POST /api/region | POST | 新增区域 |
|
||
| PUT /api/region | PUT | 修改区域 |
|
||
| DELETE /api/region/{id} | DELETE | 删除区域 |
|
||
|
||
### 7.2 接口详情
|
||
|
||
#### 7.2.1 获取年级分页列表
|
||
|
||
**请求**
|
||
|
||
```
|
||
GET /api/grade/list?gradeName=年级&status=0&pageNum=1&pageSize=10
|
||
```
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"msg": "查询成功",
|
||
"total": 12,
|
||
"rows": [
|
||
{
|
||
"gradeId": 1,
|
||
"gradeCode": "GRD001",
|
||
"gradeName": "一年级",
|
||
"orderNum": 1,
|
||
"status": "0",
|
||
"createTime": "2026-01-01 10:00:00"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 7.2.2 新增年级
|
||
|
||
**请求**
|
||
|
||
```
|
||
POST /api/grade
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"gradeName": "学前班",
|
||
"orderNum": 0,
|
||
"status": "0"
|
||
}
|
||
```
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"msg": "新增成功"
|
||
}
|
||
```
|
||
|
||
#### 7.2.3 删除年级(被引用时)
|
||
|
||
**请求**
|
||
|
||
```
|
||
DELETE /api/grade/1
|
||
```
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 500,
|
||
"msg": "该年级已被学校使用,不能删除"
|
||
}
|
||
```
|
||
|
||
#### 7.2.4 获取区域树
|
||
|
||
**请求**
|
||
|
||
```
|
||
GET /api/region/tree
|
||
```
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"msg": "查询成功",
|
||
"data": [
|
||
{
|
||
"regionId": 1,
|
||
"parentId": 0,
|
||
"regionName": "湖北",
|
||
"regionCode": "REG01",
|
||
"level": 1,
|
||
"children": [
|
||
{
|
||
"regionId": 11,
|
||
"parentId": 1,
|
||
"regionName": "武汉",
|
||
"regionCode": "REG0101",
|
||
"level": 2,
|
||
"children": [
|
||
{
|
||
"regionId": 111,
|
||
"parentId": 11,
|
||
"regionName": "武昌区",
|
||
"regionCode": "REG010101",
|
||
"level": 3,
|
||
"children": null
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 开发计划
|
||
|
||
### 8.1 阶段划分
|
||
|
||
| 阶段 | 任务 | 工时 | 交付物 |
|
||
| ---- | -------------------- | ----- | ------------------ |
|
||
| 阶段一 | 数据库表创建、初始化数据 | 0.5天 | SQL脚本 |
|
||
| 阶段二 | 后端接口开发(年级、班级、学科) | 1天 | Controller/Service |
|
||
| 阶段三 | 后端接口开发(区域管理) | 0.5天 | Controller/Service |
|
||
| 阶段四 | 前端页面开发(年级、班级、学科) | 1天 | Vue页面 |
|
||
| 阶段五 | 前端页面开发(区域管理-树形表格) | 0.5天 | Vue页面 |
|
||
| 阶段六 | 联调测试、Bug修复 | 0.5天 | 测试报告 |
|
||
| **合计** | | **4天** | |
|
||
|
||
### 8.2 里程碑
|
||
|
||
| 里程碑 | 时间节点 | 交付内容 |
|
||
| ---- | ---------- | ---------------- |
|
||
| M1 | 第1天结束 | 数据库就绪、后端接口完成 |
|
||
| M2 | 第3天结束 | 前端页面完成 |
|
||
| M3 | 第4天结束 | 联调测试完成,功能上线 |
|
||
|
||
### 8.3 任务分解
|
||
|
||
#### 阶段一:数据库(0.5天)
|
||
|
||
- [x] 创建 pg_grade 表
|
||
- [x] 创建 pg_class 表
|
||
- [x] 创建 pg_subject 表
|
||
- [x] 创建 pg_region 表
|
||
- [x] 初始化年级数据(12条)
|
||
- [x] 初始化班级数据(10条)
|
||
- [x] 初始化学科数据(12条)
|
||
- [x] 初始化区域数据(湖北省数据)
|
||
|
||
#### 阶段二:后端-年级/班级/学科(1天)
|
||
|
||
- [ ] Grade 实体类
|
||
- [ ] GradeMapper + XML
|
||
- [ ] GradeService 接口及实现
|
||
- [ ] GradeController(含权限注解)
|
||
- [ ] Class 实体类
|
||
- [ ] ClassMapper + XML
|
||
- [ ] ClassService 接口及实现
|
||
- [ ] ClassController
|
||
- [ ] Subject 实体类
|
||
- [ ] SubjectMapper + XML
|
||
- [ ] SubjectService 接口及实现
|
||
- [ ] SubjectController
|
||
- [ ] 单元测试
|
||
|
||
#### 阶段三:后端-区域管理(0.5天)
|
||
|
||
- [ ] Region 实体类
|
||
- [ ] RegionMapper + XML
|
||
- [ ] RegionService 接口及实现(含缓存)
|
||
- [ ] RegionController
|
||
- [ ] 单元测试
|
||
|
||
#### 阶段四:前端-年级/班级/学科(1天)
|
||
|
||
- [ ] grade API 封装
|
||
- [ ] grade Mock 数据
|
||
- [ ] grade/index.vue 页面
|
||
- [ ] class API 封装
|
||
- [ ] class Mock 数据
|
||
- [ ] class/index.vue 页面
|
||
- [ ] subject API 封装
|
||
- [ ] subject Mock 数据
|
||
- [ ] subject/index.vue 页面
|
||
- [ ] 路由配置
|
||
|
||
#### 阶段五:前端-区域管理(0.5天)
|
||
|
||
- [ ] region API 封装
|
||
- [ ] region Mock 数据
|
||
- [ ] region/index.vue 页面(树形表格)
|
||
- [ ] 展开/折叠全部功能
|
||
- [ ] 新增下级功能
|
||
|
||
#### 阶段六:联调测试(0.5天)
|
||
|
||
- [ ] 前后端联调
|
||
- [ ] 功能测试
|
||
- [ ] Bug修复
|
||
- [ ] 代码Review
|
||
|
||
---
|
||
|
||
## 9. 测试方案
|
||
|
||
### 9.1 测试范围
|
||
|
||
| 测试类型 | 测试内容 |
|
||
| ----- | ------------------ |
|
||
| 单元测试 | Service层核心方法 |
|
||
| 接口测试 | 所有API接口 |
|
||
| 功能测试 | 前端页面操作 |
|
||
| 边界测试 | 删除被引用数据、名称重复等 |
|
||
| 性能测试 | 区域树查询响应时间 |
|
||
|
||
### 9.2 测试用例
|
||
|
||
#### 9.2.1 年级管理测试用例
|
||
|
||
| 用例编号 | 测试场景 | 前置条件 | 测试步骤 | 预期结果 |
|
||
| ---------- | ------------ | -------- | ----------------- | ----------------- |
|
||
| TC-GRD-001 | 新增年级-正常 | 登录后台 | 输入年级名称,点击确定 | 新增成功,列表显示新数据 |
|
||
| TC-GRD-002 | 新增年级-名称重复 | 已存在"一年级" | 新增"一年级" | 提示"年级名称已存在" |
|
||
| TC-GRD-003 | 编辑年级-正常 | 存在年级数据 | 修改名称,点击确定 | 修改成功 |
|
||
| TC-GRD-004 | 删除年级-未被引用 | 年级未被学校使用 | 点击删除,确认 | 删除成功 |
|
||
| TC-GRD-005 | 删除年级-被引用 | 年级已被学校使用 | 点击删除,确认 | 提示"该年级已被学校使用,不能删除" |
|
||
| TC-GRD-006 | 搜索年级-按名称 | 存在多条数据 | 输入关键字搜索 | 返回匹配数据 |
|
||
| TC-GRD-007 | 搜索年级-按状态 | 存在启用/禁用 | 选择状态筛选 | 返回对应状态数据 |
|
||
|
||
#### 9.2.2 区域管理测试用例
|
||
|
||
| 用例编号 | 测试场景 | 前置条件 | 测试步骤 | 预期结果 |
|
||
| ---------- | ---------- | -------- | ------------- | ----------------- |
|
||
| TC-REG-001 | 查看区域树 | 存在区域数据 | 进入区域管理页面 | 显示树形结构 |
|
||
| TC-REG-002 | 展开/折叠全部 | 存在多级区域 | 点击展开全部 | 全部节点展开 |
|
||
| TC-REG-003 | 新增省级区域 | 登录后台 | 点击新增,输入名称 | 新增成功 |
|
||
| TC-REG-004 | 新增下级区域 | 存在父区域 | 点击新增下级 | 弹窗自动带入父级 |
|
||
| TC-REG-005 | 删除区域-有子级 | 区域有子节点 | 点击删除 | 提示"存在下级区域,不能删除" |
|
||
| TC-REG-006 | 删除区域-被学校引用 | 区域被学校使用 | 点击删除 | 提示"该区域已被学校使用,不能删除" |
|
||
| TC-REG-007 | 删除区域-正常 | 区域无子级无引用 | 点击删除,确认 | 删除成功 |
|
||
|
||
### 9.3 性能测试
|
||
|
||
| 测试项 | 测试方法 | 指标要求 |
|
||
| --------- | ------------------- | ---------- |
|
||
| 区域树查询 | 模拟100条区域数据 | 响应时间 ≤ 200ms |
|
||
| 区域树缓存命中 | 连续请求区域树 | 第二次 ≤ 50ms |
|
||
| 年级列表分页查询 | 模拟1000条数据 | 响应时间 ≤ 200ms |
|
||
|
||
---
|
||
|
||
## 10. 风险与应对
|
||
|
||
### 10.1 技术风险
|
||
|
||
| 风险点 | 影响程度 | 应对措施 |
|
||
| ------- | ---- | -------------------- |
|
||
| 区域树性能问题 | 中 | 使用Redis缓存,设置合理过期时间 |
|
||
| 数据引用关系复杂 | 中 | 删除前严格检查,提供友好的错误提示 |
|
||
| 编码生成冲突 | 低 | 使用数据库max函数 + 锁机制保证唯一 |
|
||
|
||
### 10.2 业务风险
|
||
|
||
| 风险点 | 影响程度 | 应对措施 |
|
||
| -------- | ---- | --------------- |
|
||
| 初始数据不完整 | 中 | 与业务方确认初始数据范围 |
|
||
| 权限控制遗漏 | 高 | 严格按照权限矩阵实现,测试验证 |
|
||
|
||
### 10.3 进度风险
|
||
|
||
| 风险点 | 影响程度 | 应对措施 |
|
||
| ------- | ---- | ------------- |
|
||
| 需求变更 | 中 | 预留缓冲时间 |
|
||
| 联调问题多 | 中 | 尽早进行前后端联调 |
|
||
|
||
---
|
||
|
||
## 审核意见
|
||
|
||
| 评审角色 | 评审人 | 评审日期 | 评审意见 | 签字 |
|
||
| ---- | --- | ---- | ---- | -- |
|
||
| 技术负责人 | | | | |
|
||
| 前端负责人 | | | | |
|
||
| 后端负责人 | | | | |
|
||
| 测试负责人 | | | | |
|
||
|
||
---
|
||
|
||
*文档结束*
|