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

534 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 教师多班级功能技术方案
> 作者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 新建关联表
```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<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 | 创建时间 |