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

534 lines
13 KiB
Markdown
Raw Normal View History

# 教师多班级功能技术方案
> 作者:湖北新华业务中台研发团队
> 创建时间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 新建实体类
**文件**`ruoyi-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
**文件**`ruoyi-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<PgMemberClass> {
}
```
#### 3.2.3 修改 DTO
**文件**`H5EducationDto.java`
```java
// 修改前
@Schema(description = "学校班级关联ID")
@NotNull(message = "请选择班级")
private Long schoolClassId;
// 修改后
@Schema(description = "学校班级关联ID列表")
@NotEmpty(message = "请选择班级")
private List<Long> schoolClassIds;
```
#### 3.2.4 修改 VO
**文件**`H5EducationVo.java`
```java
// 新增字段
private List<Long> schoolClassIds; // 班级ID列表
private List<String> 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<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`
**表单数据修改**
```javascript
// 修改前
const teacherForm = reactive({
classId: '',
})
// 修改后
const teacherForm = reactive({
classIds: [],
})
```
**模板修改**
```vue
<!-- 修改前 -->
<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>
```
**验证规则修改**
```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 | 创建时间 |