534 lines
13 KiB
Markdown
534 lines
13 KiB
Markdown
# 教师多班级功能技术方案
|
||
|
||
> 作者:湖北新华业务中台研发团队
|
||
> 创建时间: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 | 创建时间 |
|