# 盘古用户平台 - 基础数据模块技术方案

---

| 文档信息     | 内容                                |
| -------- | --------------------------------- |
| **文档版本** | 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 进度风险

| 风险点     | 影响程度 | 应对措施          |
| ------- | ---- | ------------- |
| 需求变更    | 中    | 预留缓冲时间        |
| 联调问题多   | 中    | 尽早进行前后端联调     |

---

## 审核意见

| 评审角色 | 评审人 | 评审日期 | 评审意见 | 签字 |
| ---- | --- | ---- | ---- | -- |
| 技术负责人 |     |      |      |    |
| 前端负责人 |     |      |      |    |
| 后端负责人 |     |      |      |    |
| 测试负责人 |     |      |      |    |

---

*文档结束*
