# 会员教育信息重构方案 > 作者:湖北新华业务中台研发团队 > 创建时间:2026-02-03 > 状态:**已审核通过** --- ## 一、需求变更说明 ### 1.1 变更内容 1. **去掉身份类型**:会员不再区分"家长"和"教师" 2. **重构会员信息表**:会员与教育信息变为**一对多**关系,一个会员可以有多个教育身份 3. **pg_member 表移除字段**: - `region_id` - 区域ID - `school_id` - 学校ID - `school_grade_id` - 年级ID - `school_class_id` - 班级ID - `identity_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班 - 数学 ``` ```sql 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` **响应变更**: ```json // 修改前 { "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` **响应**: ```json { "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` **请求参数**: ```json { "schoolId": 1, "schoolGradeId": 5, "schoolClassId": 104, "subjectId": 1 } ``` #### 5.1.4 编辑教育身份 **接口**:`PUT /h5/member/educations/{educationId}` **请求参数**: ```json { "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脚本 ```sql -- 新建会员教育信息表(一个教育身份 = 一个班级的教学关系) 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 数据迁移脚本 ```sql -- 迁移现有教育数据(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 删除字段(迁移完成后执行) ```sql -- 删除 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** ```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 主要改动**: 1. `getEducation()` → `getEducations()` 返回列表 2. `saveEducation()` 支持新增/编辑 3. `deleteEducation(Long educationId)` 按ID删除 4. 新增 `setDefaultEducation(Long educationId)` 设置默认 ### 6.3 管理后台前端改造 #### 6.3.1 会员列表页(index.vue) **删除内容**: ```vue ``` #### 6.3.2 会员编辑弹窗(MemberDialog.vue) **删除内容**: ```vue ``` #### 6.3.3 新建会员详情页 **MemberDetail.vue**(新建) ```vue ``` #### 6.3.4 新建教育身份Tab **EducationTab.vue**(新建) ```vue ``` ### 6.4 H5前端改造 #### 6.4.1 user.ts 修改 ```typescript // 删除 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 | 确认无问题后 | --- ## 十、回滚方案 1. 后端代码回滚 2. 前端代码回滚 3. 数据库: - 从新表恢复数据到 pg_member - 删除新建的两张表 --- ## 十一、风险评估 | 风险 | 等级 | 应对措施 | |------|------|----------| | 数据迁移丢失 | 高 | 完整备份,测试环境验证 | | 接口不兼容 | 高 | 前后端同步上线 | | 性能问题 | 低 | 新表已加索引 | | 业务中断 | 中 | 选择低峰期上线 | --- ## 附录A:现有数据统计(上线前执行) ```sql -- 统计需要迁移的教育数据 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` ```sql 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='会员学生关联表'; ``` #### 数据迁移 ```sql -- 迁移现有绑定关系 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 | **接口变更**: ```javascript // 变更前 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` 实现区域和学科数据的前端缓存: ```typescript // 缓存结构 { 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` | ❌ 不缓存 | 依赖学校+年级动态查询 |