13 KiB
13 KiB
教师多班级功能技术方案
作者:湖北新华业务中台研发团队
创建时间: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 新建关联表
-- 会员-班级关联表(多对多)
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 数据迁移脚本
-- 将现有数据迁移到关联表
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
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
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<PgMemberClass> {
}
3.2.3 修改 DTO
文件:H5EducationDto.java
// 修改前
@Schema(description = "学校班级关联ID")
@NotNull(message = "请选择班级")
private Long schoolClassId;
// 修改后
@Schema(description = "学校班级关联ID列表")
@NotEmpty(message = "请选择班级")
private List<Long> schoolClassIds;
3.2.4 修改 VO
文件:H5EducationVo.java
// 新增字段
private List<Long> schoolClassIds; // 班级ID列表
private List<String> classNames; // 班级名称列表(用于前端展示)
3.2.5 修改 Service
文件:H5MemberServiceImpl.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<PgMemberClass>()
.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<PgMemberClass> memberClasses = memberClassMapper.selectList(
new LambdaQueryWrapper<PgMemberClass>()
.eq(PgMemberClass::getMemberId, memberId)
);
List<Long> classIds = memberClasses.stream()
.map(PgMemberClass::getSchoolClassId)
.collect(Collectors.toList());
vo.setSchoolClassIds(classIds);
// 填充班级名称
if (!classIds.isEmpty()) {
List<String> 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<PgMemberClass>()
.eq(PgMemberClass::getMemberId, memberId)
);
// 清空会员教育信息
// ... 原有逻辑 ...
}
3.3 H5 前端改造
3.3.1 修改表单组件
文件:user_authentication_center_front/user-front/src/components/TeacherIdentityForm.vue
表单数据修改:
// 修改前
const teacherForm = reactive({
classId: '',
})
// 修改后
const teacherForm = reactive({
classIds: [],
})
模板修改:
<!-- 修改前 -->
<el-form-item label="班级" prop="classId">
<el-select v-model="teacherForm.classId">
<!-- ... -->
</el-select>
</el-form-item>
<!-- 修改后 -->
<el-form-item label="班级" prop="classIds">
<el-select
v-model="teacherForm.classIds"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
placeholder="请选择班级(可多选)"
size="large"
>
<el-option
v-for="classItem in classOptions"
:key="classItem.schoolClassId"
:label="classItem.className"
:value="classItem.schoolClassId"
/>
</el-select>
</el-form-item>
验证规则修改:
// 修改前
classId: [{ required: true, message: '请选择班级', trigger: 'change' }],
// 修改后
classIds: [{
required: true,
type: 'array',
min: 1,
message: '请至少选择一个班级',
trigger: 'change'
}],
提交逻辑修改:
const requestData = {
schoolId: teacherForm.schoolId,
schoolGradeId: teacherForm.gradeId,
schoolClassIds: teacherForm.classIds, // 数组
subjectId: teacherForm.subjectId,
}
回显逻辑修改:
// setFormData 中
teacherForm.classIds = formData.schoolClassIds || []
3.3.2 修改列表展示
文件:user_authentication_center_front/user-front/src/views/userCenter/index.vue
// 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
请求参数变更:
// 修改前
{
"schoolId": 1,
"schoolGradeId": 5,
"schoolClassId": 104,
"subjectId": 1
}
// 修改后
{
"schoolId": 1,
"schoolGradeId": 5,
"schoolClassIds": [104, 105, 106],
"subjectId": 1
}
5.2 GET /h5/member/education
响应参数变更:
// 修改前
{
"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 | 验证测试 | 测试 | 回归测试 |
八、回滚方案
- 后端代码回滚到上一版本
- 前端代码回滚到上一版本
- 数据库:
- 从
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 | 创建时间 |