pangu-user-platform/docs/教师多班级功能技术方案.md

13 KiB
Raw Permalink Blame History

教师多班级功能技术方案

作者pangu
创建时间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 验证测试 测试 回归测试

八、回滚方案

  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 创建时间