# 学校管理模块 - 开发任务清单

---

| 文档信息 | 内容 |
|---------|------|
| **文档版本** | V1.0 |
| **模块名称** | 学校管理模块 |
| **编写团队** | pangu |
| **创建日期** | 2026-01-31 |

---

## 任务概览

| 类别 | 任务数 | 预估工时 | 状态 |
|-----|:------:|:------:|:----:|
| 后端开发 | 14 | 25h | 待开始 |
| 前端开发 | 10 | 23h | 待开始 |
| 测试 | 3 | 8h | 待开始 |
| **合计** | **27** | **56h** | - |

---

## 一、后端开发任务

### 1.1 实体层开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| BE-SCH-01 | 创建实体类 | 创建 School、SchoolGrade、SchoolClass 实体类 | P0 | 1h | ☐ |

**BE-SCH-01 详细说明**

创建以下文件：

```
domain/entity/
├── School.java              # 学校实体
├── SchoolGrade.java         # 学校年级关联实体
└── SchoolClass.java         # 学校班级关联实体

domain/dto/
├── SchoolQueryDTO.java      # 学校查询DTO
├── SchoolCreateDTO.java     # 学校新增DTO
├── BindGradesDTO.java       # 年级挂载DTO
└── BindClassesDTO.java      # 班级挂载DTO

domain/vo/
├── SchoolVO.java            # 学校VO
└── SchoolTreeVO.java        # 学校树VO
```

**验收标准**
- [ ] 所有实体类包含表中所有字段
- [ ] DTO包含必要的验证注解（@NotNull, @NotBlank等）
- [ ] VO符合接口响应规范

---

### 1.2 Mapper层开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| BE-SCH-02 | SchoolMapper开发 | 创建学校Mapper接口及XML | P0 | 2h | ☐ |
| BE-SCH-03 | SchoolGradeMapper开发 | 创建学校年级Mapper接口及XML | P0 | 1h | ☐ |
| BE-SCH-04 | SchoolClassMapper开发 | 创建学校班级Mapper接口及XML | P0 | 1h | ☐ |

**BE-SCH-02 详细说明 - SchoolMapper**

```java
public interface SchoolMapper extends BaseMapper<School> {
    
    /**
     * 按区域查询学校列表
     */
    List<School> selectSchoolsByRegionId(@Param("regionId") Long regionId);
    
    /**
     * 查询学校列表（带数据权限）
     */
    List<SchoolVO> selectSchoolList(SchoolQueryDTO query);
    
    /**
     * 查询当前年份最大编码
     */
    String selectMaxCode(@Param("prefix") String prefix);
    
    /**
     * 统计学校数量（按区域）
     */
    int countByRegionId(@Param("regionId") Long regionId);
}
```

**BE-SCH-03 详细说明 - SchoolGradeMapper**

```java
public interface SchoolGradeMapper extends BaseMapper<SchoolGrade> {
    
    /**
     * 按学校ID批量查询年级
     */
    List<SchoolGrade> selectBySchoolIds(@Param("schoolIds") List<Long> schoolIds);
    
    /**
     * 检查是否已存在
     */
    boolean exists(@Param("schoolId") Long schoolId, @Param("gradeId") Long gradeId);
    
    /**
     * 统计学校下的年级数量
     */
    int countBySchoolId(@Param("schoolId") Long schoolId);
}
```

**BE-SCH-04 详细说明 - SchoolClassMapper**

```java
public interface SchoolClassMapper extends BaseMapper<SchoolClass> {
    
    /**
     * 按学校年级ID批量查询班级
     */
    List<SchoolClass> selectBySchoolGradeIds(@Param("schoolGradeIds") List<Long> schoolGradeIds);
    
    /**
     * 检查是否已存在
     */
    boolean exists(@Param("schoolGradeId") Long schoolGradeId, @Param("classId") Long classId);
    
    /**
     * 统计年级下的班级数量
     */
    int countBySchoolGradeId(@Param("schoolGradeId") Long schoolGradeId);
}
```

**验收标准**
- [ ] 所有SQL语句已测试通过
- [ ] 软删除条件已正确添加（del_flag = '0'）
- [ ] 数据权限注解已添加

---

### 1.3 Service层开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| BE-SCH-05 | 创建Service接口 | 创建 ISchoolService 接口 | P0 | 1h | ☐ |
| BE-SCH-06 | 实现学校CRUD | 实现学校增删改查方法 | P0 | 3h | ☐ |
| BE-SCH-07 | 实现年级/班级挂载 | 实现 bindGrades、bindClasses 方法 | P0 | 2h | ☐ |
| BE-SCH-08 | 实现学校树查询 | 实现 selectSchoolTree 方法 | P0 | 2h | ☐ |
| BE-SCH-09 | 实现删除校验 | 实现删除前关联检查逻辑 | P0 | 2h | ☐ |
| BE-SCH-10 | 实现编码生成 | 实现学校编码自动生成逻辑 | P0 | 1h | ☐ |

**BE-SCH-06 详细说明 - 学校CRUD**

```java
// 查询方法
List<SchoolVO> selectSchoolList(SchoolQueryDTO query);
SchoolVO selectSchoolById(Long schoolId);

// 新增方法
int insertSchool(SchoolCreateDTO dto);
// 要点：
// 1. 调用 generateSchoolCode() 生成编码
// 2. 查询并设置 regionPath
// 3. 设置 createBy、createTime

// 修改方法
int updateSchool(School school);
// 要点：
// 1. 如果修改了regionId，需要更新regionPath
// 2. 设置 updateBy、updateTime

// 删除方法
int deleteSchool(Long schoolId);
// 要点：
// 1. 调用 deleteSchool 校验逻辑
// 2. 执行软删除（设置 del_flag = '1'）
```

**BE-SCH-08 详细说明 - 学校树查询**

```java
/**
 * 查询学校树形结构
 * 
 * 实现步骤：
 * 1. 根据 regionId 查询学校列表
 * 2. 提取所有 schoolId，批量查询学校年级关联
 * 3. 提取所有 schoolGradeId，批量查询学校班级关联
 * 4. 内存中组装三级树形结构
 * 
 * 性能优化：
 * - 使用批量查询减少数据库交互
 * - 使用 Map 进行分组，避免循环嵌套查询
 */
List<SchoolTreeVO> selectSchoolTree(Long regionId);
```

**BE-SCH-09 详细说明 - 删除校验**

```java
/**
 * 删除学校前校验
 * 
 * 校验规则：
 * 1. 检查是否有年级/班级子节点
 *    - 调用 schoolGradeMapper.countBySchoolId(schoolId)
 *    - 如果 > 0，抛出异常："该学校下存在年级数据，请先删除年级"
 * 
 * 2. 检查是否被学生信息引用
 *    - 调用 studentMapper.countBySchoolId(schoolId)
 *    - 如果 > 0，抛出异常："该学校已被学生信息引用，无法删除"
 */

/**
 * 删除年级前校验
 * 
 * 校验规则：
 * 1. 检查是否有班级子节点
 * 2. 检查是否被学生信息引用（school_grade_id）
 */

/**
 * 删除班级前校验
 * 
 * 校验规则：
 * 1. 检查是否被学生信息引用（school_class_id）
 */
```

**BE-SCH-10 详细说明 - 编码生成**

```java
/**
 * 生成学校编码
 * 
 * 格式：SCH + 年份(4位) + 序号(4位)
 * 示例：SCH20260001, SCH20260002
 * 
 * 实现步骤：
 * 1. 获取当前年份
 * 2. 拼接前缀 "SCH" + year
 * 3. 查询数据库中该前缀的最大编码
 * 4. 提取序号部分 +1
 * 5. 格式化为4位数字（不足补0）
 * 
 * 并发处理：
 * - 使用数据库唯一索引保证唯一性
 * - 冲突时重试生成
 */
private String generateSchoolCode() {
    String year = String.valueOf(LocalDate.now().getYear());
    String prefix = "SCH" + year;
    
    String maxCode = schoolMapper.selectMaxCode(prefix);
    
    int seq = 1;
    if (maxCode != null) {
        seq = Integer.parseInt(maxCode.substring(7)) + 1;
    }
    
    return prefix + String.format("%04d", seq);
}
```

**验收标准**
- [ ] 所有Service方法已实现并通过单元测试
- [ ] 事务注解已正确添加
- [ ] 异常信息清晰易懂

---

### 1.4 Controller层开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| BE-SCH-11 | 创建Controller | 创建 SchoolController 控制器 | P0 | 2h | ☐ |

**BE-SCH-11 详细说明**

```java
@RestController
@RequestMapping("/api/school")
public class SchoolController {
    
    // 接口列表
    GET  /tree                    # 获取学校树
    GET  /list                    # 获取学校列表（分页）
    GET  /{schoolId}              # 获取学校详情
    POST /                        # 新增学校
    PUT  /                        # 修改学校
    DELETE /{schoolId}            # 删除学校
    POST /bindGrades              # 挂载年级
    POST /bindClasses             # 挂载班级
    DELETE /grade/{schoolGradeId} # 删除学校年级
    DELETE /class/{schoolClassId} # 删除学校班级
}
```

**验收标准**
- [ ] 所有接口遵循RESTful规范
- [ ] 参数校验注解已添加
- [ ] 操作日志注解已添加（@Log）
- [ ] 权限注解已添加（@PreAuthorize）

---

### 1.5 权限与测试

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| BE-SCH-12 | 数据权限控制 | 实现分公司用户数据隔离 | P1 | 2h | ☐ |
| BE-SCH-13 | 单元测试 | 编写Service层单元测试 | P1 | 3h | ☐ |
| BE-SCH-14 | 接口联调 | 与前端联调并修复Bug | P1 | 2h | ☐ |

**BE-SCH-12 详细说明 - 数据权限**

```java
/**
 * 数据权限实现
 * 
 * 超级管理员：查看全部学校
 * 分公司用户：只能查看所属区域的学校
 * 
 * 实现方式：
 * 1. 在 selectSchoolList 方法上添加 @DataScope 注解
 * 2. SQL中关联区域表和部门表
 * 3. MyBatis拦截器自动拼接权限条件
 */

// Mapper.xml 示例
<select id="selectSchoolList" resultType="SchoolVO">
    SELECT s.*, r.region_name
    FROM pg_school s
    LEFT JOIN pg_region r ON s.region_id = r.region_id
    LEFT JOIN sys_dept d ON s.region_id = d.dept_id
    WHERE s.del_flag = '0'
    <if test="schoolName != null and schoolName != ''">
        AND s.school_name LIKE CONCAT('%', #{schoolName}, '%')
    </if>
    <if test="status != null and status != ''">
        AND s.status = #{status}
    </if>
    <!-- 数据权限会自动拼接在这里 -->
    ${params.dataScope}
</select>
```

**BE-SCH-13 详细说明 - 单元测试**

```java
// 测试用例清单
@Test void testInsertSchool()                    // 新增学校
@Test void testUpdateSchool()                    // 修改学校
@Test void testDeleteSchool_Success()            // 删除学校成功
@Test void testDeleteSchool_HasGrades()          // 删除学校失败-有年级
@Test void testDeleteSchool_HasStudents()        // 删除学校失败-有学生
@Test void testBindGrades()                      // 挂载年级
@Test void testBindGrades_Duplicate()            // 重复挂载年级
@Test void testBindClasses()                     // 挂载班级
@Test void testDeleteSchoolGrade_Success()       // 删除年级成功
@Test void testDeleteSchoolGrade_HasClasses()    // 删除年级失败-有班级
@Test void testDeleteSchoolClass_Success()       // 删除班级成功
@Test void testDeleteSchoolClass_HasStudents()   // 删除班级失败-有学生
@Test void testSelectSchoolTree()                // 查询学校树
@Test void testGenerateSchoolCode()              // 生成学校编码
```

---

## 二、前端开发任务

### 2.1 页面框架开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| FE-SCH-01 | 创建主页面框架 | 创建 school/index.vue 主页面 | P0 | 2h | ☐ |

**FE-SCH-01 详细说明**

```vue
<!-- 页面结构 -->
<div class="school-container">
  <!-- 左侧区域树：宽度250px，固定 -->
  <div class="region-tree-wrapper">
    <RegionTree />
  </div>
  
  <!-- 右侧内容区 -->
  <div class="main-content">
    <!-- 搜索栏 -->
    <el-form />
    
    <!-- 工具栏 -->
    <div class="toolbar">
      <el-button>新增学校</el-button>
    </div>
    
    <!-- 学校树表格 -->
    <SchoolTree />
  </div>
</div>

<!-- 样式 -->
.school-container {
  display: flex;
  height: calc(100vh - 84px);
}

.region-tree-wrapper {
  width: 250px;
  border-right: 1px solid #e6e6e6;
  padding: 20px;
  overflow-y: auto;
}

.main-content {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
```

**验收标准**
- [ ] 左右分栏布局正确
- [ ] 响应式滚动正常
- [ ] 加载状态显示

---

### 2.2 组件开发

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| FE-SCH-02 | RegionTree 组件 | 开发左侧区域树组件 | P0 | 2h | ☐ |
| FE-SCH-03 | SchoolTree 组件 | 开发右侧学校树表格组件 | P0 | 4h | ☐ |
| FE-SCH-04 | SchoolDialog 组件 | 开发新增/编辑学校弹窗 | P0 | 3h | ☐ |
| FE-SCH-05 | GradeSelectDialog 组件 | 开发年级选择弹窗 | P0 | 2h | ☐ |
| FE-SCH-06 | ClassSelectDialog 组件 | 开发班级选择弹窗 | P0 | 2h | ☐ |

**FE-SCH-02 详细说明 - RegionTree**

```vue
<!-- 组件功能 -->
1. 加载并展示区域树（省-市-区）
2. 默认展开第一级
3. 点击节点触发 node-click 事件
4. 高亮当前选中节点

<!-- 核心代码 -->
<el-tree
  :data="regionTree"
  :props="{ label: 'regionName', children: 'children' }"
  :default-expand-all="false"
  :expand-on-click-node="false"
  highlight-current
  node-key="regionId"
  @node-click="handleNodeClick"
/>
```

**FE-SCH-03 详细说明 - SchoolTree**

```vue
<!-- 组件功能 -->
1. 使用 el-table 的 row-key 和 tree-props 实现树形展示
2. 根据节点类型（school/grade/class）显示不同操作按钮
3. 支持展开/收起
4. 加载状态显示

<!-- 核心代码 -->
<el-table
  :data="schoolTreeData"
  row-key="id"
  :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
  default-expand-all
>
  <el-table-column prop="name" label="名称" />
  <el-table-column prop="code" label="编码" width="150">
    <template #default="{ row }">
      {{ row.type === 'school' ? row.code : '-' }}
    </template>
  </el-table-column>
  <!-- ... -->
  <el-table-column label="操作" width="250">
    <template #default="{ row }">
      <!-- 学校操作 -->
      <template v-if="row.type === 'school'">
        <el-button link @click="handleEdit(row)">编辑</el-button>
        <el-button link @click="handleAddGrade(row)">新增年级</el-button>
        <el-popconfirm @confirm="handleDelete(row)">
          <template #reference>
            <el-button link type="danger">删除</el-button>
          </template>
        </el-popconfirm>
      </template>
      <!-- 年级操作 -->
      <template v-else-if="row.type === 'grade'">
        <el-button link @click="handleAddClass(row)">新增班级</el-button>
        <el-popconfirm @confirm="handleDeleteGrade(row)">
          <template #reference>
            <el-button link type="danger">删除</el-button>
          </template>
        </el-popconfirm>
      </template>
      <!-- 班级操作 -->
      <template v-else>
        <el-popconfirm @confirm="handleDeleteClass(row)">
          <template #reference>
            <el-button link type="danger">删除</el-button>
          </template>
        </el-popconfirm>
      </template>
    </template>
  </el-table-column>
</el-table>
```

**FE-SCH-04 详细说明 - SchoolDialog**

```vue
<!-- 组件功能 -->
1. 支持新增和编辑两种模式
2. 新增时学校编码不显示（后端自动生成）
3. 所属区域使用级联选择器
4. 表单验证

<!-- 表单字段 -->
| 字段 | 组件 | 验证规则 |
|-----|------|---------|
| 学校名称 | el-input | 必填，最大100字符 |
| 学校编码 | el-input | 编辑时只读显示 |
| 学校类型 | el-select | 必填 |
| 所属区域 | el-cascader | 必填 |
| 详细地址 | el-input | 选填 |
| 联系人 | el-input | 选填 |
| 联系电话 | el-input | 选填，手机号格式 |
| 状态 | el-switch | 默认启用 |

<!-- 核心方法 -->
// 暴露给父组件的方法
defineExpose({
  open,      // 打开新增弹窗
  openEdit   // 打开编辑弹窗
})
```

**FE-SCH-05 详细说明 - GradeSelectDialog**

```vue
<!-- 组件功能 -->
1. 加载年级字典列表
2. 已挂载的年级禁用（disabled）
3. 支持多选（el-checkbox-group）
4. 确认后调用 bindGrades 接口

<!-- 核心代码 -->
<el-checkbox-group v-model="selectedGrades">
  <el-checkbox
    v-for="item in gradeList"
    :key="item.gradeId"
    :label="item.gradeId"
    :disabled="item.disabled"
  >
    {{ item.gradeName }}
    <el-tag v-if="item.disabled" size="small" type="info">已挂载</el-tag>
  </el-checkbox>
</el-checkbox-group>
```

**FE-SCH-06 详细说明 - ClassSelectDialog**

与 GradeSelectDialog 类似，调用 bindClasses 接口。

**验收标准**
- [ ] 组件功能完整
- [ ] Props/Events 定义清晰
- [ ] 加载状态处理
- [ ] 错误提示友好

---

### 2.3 接口与联调

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| FE-SCH-07 | 封装API接口 | 创建 api/school.js | P0 | 1h | ☐ |
| FE-SCH-08 | 主页面逻辑实现 | 实现主页面交互逻辑并联调 | P0 | 3h | ☐ |

**FE-SCH-07 详细说明 - API封装**

```javascript
// api/school.js

// 获取学校树
export function getSchoolTree(regionId)

// 获取学校列表
export function getSchoolList(query)

// 获取学校详情
export function getSchool(schoolId)

// 新增学校
export function addSchool(data)

// 修改学校
export function updateSchool(data)

// 删除学校
export function deleteSchool(schoolId)

// 挂载年级
export function bindGrades(data)

// 挂载班级
export function bindClasses(data)

// 删除学校年级
export function deleteSchoolGrade(schoolGradeId)

// 删除学校班级
export function deleteSchoolClass(schoolClassId)
```

**FE-SCH-08 详细说明 - 主页面逻辑**

```javascript
// 主要逻辑点
1. 初始化加载区域树和学校树
2. 区域树节点点击 -> 刷新学校树
3. 搜索栏查询 -> 刷新学校树
4. 新增/编辑学校 -> 打开弹窗 -> 提交 -> 刷新列表
5. 删除学校 -> 确认 -> 调用接口 -> 刷新列表
6. 新增年级 -> 打开选择弹窗 -> 确认 -> 刷新列表
7. 新增班级 -> 打开选择弹窗 -> 确认 -> 刷新列表
```

---

### 2.4 优化与测试

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| FE-SCH-09 | 样式优化 | 优化页面样式和交互体验 | P1 | 2h | ☐ |
| FE-SCH-10 | Bug修复 | 修复测试发现的问题 | P1 | 2h | ☐ |

**FE-SCH-09 详细说明 - 样式优化**

```
优化项：
1. 区域树节点悬停效果
2. 表格行悬停高亮
3. 操作按钮间距调整
4. 弹窗宽度适配
5. 加载骨架屏
6. 空状态提示
```

---

## 三、测试任务

| 任务编号 | 任务名称 | 描述 | 优先级 | 工时 | 状态 |
|:------:|---------|------|:------:|:----:|:----:|
| TEST-01 | 接口测试 | 使用Postman测试所有接口 | P0 | 3h | ☐ |
| TEST-02 | 功能测试 | UI功能测试 | P0 | 3h | ☐ |
| TEST-03 | 集成测试 | 前后端联调测试 | P1 | 2h | ☐ |

**TEST-01 详细说明 - 接口测试用例**

| 用例编号 | 测试场景 | 预期结果 |
|:------:|---------|---------|
| API-01 | 获取学校树（有区域ID）| 返回该区域下学校树 |
| API-02 | 获取学校树（无区域ID）| 返回全部学校树 |
| API-03 | 新增学校（正常）| 成功，学校编码自动生成 |
| API-04 | 新增学校（名称为空）| 失败，返回验证错误 |
| API-05 | 编辑学校 | 成功 |
| API-06 | 删除学校（无子级）| 成功 |
| API-07 | 删除学校（有年级）| 失败，提示有年级 |
| API-08 | 删除学校（有学生）| 失败，提示被引用 |
| API-09 | 挂载年级 | 成功 |
| API-10 | 重复挂载年级 | 成功（忽略重复）|
| API-11 | 挂载班级 | 成功 |
| API-12 | 删除年级（有班级）| 失败，提示有班级 |
| API-13 | 删除班级（有学生）| 失败，提示被引用 |

**TEST-02 详细说明 - 功能测试用例**

| 用例编号 | 测试场景 | 测试步骤 | 预期结果 |
|:------:|---------|---------|---------|
| UI-01 | 区域树展示 | 进入页面 | 左侧显示区域树 |
| UI-02 | 区域筛选 | 点击区域节点 | 右侧学校列表刷新 |
| UI-03 | 学校树展开 | 点击展开图标 | 显示年级和班级 |
| UI-04 | 新增学校 | 点击新增，填写表单，提交 | 成功，列表刷新 |
| UI-05 | 编辑学校 | 点击编辑，修改，提交 | 成功，数据更新 |
| UI-06 | 删除学校 | 点击删除，确认 | 成功/失败提示 |
| UI-07 | 新增年级 | 点击新增年级，选择，确认 | 成功，树刷新 |
| UI-08 | 新增班级 | 点击新增班级，选择，确认 | 成功，树刷新 |
| UI-09 | 表单验证 | 必填项留空提交 | 显示验证错误 |
| UI-10 | 搜索功能 | 输入关键词，点击搜索 | 列表过滤 |

---

## 四、开发进度跟踪

### 4.1 每日站会记录

| 日期 | 昨日完成 | 今日计划 | 阻塞问题 |
|-----|---------|---------|---------|
| | | | |
| | | | |
| | | | |

### 4.2 里程碑

| 里程碑 | 目标日期 | 完成日期 | 状态 |
|-------|---------|---------|:----:|
| 后端实体层完成 | | | ☐ |
| 后端Service层完成 | | | ☐ |
| 后端Controller完成 | | | ☐ |
| 前端页面框架完成 | | | ☐ |
| 前端组件开发完成 | | | ☐ |
| 前后端联调完成 | | | ☐ |
| 测试验收完成 | | | ☐ |

---

## 五、问题跟踪

| 问题编号 | 问题描述 | 发现日期 | 负责人 | 状态 | 解决方案 |
|:------:|---------|---------|-------|:----:|---------|
| | | | | | |

---

*文档结束*
