24 KiB
24 KiB
会员教育信息重构方案
作者:湖北新华业务中台研发团队
创建时间:2026-02-03
状态:已审核通过
一、需求变更说明
1.1 变更内容
- 去掉身份类型:会员不再区分"家长"和"教师"
- 重构会员信息表:会员与教育信息变为一对多关系,一个会员可以有多个教育身份
- pg_member 表移除字段:
region_id- 区域IDschool_id- 学校IDschool_grade_id- 年级IDschool_class_id- 班级IDidentity_type- 身份类型- 保留(微信多应用打通需要)union_id
1.2 变更原因
- 业务场景:一个用户可能同时是多个学校/班级的教师
- 简化会员模型:会员表只存储基础信息,教育信息独立管理
二、当前架构分析
2.1 现有数据结构
pg_member 表(部分字段):
| 字段 | 类型 | 说明 | 变更 |
|---|---|---|---|
| member_id | bigint | 主键 | 保留 |
| member_code | varchar(32) | 会员编号 | 保留 |
| phone | varchar(20) | 手机号 | 保留 |
| password | varchar(100) | 密码 | 保留 |
| nickname | varchar(50) | 昵称 | 保留 |
| avatar | varchar(500) | 头像 | 保留 |
| gender | char(1) | 性别 | 保留 |
| birthday | date | 生日 | 保留 |
| identity_type | char(1) | 身份类型 | 删除 |
| open_id | varchar(100) | 微信OpenID | 保留 |
| union_id | varchar(100) | 微信UnionID | 保留 |
| region_id | bigint | 区域ID | 删除 |
| school_id | bigint | 学校ID | 删除 |
| school_grade_id | bigint | 年级ID | 删除 |
| school_class_id | bigint | 班级ID | 删除 |
| register_source | char(1) | 注册来源 | 保留 |
| register_time | datetime | 注册时间 | 保留 |
| status | char(1) | 状态 | 保留 |
2.2 现有关联关系
pg_member(会员)
├── 一对一:教育信息(存在会员表中) ← 需要改为一对多
└── 一对多:pg_student(学生) ← 已改为多对多
三、目标架构设计
3.1 新数据结构
pg_member(会员)- 只存基础信息
├── 一对多:pg_education(会员教育信息) ← 新建
└── 多对多:pg_member_student ↔ pg_student ← 已重构(支持多会员绑定同一学生)
3.2 新建表:pg_member_education
设计理念:一个教育身份 = 一个班级的教学关系
张老师教3个班 → 3条教育身份记录
教育身份1: 二中 - 高二 - 1班 - 数学
教育身份2: 二中 - 高二 - 2班 - 数学
教育身份3: 二中 - 高二 - 3班 - 数学
CREATE TABLE pg_member_education (
education_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '教育身份ID',
member_id BIGINT NOT NULL COMMENT '会员ID',
region_id BIGINT COMMENT '区域ID',
school_id BIGINT NOT NULL COMMENT '学校ID',
school_grade_id BIGINT NOT NULL COMMENT '年级ID(school_grade关联ID)',
school_class_id BIGINT NOT NULL COMMENT '班级ID(school_class关联ID)',
subject_id BIGINT COMMENT '学科ID',
is_default CHAR(1) DEFAULT '0' COMMENT '是否默认身份(0否 1是)',
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志',
KEY idx_member_id (member_id),
KEY idx_school_id (school_id),
KEY idx_class_id (school_class_id)
) COMMENT '会员教育信息表';
3.3 最终关系图
pg_member(会员基础信息)
│
├── 一对多 → pg_member_education(教育身份,每班一条)
│
└── 一对多 → pg_student(学生/孩子)
四、影响范围分析
4.1 数据库层
| 操作 | 对象 | 说明 |
|---|---|---|
| 修改 | pg_member | 删除5个字段 |
| 新建 | pg_member_education | 会员教育信息表(含班级字段) |
| 迁移 | 现有教育数据 | 迁移到新表 |
4.2 后端层
| 文件 | 改动类型 | 说明 |
|---|---|---|
PgMember.java |
修改 | 删除5个字段(保留unionId) |
PgMemberEducation.java |
新建 | 教育身份实体 |
PgMemberEducationMapper.java |
新建 | Mapper |
H5MemberServiceImpl.java |
重构 | 教育相关逻辑 |
H5EducationDto.java |
修改 | 添加educationId |
H5EducationVo.java |
修改 | 添加educationId等字段 |
H5MemberController.java |
修改 | 接口调整 |
H5MemberInfoVo.java |
修改 | 删除identityType |
PgMemberController.java |
修改 | 删除身份类型相关 |
PgMemberServiceImpl.java |
修改 | 删除教育信息处理 |
4.3 管理后台前端
| 文件 | 改动类型 | 说明 |
|---|---|---|
business/member/index.vue |
修改 | 删除身份类型筛选和列 |
business/member/components/MemberDialog.vue |
重构 | 删除身份类型和教师信息区块 |
business/member/components/EducationTab.vue |
新建 | 教育身份管理Tab |
business/member/components/MemberDetail.vue |
新建 | 会员详情页(含教育身份Tab) |
api/pangu/member.js |
修改 | 删除/新增接口 |
api/pangu/memberEducation.js |
新建 | 教育身份接口 |
4.4 H5前端
| 文件 | 改动类型 | 说明 |
|---|---|---|
stores/user.ts |
修改 | 删除 identityType |
views/userCenter/index.vue |
修改 | 教育身份列表支持多个 |
components/TeacherIdentityForm.vue |
修改 | 新增/编辑某个教育身份 |
api/user.js |
修改 | 接口调整 |
五、接口变更
5.1 H5接口变更
5.1.1 获取会员信息(修改)
接口:GET /h5/member/info
响应变更:
// 修改前
{
"code": 200,
"data": {
"consumerId": 1,
"phone": "138****0000",
"nickname": "测试用户",
"identityType": "2", // ← 删除
"education": { ... }, // 单个
"students": [...]
}
}
// 修改后
{
"code": 200,
"data": {
"consumerId": 1,
"phone": "138****0000",
"nickname": "测试用户",
"educations": [...], // 多个教育身份
"students": [...]
}
}
5.1.2 教育身份列表(修改)
接口:GET /h5/member/educations
响应:
{
"code": 200,
"data": [
{
"educationId": 1,
"schoolId": 1,
"schoolName": "武汉市第二中学",
"schoolGradeId": 5,
"gradeName": "高二",
"schoolClassId": 104,
"className": "1班",
"subjectId": 1,
"subjectName": "数学",
"isDefault": "1"
},
{
"educationId": 2,
"schoolId": 1,
"schoolName": "武汉市第二中学",
"schoolGradeId": 5,
"gradeName": "高二",
"schoolClassId": 105,
"className": "2班",
"subjectId": 1,
"subjectName": "数学",
"isDefault": "0"
}
]
}
5.1.3 新增教育身份
接口:POST /h5/member/educations
请求参数:
{
"schoolId": 1,
"schoolGradeId": 5,
"schoolClassId": 104,
"subjectId": 1
}
5.1.4 编辑教育身份
接口:PUT /h5/member/educations/{educationId}
请求参数:
{
"schoolId": 1,
"schoolGradeId": 5,
"schoolClassId": 104,
"subjectId": 1
}
5.1.5 删除教育身份
接口:DELETE /h5/member/educations/{educationId}
5.1.6 设置默认教育身份
接口:PUT /h5/member/educations/{educationId}/default
5.2 管理后台接口变更
5.2.1 会员列表(修改)
- 删除
identityType筛选参数 - 响应中删除
identityType字段
5.2.2 会员详情(修改)
- 删除教育信息字段
- 新增
educations列表
5.2.3 新增/编辑会员(修改)
- 删除
identityType、regionId、schoolId、schoolGradeId、schoolClassId参数
5.2.4 会员教育身份管理(新增)
| 接口 | 方法 | 说明 |
|---|---|---|
/business/member/{memberId}/educations |
GET | 获取会员教育身份列表 |
/business/member/{memberId}/education |
POST | 添加教育身份 |
/business/member/{memberId}/education/{educationId} |
PUT | 修改教育身份 |
/business/member/{memberId}/education/{educationId} |
DELETE | 删除教育身份 |
六、详细技术方案
6.1 数据库改造
6.1.1 DDL脚本
-- 新建会员教育信息表(一个教育身份 = 一个班级的教学关系)
CREATE TABLE pg_member_education (
education_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '教育身份ID',
member_id BIGINT NOT NULL COMMENT '会员ID',
region_id BIGINT COMMENT '区域ID',
school_id BIGINT NOT NULL COMMENT '学校ID',
school_grade_id BIGINT NOT NULL COMMENT '年级ID',
school_class_id BIGINT NOT NULL COMMENT '班级ID',
subject_id BIGINT COMMENT '学科ID',
is_default CHAR(1) DEFAULT '0' COMMENT '是否默认身份(0否 1是)',
status CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户编号',
create_dept BIGINT COMMENT '创建部门',
create_by BIGINT COMMENT '创建者',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_by BIGINT COMMENT '更新者',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志',
KEY idx_member_id (member_id),
KEY idx_school_id (school_id),
KEY idx_class_id (school_class_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='会员教育信息表';
6.1.2 数据迁移脚本
-- 迁移现有教育数据(identity_type = '2' 的教师数据)
INSERT INTO pg_member_education (
member_id, region_id, school_id, school_grade_id, school_class_id, subject_id, is_default, status, tenant_id, create_time
)
SELECT
member_id, region_id, school_id, school_grade_id, school_class_id, NULL, '1', '0', tenant_id, NOW()
FROM pg_member
WHERE identity_type = '2'
AND school_id IS NOT NULL
AND school_class_id IS NOT NULL
AND del_flag = '0';
6.1.3 删除字段(迁移完成后执行)
-- 删除 pg_member 表中的字段(建议先备份)
-- 注意:union_id 保留,不删除
ALTER TABLE pg_member
DROP COLUMN identity_type,
DROP COLUMN region_id,
DROP COLUMN school_id,
DROP COLUMN school_grade_id,
DROP COLUMN school_class_id;
6.2 后端改造
6.2.1 新建实体类
PgMemberEducation.java
package org.dromara.pangu.member.domain;
@Data
@TableName("pg_member_education")
public class PgMemberEducation extends BaseEntity {
@TableId(type = IdType.AUTO)
private Long educationId;
private Long memberId;
private Long regionId;
private Long schoolId;
private Long schoolGradeId;
private Long schoolClassId;
private Long subjectId;
private String isDefault;
private String status;
private String tenantId;
@TableLogic
private String delFlag;
// 非数据库字段(用于VO展示)
@TableField(exist = false)
private String schoolName;
@TableField(exist = false)
private String gradeName;
@TableField(exist = false)
private String className;
@TableField(exist = false)
private String subjectName;
}
6.2.2 修改 PgMember.java
删除以下字段:
- identityType
- regionId
- regionIds(@TableField(exist = false))
- schoolId
- schoolGradeId
- schoolClassId
保留字段:
- unionId(微信多应用打通需要)
6.2.3 新建 Mapper
- PgMemberEducationMapper.java
6.2.4 修改 H5 Service
H5MemberServiceImpl.java 主要改动:
getEducation()→getEducations()返回列表saveEducation()支持新增/编辑deleteEducation(Long educationId)按ID删除- 新增
setDefaultEducation(Long educationId)设置默认
6.3 管理后台前端改造
6.3.1 会员列表页(index.vue)
删除内容:
<!-- 删除:身份类型筛选 -->
<el-form-item label="身份类型">
<el-select v-model="queryParams.identityType" ... />
</el-form-item>
<!-- 删除:身份类型列 -->
<el-table-column prop="identityType" label="身份类型" ... />
6.3.2 会员编辑弹窗(MemberDialog.vue)
删除内容:
<!-- 删除:身份类型选择 -->
<el-form-item label="身份类型" prop="identityType">
<el-radio-group v-model="form.identityType" ... />
</el-form-item>
<!-- 删除:教师信息区块 -->
<template v-if="form.identityType === '2'">
<el-divider>学校信息(教师必填)</el-divider>
<!-- 区域、学校、年级、班级选择 -->
</template>
6.3.3 新建会员详情页
MemberDetail.vue(新建)
<template>
<el-dialog v-model="visible" title="会员详情" width="900px">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<!-- 会员基本信息展示 -->
</el-tab-pane>
<el-tab-pane label="教育身份" name="education">
<EducationTab :member-id="memberId" />
</el-tab-pane>
<el-tab-pane label="绑定学生" name="students">
<!-- 学生列表 -->
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
6.3.4 新建教育身份Tab
EducationTab.vue(新建)
<template>
<div>
<el-button type="primary" @click="handleAdd">添加教育身份</el-button>
<el-table :data="educationList">
<el-table-column prop="schoolName" label="学校" />
<el-table-column prop="gradeName" label="年级" />
<el-table-column label="班级">
<template #default="{ row }">
{{ row.classNames?.join('、') }}
</template>
</el-table-column>
<el-table-column prop="subjectName" label="学科" />
<el-table-column prop="isDefault" label="默认" width="80">
<template #default="{ row }">
<el-tag v-if="row.isDefault === '1'" type="success">是</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button link @click="handleEdit(row)">编辑</el-button>
<el-button link @click="handleSetDefault(row)">设为默认</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 编辑弹窗 -->
<EducationDialog ref="dialogRef" @success="loadData" />
</div>
</template>
6.4 H5前端改造
6.4.1 user.ts 修改
// 删除
identityType: string
// 修改
education: any // 单个 →
educations: any[] // 多个
6.4.2 userCenter/index.vue 修改
支持展示多个教育身份,而不是只展示一个。
6.4.3 TeacherIdentityForm.vue 修改
新增 educationId 字段用于编辑。
七、文件改动清单
7.1 数据库
| 文件/操作 | 类型 |
|---|---|
| DDL: pg_member_education | 新建表(含 school_class_id) |
| DML: 数据迁移脚本 | 执行 |
| DDL: pg_member 删除字段 | 修改表 |
7.2 后端(共 12+ 文件)
| 文件 | 类型 | 说明 |
|---|---|---|
member/domain/PgMember.java |
修改 | 删除5个字段(保留unionId) |
member/domain/PgMemberEducation.java |
新建 | |
member/mapper/PgMemberEducationMapper.java |
新建 | |
member/service/PgMemberEducationService.java |
新建 | |
member/service/impl/PgMemberEducationServiceImpl.java |
新建 | |
member/controller/PgMemberController.java |
修改 | 删除身份类型相关 |
member/service/impl/PgMemberServiceImpl.java |
修改 | |
h5/domain/dto/H5EducationDto.java |
修改 | 添加educationId |
h5/domain/vo/H5EducationVo.java |
修改 | |
h5/domain/vo/H5MemberInfoVo.java |
修改 | 删除identityType |
h5/controller/H5MemberController.java |
修改 | 接口调整 |
h5/service/impl/H5MemberServiceImpl.java |
重构 | 教育相关逻辑 |
7.3 管理后台前端(共 8+ 文件)
| 文件 | 类型 | 说明 |
|---|---|---|
views/business/member/index.vue |
修改 | 删除身份类型 |
views/business/member/components/MemberDialog.vue |
重构 | 删除教师信息 |
views/business/member/components/MemberDetail.vue |
新建 | 会员详情 |
views/business/member/components/EducationTab.vue |
新建 | 教育身份Tab |
views/business/member/components/EducationDialog.vue |
新建 | 教育身份编辑 |
api/pangu/member.js |
修改 | |
api/pangu/memberEducation.js |
新建 |
7.4 H5前端(共 5+ 文件)
| 文件 | 类型 | 说明 |
|---|---|---|
stores/user.ts |
修改 | 删除identityType |
views/userCenter/index.vue |
修改 | 支持多教育身份 |
components/TeacherIdentityForm.vue |
修改 | 添加educationId |
api/user.js |
修改 | 接口调整 |
views/register/index.vue |
可能修改 | 如有身份类型相关 |
八、测试要点
| 场景 | 测试内容 |
|---|---|
| 数据迁移 | 现有教师数据正确迁移到新表 |
| H5-新增教育身份 | 首次添加、多次添加(每班一条记录) |
| H5-编辑教育身份 | 修改学校/年级/班级/学科 |
| H5-删除教育身份 | 删除非默认、删除默认 |
| H5-设置默认 | 切换默认身份 |
| H5-多教育身份 | 一个老师添加多条教育身份(教多个班) |
| 管理后台-会员列表 | 不显示身份类型 |
| 管理后台-会员详情 | 查看多个教育身份 |
| 管理后台-教育身份管理 | 增删改查 |
九、上线计划
| 步骤 | 内容 | 说明 |
|---|---|---|
| 1 | 备份数据库 | 全量备份 pg_member |
| 2 | 执行建表DDL | 创建新表 |
| 3 | 执行数据迁移 | 迁移现有教育数据 |
| 4 | 部署后端 | 新代码 |
| 5 | 部署管理后台前端 | 新代码 |
| 6 | 部署H5前端 | 新代码 |
| 7 | 验证测试 | 回归测试 |
| 8 | 执行删除字段DDL | 确认无问题后 |
十、回滚方案
- 后端代码回滚
- 前端代码回滚
- 数据库:
- 从新表恢复数据到 pg_member
- 删除新建的两张表
十一、风险评估
| 风险 | 等级 | 应对措施 |
|---|---|---|
| 数据迁移丢失 | 高 | 完整备份,测试环境验证 |
| 接口不兼容 | 高 | 前后端同步上线 |
| 性能问题 | 低 | 新表已加索引 |
| 业务中断 | 中 | 选择低峰期上线 |
附录A:现有数据统计(上线前执行)
-- 统计需要迁移的教育数据
SELECT COUNT(*) FROM pg_member
WHERE identity_type = '2' AND school_id IS NOT NULL AND del_flag = '0';
-- 统计有班级信息的数据
SELECT COUNT(*) FROM pg_member
WHERE identity_type = '2' AND school_class_id IS NOT NULL AND del_flag = '0';
附录B:学生多会员绑定重构(2026-02-03)
B.1 需求说明
原设计:一个学生只能被一个会员绑定(pg_student.member_id 单值字段)
新设计:一个学生可以被多个会员绑定(如爸爸和妈妈都能绑定同一个孩子)
B.2 数据库变更
新建关联表 pg_member_student
CREATE TABLE pg_member_student (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
member_id BIGINT NOT NULL COMMENT '会员ID',
student_id BIGINT NOT NULL COMMENT '学生ID',
relation VARCHAR(20) COMMENT '关系(父亲/母亲/其他)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_member_student (member_id, student_id),
KEY idx_member_id (member_id),
KEY idx_student_id (student_id)
) COMMENT='会员学生关联表';
数据迁移
-- 迁移现有绑定关系
INSERT IGNORE INTO pg_member_student (member_id, student_id)
SELECT member_id, student_id FROM pg_student WHERE member_id IS NOT NULL;
-- 移除 pg_student.member_id 字段
ALTER TABLE pg_student DROP COLUMN member_id;
B.3 后端变更
| 文件 | 变更 |
|---|---|
| PgMemberStudent.java | 新建关联实体 |
| PgMemberStudentMapper.java | 新建 Mapper |
| PgStudent.java | 删除 memberId 字段 |
| StudentVo.java | memberCount + List members |
| PgStudentServiceImpl.java | 绑定/解绑/查询改用关联表 |
| H5MemberServiceImpl.java | 绑定/解绑/查询改用关联表 |
| H5StudentVo.java | 新增 relation 字段 |
| H5StudentBindDto.java | 新增 relation 字段 |
B.4 前端变更
| 文件 | 变更 |
|---|---|
| MemberDialog.vue | 解绑接口路径调整 |
| StudentSelectDialog.vue | 绑定状态显示多会员数量 |
B.5 关系模型
变更前:会员 --1:N--> 学生(通过 pg_student.member_id)
变更后:会员 --N:M--> 学生(通过 pg_member_student 关联表)
附录C:H5 前端同步变更
完成时间:2026-02-03 项目:user_authentication_center_front
C.1 变更概述
H5 前端同步适配后端「会员教育信息重构」和「学生多会员绑定」变更。
C.2 注册流程简化
删除注册页面的身份类型选择步骤:
| 变更前 | 变更后 |
|---|---|
| 步骤1: 基本信息 → 步骤2: 身份选择 → 步骤3: 详细信息 | 仅基本信息,注册后跳转用户中心 |
涉及文件:
views/register/index.vue- 删除身份选择和详细信息步骤
C.3 教育身份改为数组
| 文件 | 变更 |
|---|---|
stores/user.ts |
删除 identityType,education: any → educations: any[] |
api/user.js |
接口路径 /h5/member/education → /h5/member/educations |
views/userCenter/index.vue |
列表展示多教育身份,支持删除指定 ID |
接口变更:
// 变更前
POST /h5/member/education // 新增(覆盖式)
GET /h5/member/education // 获取单个
DELETE /h5/member/education // 删除
// 变更后
POST /h5/member/educations // 新增
PUT /h5/member/educations/{id} // 修改
GET /h5/member/educations // 获取列表
DELETE /h5/member/educations/{id} // 删除指定
PUT /h5/member/educations/{id}/default // 设为默认
C.4 基础数据前端缓存
新建 stores/baseData.ts 实现区域和学科数据的前端缓存:
// 缓存结构
{
regionTree: [], // 区域树
subjects: [], // 学科列表
expire: timestamp, // 过期时间(24小时)
}
// 存储位置
localStorage.setItem('h5:baseData', JSON.stringify(data))
数据流:
用户登录成功
↓
强制刷新基础数据(fetchRegionTree(true), fetchSubjects(true))
↓
存入 Pinia Store + localStorage
↓
后续页面从 Store 读取,秒加载
涉及文件:
| 文件 | 变更 |
|---|---|
stores/baseData.ts |
新建 - 基础数据 Store |
views/login/index.vue |
登录成功后调用 fetchRegionTree(true) 和 fetchSubjects(true) |
components/TeacherIdentityForm.vue |
区域和学科使用 Store 缓存 |
components/ParentChildrenForm.vue |
区域使用 Store 缓存 |
C.5 缓存策略说明
| 数据 | 是否缓存 | 原因 |
|---|---|---|
区域树 /h5/base/regions |
✅ 缓存 | 数据量大且变化少 |
学科列表 /h5/base/subjects |
✅ 缓存 | 基础数据,变化少 |
学校列表 /h5/base/schools |
❌ 不缓存 | 依赖区域动态查询 |
年级列表 /h5/base/grades |
❌ 不缓存 | 依赖学校动态查询 |
班级列表 /h5/base/classes |
❌ 不缓存 | 依赖学校+年级动态查询 |