# 教师多班级功能技术方案 > 作者:湖北新华业务中台研发团队 > 创建时间:2026-02-03 > 状态:待审核 --- ## 一、需求描述 ### 1.1 业务场景 - 张老师是武汉二中高二年级的数学老师 - 他同时教 1班、2班、3班 - 系统应支持在**一个教育身份**中选择**多个班级** ### 1.2 预期效果 **添加/编辑教育身份时**: ``` 所在地区: 湖北省 / 武汉市 / 硚口区 学校: 武汉市第二中学 年级: 高二 班级: [1班] [2班] [3班] ← 多选 学科: 数学 ``` **个人中心展示**: ``` ┌────────────────────────────────┐ │ 📘 数学 当前使用 │ │ 武汉市第二中学 │ │ 班级: 高二1班、高二2班、高二3班 │ │ 学科: 数学 │ │ 编辑 解除 切换 │ └────────────────────────────────┘ ``` --- ## 二、当前架构分析 ### 2.1 现有数据结构 | 表名 | 字段 | 类型 | 说明 | |------|------|------|------| | `pg_member` | `school_class_id` | `bigint` | 单值,只能存一个班级 | ### 2.2 现有接口 | 接口 | 方法 | 说明 | |------|------|------| | `/h5/member/education` | GET | 返回单个教育身份 | | `/h5/member/education` | POST | 保存单个教育身份(覆盖) | | `/h5/member/education` | DELETE | 删除教育身份 | ### 2.3 问题 - 班级字段是单值,不支持多班级 - 接口设计为覆盖式更新 --- ## 三、技术方案 ### 3.1 数据库改造 #### 3.1.1 新建关联表 ```sql -- 会员-班级关联表(多对多) CREATE TABLE pg_member_class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL COMMENT '会员ID', school_class_id BIGINT NOT NULL COMMENT '学校班级关联ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_member_class (member_id, school_class_id), KEY idx_member_id (member_id) ) COMMENT '会员班级关联表'; ``` #### 3.1.2 数据迁移脚本 ```sql -- 将现有数据迁移到关联表 INSERT INTO pg_member_class (member_id, school_class_id) SELECT member_id, school_class_id FROM pg_member WHERE school_class_id IS NOT NULL AND identity_type = '2'; ``` #### 3.1.3 pg_member 表调整 | 字段 | 处理方式 | |------|----------| | `school_class_id` | 保留(向后兼容)或后续版本删除 | --- ### 3.2 后端改造 #### 3.2.1 新建实体类 **文件**:`pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberClass.java` ```java package org.dromara.pangu.member.domain; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.util.Date; @Data @TableName("pg_member_class") public class PgMemberClass { @TableId(type = IdType.AUTO) private Long id; private Long memberId; private Long schoolClassId; private Date createTime; } ``` #### 3.2.2 新建 Mapper **文件**:`pangu-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberClassMapper.java` ```java package org.dromara.pangu.member.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.dromara.pangu.member.domain.PgMemberClass; @Mapper public interface PgMemberClassMapper extends BaseMapper { } ``` #### 3.2.3 修改 DTO **文件**:`H5EducationDto.java` ```java // 修改前 @Schema(description = "学校班级关联ID") @NotNull(message = "请选择班级") private Long schoolClassId; // 修改后 @Schema(description = "学校班级关联ID列表") @NotEmpty(message = "请选择班级") private List schoolClassIds; ``` #### 3.2.4 修改 VO **文件**:`H5EducationVo.java` ```java // 新增字段 private List schoolClassIds; // 班级ID列表 private List classNames; // 班级名称列表(用于前端展示) ``` #### 3.2.5 修改 Service **文件**:`H5MemberServiceImpl.java` ```java @Autowired private PgMemberClassMapper memberClassMapper; @Override @Transactional(rollbackFor = Exception.class) public void saveEducation(H5EducationDto dto) { Long memberId = getCurrentMemberId(); PgMember member = memberMapper.selectById(memberId); if (member == null) { throw new ServiceException("会员不存在"); } // 1. 校验学校、年级存在 // ... 原有校验逻辑 ... // 2. 校验所有班级是否存在且属于该年级 for (Long classId : dto.getSchoolClassIds()) { PgSchoolClass schoolClass = schoolClassMapper.selectById(classId); if (schoolClass == null || !schoolClass.getSchoolGradeId().equals(dto.getSchoolGradeId())) { throw new ServiceException("班级不存在或不属于该年级"); } } // 3. 更新会员基本信息 member.setSchoolId(dto.getSchoolId()); member.setSchoolGradeId(dto.getSchoolGradeId()); member.setSubjectId(dto.getSubjectId()); member.setIdentityType("2"); member.setRegionId(school.getRegionId()); memberMapper.updateById(member); // 4. 删除旧的班级关联 memberClassMapper.delete( new LambdaQueryWrapper() .eq(PgMemberClass::getMemberId, memberId) ); // 5. 插入新的班级关联 for (Long classId : dto.getSchoolClassIds()) { PgMemberClass mc = new PgMemberClass(); mc.setMemberId(memberId); mc.setSchoolClassId(classId); memberClassMapper.insert(mc); } } @Override public H5EducationVo getEducation() { Long memberId = getCurrentMemberId(); PgMember member = memberMapper.selectById(memberId); if (member == null) { throw new ServiceException("会员不存在"); } if (!"2".equals(member.getIdentityType()) || member.getSchoolId() == null) { return null; } H5EducationVo vo = buildEducationVo(member); // 查询关联的班级列表 List memberClasses = memberClassMapper.selectList( new LambdaQueryWrapper() .eq(PgMemberClass::getMemberId, memberId) ); List classIds = memberClasses.stream() .map(PgMemberClass::getSchoolClassId) .collect(Collectors.toList()); vo.setSchoolClassIds(classIds); // 填充班级名称 if (!classIds.isEmpty()) { List classNames = new ArrayList<>(); for (Long classId : classIds) { PgSchoolClass schoolClass = schoolClassMapper.selectById(classId); if (schoolClass != null) { PgClass pgClass = classMapper.selectById(schoolClass.getClassId()); if (pgClass != null) { classNames.add(pgClass.getClassName()); } } } vo.setClassNames(classNames); } return vo; } @Override @Transactional(rollbackFor = Exception.class) public void deleteEducation() { Long memberId = getCurrentMemberId(); // 删除班级关联 memberClassMapper.delete( new LambdaQueryWrapper() .eq(PgMemberClass::getMemberId, memberId) ); // 清空会员教育信息 // ... 原有逻辑 ... } ``` --- ### 3.3 H5 前端改造 #### 3.3.1 修改表单组件 **文件**:`user_authentication_center_front/user-front/src/components/TeacherIdentityForm.vue` **表单数据修改**: ```javascript // 修改前 const teacherForm = reactive({ classId: '', }) // 修改后 const teacherForm = reactive({ classIds: [], }) ``` **模板修改**: ```vue ``` **验证规则修改**: ```javascript // 修改前 classId: [{ required: true, message: '请选择班级', trigger: 'change' }], // 修改后 classIds: [{ required: true, type: 'array', min: 1, message: '请至少选择一个班级', trigger: 'change' }], ``` **提交逻辑修改**: ```javascript const requestData = { schoolId: teacherForm.schoolId, schoolGradeId: teacherForm.gradeId, schoolClassIds: teacherForm.classIds, // 数组 subjectId: teacherForm.subjectId, } ``` **回显逻辑修改**: ```javascript // setFormData 中 teacherForm.classIds = formData.schoolClassIds || [] ``` #### 3.3.2 修改列表展示 **文件**:`user_authentication_center_front/user-front/src/views/userCenter/index.vue` ```javascript // getEducationIdentities 方法中 educationIdentities.value = [{ id: 1, role: `${item.subjectName || '教师'}`, school: item.schoolName || '', icon: 'mdi:account-tie', active: true, details: [ { label: '班级', value: item.classNames?.join('、') || '' }, // 多班级用顿号连接 { label: '学科', value: item.subjectName || '' }, ], }] ``` --- ## 四、文件改动清单 | 层级 | 文件路径 | 改动类型 | |------|----------|----------| | **数据库** | DDL 脚本 | 新建 | | **后端** | `pangu-business/.../member/domain/PgMemberClass.java` | 新建 | | **后端** | `pangu-business/.../member/mapper/PgMemberClassMapper.java` | 新建 | | **后端** | `pangu-business/.../h5/domain/dto/H5EducationDto.java` | 修改 | | **后端** | `pangu-business/.../h5/domain/vo/H5EducationVo.java` | 修改 | | **后端** | `pangu-business/.../h5/service/impl/H5MemberServiceImpl.java` | 修改 | | **H5前端** | `user-front/src/components/TeacherIdentityForm.vue` | 修改 | | **H5前端** | `user-front/src/views/userCenter/index.vue` | 修改 | --- ## 五、接口变更 ### 5.1 POST /h5/member/education **请求参数变更**: ```json // 修改前 { "schoolId": 1, "schoolGradeId": 5, "schoolClassId": 104, "subjectId": 1 } // 修改后 { "schoolId": 1, "schoolGradeId": 5, "schoolClassIds": [104, 105, 106], "subjectId": 1 } ``` ### 5.2 GET /h5/member/education **响应参数变更**: ```json // 修改前 { "code": 200, "data": { "schoolId": 1, "schoolName": "武汉市第二中学", "schoolGradeId": 5, "gradeName": "高二", "schoolClassId": 104, "className": "1班", "subjectId": 1, "subjectName": "数学" } } // 修改后 { "code": 200, "data": { "schoolId": 1, "schoolName": "武汉市第二中学", "schoolGradeId": 5, "gradeName": "高二", "schoolClassIds": [104, 105, 106], "classNames": ["1班", "2班", "3班"], "subjectId": 1, "subjectName": "数学" } } ``` --- ## 六、测试要点 | 场景 | 测试内容 | 预期结果 | |------|----------|----------| | 新增 | 选择多个班级保存 | 保存成功,关联表有多条记录 | | 编辑 | 回显多个班级 | 多选框显示已选班级 | | 编辑 | 增加/减少班级 | 关联表正确更新 | | 删除 | 删除教育身份 | 关联表数据同步删除 | | 展示 | 列表显示多班级 | 显示"1班、2班、3班" | | 边界 | 不选班级提交 | 校验失败,提示选择班级 | | 边界 | 选择不同年级的班级 | 校验失败,提示班级不属于该年级 | --- ## 七、上线计划 | 步骤 | 内容 | 负责人 | 备注 | |------|------|--------|------| | 1 | 执行数据库 DDL | DBA | 建表 | | 2 | 执行数据迁移脚本 | DBA | 迁移现有数据 | | 3 | 部署后端服务 | 运维 | - | | 4 | 部署 H5 前端 | 运维 | 需同步上线 | | 5 | 验证测试 | 测试 | 回归测试 | --- ## 八、回滚方案 1. 后端代码回滚到上一版本 2. 前端代码回滚到上一版本 3. 数据库: - 从 `pg_member_class` 恢复 `pg_member.school_class_id`(取第一条) - 删除 `pg_member_class` 表 --- ## 九、风险评估 | 风险 | 等级 | 应对措施 | |------|------|----------| | 数据迁移失败 | 中 | 先在测试环境验证,生产环境备份 | | 前后端不同步 | 高 | 同步上线,灰度发布 | | 性能问题 | 低 | 关联表已加索引 | | 兼容性问题 | 中 | 管理后台如有相关页面需同步修改 | --- ## 附录:相关表结构 ### pg_member(会员表,现有) | 字段 | 类型 | 说明 | |------|------|------| | member_id | bigint | 主键 | | consumer_id | bigint | 用户ID | | school_id | bigint | 学校ID | | school_grade_id | bigint | 年级ID | | school_class_id | bigint | 班级ID(单值,改造后可废弃) | | subject_id | bigint | 学科ID | | identity_type | varchar | 身份类型:1-家长,2-教师 | ### pg_member_class(会员班级关联表,新建) | 字段 | 类型 | 说明 | |------|------|------| | id | bigint | 主键 | | member_id | bigint | 会员ID | | school_class_id | bigint | 学校班级关联ID | | create_time | datetime | 创建时间 |