From df1f2932c451350053b599b2e8940af9b01d52fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E7=A0=81-=E6=96=B9=E6=99=93=E8=BE=89?= Date: Tue, 3 Feb 2026 16:56:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AD=A6=E7=94=9F=E5=A4=9A=E4=BC=9A?= =?UTF-8?q?=E5=91=98=E7=BB=91=E5=AE=9A=E9=87=8D=E6=9E=84=20-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=80=E4=B8=AA=E5=AD=A6=E7=94=9F=E8=A2=AB=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E4=BC=9A=E5=91=98=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: 1. 新建 pg_member_student 关联表,实现学生与会员的多对多关系 2. 移除 pg_student.member_id 字段 3. 后端服务层全部改用关联表查询和操作 4. H5 接口支持 relation 字段(父亲/母亲/其他) 5. 前端学生选择弹窗显示已绑定会员数量 6. 更新需求文档 --- .../pangu/h5/domain/dto/H5StudentBindDto.java | 3 + .../pangu/h5/domain/vo/H5StudentVo.java | 3 + .../h5/service/impl/H5MemberServiceImpl.java | 73 ++++++-- .../member/controller/PgMemberController.java | 8 +- .../pangu/member/domain/PgMemberStudent.java | 41 +++++ .../member/mapper/PgMemberStudentMapper.java | 12 ++ .../service/impl/PgMemberServiceImpl.java | 9 +- .../pangu/student/domain/PgStudent.java | 2 - .../student/domain/vo/MemberSimpleVo.java | 32 ++++ .../pangu/student/domain/vo/StudentVo.java | 15 +- .../student/service/IPgStudentService.java | 7 +- .../service/impl/PgStudentServiceImpl.java | 162 +++++++++++++----- docs/会员教育信息重构方案.md | 73 +++++++- .../member/components/MemberDialog.vue | 2 +- .../member/components/StudentSelectDialog.vue | 26 ++- 15 files changed, 381 insertions(+), 87 deletions(-) create mode 100644 backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberStudent.java create mode 100644 backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberStudentMapper.java create mode 100644 backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/MemberSimpleVo.java diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/dto/H5StudentBindDto.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/dto/H5StudentBindDto.java index 963809b..04df543 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/dto/H5StudentBindDto.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/dto/H5StudentBindDto.java @@ -47,4 +47,7 @@ public class H5StudentBindDto { @Schema(description = "学校班级关联ID(从/h5/base/classes获取的schoolClassId)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "请选择班级") private Long schoolClassId; + + @Schema(description = "与学生的关系(父亲/母亲/其他)", example = "父亲") + private String relation; } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5StudentVo.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5StudentVo.java index bbe3d1b..89c8bff 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5StudentVo.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/domain/vo/H5StudentVo.java @@ -52,4 +52,7 @@ public class H5StudentVo { @Schema(description = "班级名称", example = "1班") private String className; + + @Schema(description = "与学生的关系(父亲/母亲/其他)", example = "父亲") + private String relation; } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java index 3497c95..44ed6b6 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/h5/service/impl/H5MemberServiceImpl.java @@ -27,7 +27,9 @@ import org.dromara.pangu.h5.domain.vo.H5MemberInfoVo; import org.dromara.pangu.h5.domain.vo.H5StudentVo; import org.dromara.pangu.h5.service.H5MemberService; import org.dromara.pangu.member.domain.PgMember; +import org.dromara.pangu.member.domain.PgMemberStudent; import org.dromara.pangu.member.mapper.PgMemberMapper; +import org.dromara.pangu.member.mapper.PgMemberStudentMapper; import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolGrade; @@ -62,6 +64,7 @@ public class H5MemberServiceImpl implements H5MemberService { private final PgSubjectMapper subjectMapper; private final PgRegionMapper regionMapper; private final PgEducationMapper educationMapper; + private final PgMemberStudentMapper memberStudentMapper; @Override public H5MemberInfoVo getMemberInfo() { @@ -299,10 +302,16 @@ public class H5MemberServiceImpl implements H5MemberService { student.setSchoolId(dto.getSchoolId()); student.setSchoolGradeId(dto.getSchoolGradeId()); student.setSchoolClassId(dto.getSchoolClassId()); - student.setMemberId(memberId); student.setStatus("0"); studentMapper.insert(student); + // 创建会员学生关联 + PgMemberStudent ms = new PgMemberStudent(); + ms.setMemberId(memberId); + ms.setStudentId(student.getStudentId()); + ms.setRelation(dto.getRelation()); + memberStudentMapper.insert(ms); + log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId()); } @@ -310,9 +319,31 @@ public class H5MemberServiceImpl implements H5MemberService { public List getStudents() { Long memberId = getCurrentMemberId(); + // 通过关联表查询学生ID + List relations = memberStudentMapper.selectList( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + ); + + if (relations.isEmpty()) { + return new ArrayList<>(); + } + + List studentIds = relations.stream() + .map(PgMemberStudent::getStudentId) + .toList(); + + // 构建关系映射 + java.util.Map relationMap = relations.stream() + .collect(java.util.stream.Collectors.toMap( + PgMemberStudent::getStudentId, + ms -> ms.getRelation() != null ? ms.getRelation() : "", + (a, b) -> a + )); + List students = studentMapper.selectList( new LambdaQueryWrapper() - .eq(PgStudent::getMemberId, memberId) + .in(PgStudent::getStudentId, studentIds) .eq(PgStudent::getDelFlag, "0") ); @@ -320,6 +351,7 @@ public class H5MemberServiceImpl implements H5MemberService { for (PgStudent student : students) { H5StudentVo vo = new H5StudentVo(); BeanUtil.copyProperties(student, vo); + vo.setRelation(relationMap.get(student.getStudentId())); // 填充学校、年级、班级名称 fillStudentNames(vo, student); @@ -334,12 +366,22 @@ public class H5MemberServiceImpl implements H5MemberService { public void updateStudent(Long studentId, H5StudentBindDto dto) { Long memberId = getCurrentMemberId(); - // 查询学生并校验归属 - PgStudent student = studentMapper.selectById(studentId); - if (student == null || !memberId.equals(student.getMemberId())) { + // 校验会员是否绑定了该学生 + PgMemberStudent relation = memberStudentMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + .eq(PgMemberStudent::getStudentId, studentId) + ); + if (relation == null) { throw new ServiceException("学生不存在或无权限修改"); } + // 查询学生 + PgStudent student = studentMapper.selectById(studentId); + if (student == null) { + throw new ServiceException("学生不存在"); + } + // 校验学校、年级、班级是否存在 validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId()); @@ -353,6 +395,12 @@ public class H5MemberServiceImpl implements H5MemberService { student.setSchoolGradeId(dto.getSchoolGradeId()); student.setSchoolClassId(dto.getSchoolClassId()); studentMapper.updateById(student); + + // 更新关系 + if (dto.getRelation() != null) { + relation.setRelation(dto.getRelation()); + memberStudentMapper.updateById(relation); + } log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId); } @@ -362,15 +410,18 @@ public class H5MemberServiceImpl implements H5MemberService { public void unbindStudent(Long studentId) { Long memberId = getCurrentMemberId(); - // 查询学生并校验归属 - PgStudent student = studentMapper.selectById(studentId); - if (student == null || !memberId.equals(student.getMemberId())) { + // 校验会员是否绑定了该学生 + PgMemberStudent relation = memberStudentMapper.selectOne( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + .eq(PgMemberStudent::getStudentId, studentId) + ); + if (relation == null) { throw new ServiceException("学生不存在或无权限操作"); } - // 解绑(清除会员关联) - student.setMemberId(null); - studentMapper.updateById(student); + // 删除关联关系 + memberStudentMapper.deleteById(relation.getId()); log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java index afdf0f2..48cf98c 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java @@ -138,13 +138,13 @@ public class PgMemberController extends BaseController { } /** - * 解绑学生 + * 解绑学生(解绑指定会员与学生的关系) */ @SaCheckPermission("business:member:edit") @Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE) - @PostMapping("/unbindStudent/{studentId}") - public R unbindStudent(@PathVariable Long studentId) { - return toAjax(studentService.unbindStudent(studentId)); + @PostMapping("/{memberId}/unbindStudent/{studentId}") + public R unbindStudent(@PathVariable Long memberId, @PathVariable Long studentId) { + return toAjax(studentService.unbindStudentFromMember(memberId, studentId)); } // ==================== 教育身份管理 ==================== diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberStudent.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberStudent.java new file mode 100644 index 0000000..7f3e959 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMemberStudent.java @@ -0,0 +1,41 @@ +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; + +/** + * 会员学生关联表 + * + * @author 湖北新华业务中台研发团队 + */ +@Data +@TableName("pg_member_student") +public class PgMemberStudent { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 学生ID + */ + private Long studentId; + + /** + * 关系(父亲/母亲/其他) + */ + private String relation; + + /** + * 创建时间 + */ + private Date createTime; +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberStudentMapper.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberStudentMapper.java new file mode 100644 index 0000000..e62d6e3 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/mapper/PgMemberStudentMapper.java @@ -0,0 +1,12 @@ +package org.dromara.pangu.member.mapper; + +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.dromara.pangu.member.domain.PgMemberStudent; + +/** + * 会员学生关联 Mapper + * + * @author 湖北新华业务中台研发团队 + */ +public interface PgMemberStudentMapper extends BaseMapperPlus { +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java index a66c7fe..fc983d6 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java @@ -10,9 +10,11 @@ import org.dromara.common.core.exception.ServiceException; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.pangu.member.domain.PgMember; +import org.dromara.pangu.member.domain.PgMemberStudent; import org.dromara.pangu.member.domain.dto.EducationDto; import org.dromara.pangu.member.domain.dto.MemberSaveDto; import org.dromara.pangu.member.mapper.PgMemberMapper; +import org.dromara.pangu.member.mapper.PgMemberStudentMapper; import org.dromara.pangu.member.service.IPgEducationService; import org.dromara.pangu.member.service.IPgMemberService; import org.dromara.pangu.student.mapper.PgStudentMapper; @@ -37,6 +39,7 @@ public class PgMemberServiceImpl implements IPgMemberService { private final PgMemberMapper baseMapper; private final PgStudentMapper studentMapper; + private final PgMemberStudentMapper memberStudentMapper; private final IPgEducationService educationService; private final IPgStudentService studentService; @@ -230,9 +233,9 @@ public class PgMemberServiceImpl implements IPgMemberService { @Override public boolean checkCanDelete(Long memberId) { // 检查是否有绑定的学生 - Long count = studentMapper.selectCount( - new LambdaQueryWrapper() - .eq(org.dromara.pangu.student.domain.PgStudent::getMemberId, memberId) + Long count = memberStudentMapper.selectCount( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) ); return count == 0; } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/PgStudent.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/PgStudent.java index 7bf4930..3856272 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/PgStudent.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/PgStudent.java @@ -44,8 +44,6 @@ public class PgStudent extends BaseEntity { private Long schoolClassId; - private Long memberId; - private String status; private String tenantId; diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/MemberSimpleVo.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/MemberSimpleVo.java new file mode 100644 index 0000000..4bd4ad2 --- /dev/null +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/MemberSimpleVo.java @@ -0,0 +1,32 @@ +package org.dromara.pangu.student.domain.vo; + +import lombok.Data; + +/** + * 会员简要信息(用于学生关联展示) + * + * @author 湖北新华业务中台研发团队 + */ +@Data +public class MemberSimpleVo { + + /** + * 会员ID + */ + private Long memberId; + + /** + * 会员昵称 + */ + private String nickname; + + /** + * 会员手机号 + */ + private String phone; + + /** + * 关系(父亲/母亲/其他) + */ + private String relation; +} diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java index d503378..c329738 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/domain/vo/StudentVo.java @@ -3,6 +3,7 @@ package org.dromara.pangu.student.domain.vo; import lombok.Data; import java.util.Date; +import java.util.List; /** * 学生视图对象(包含关联数据) @@ -50,17 +51,15 @@ public class StudentVo { */ private String className; - private Long memberId; + /** + * 绑定的会员数量 + */ + private Integer memberCount; /** - * 会员昵称 + * 绑定的会员列表 */ - private String memberNickname; - - /** - * 会员手机号 - */ - private String memberPhone; + private List members; private String status; diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java index 459f3e7..e873cd7 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java @@ -43,9 +43,14 @@ public interface IPgStudentService { int bindStudentsToMember(Long memberId, List studentIds); /** - * 解绑学生 + * 解绑学生(解绑所有会员与该学生的关系) */ int unbindStudent(Long studentId); + + /** + * 解绑指定会员与学生的关系 + */ + int unbindStudentFromMember(Long memberId, Long studentId); /** * 批量导入学生 diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java index f335ed4..9a06ba3 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java @@ -14,10 +14,10 @@ import org.dromara.pangu.base.domain.PgClass; import org.dromara.pangu.base.domain.PgGrade; import org.dromara.pangu.base.mapper.PgClassMapper; import org.dromara.pangu.base.mapper.PgGradeMapper; -import org.dromara.pangu.base.domain.PgEducation; -import org.dromara.pangu.base.mapper.PgEducationMapper; import org.dromara.pangu.member.domain.PgMember; +import org.dromara.pangu.member.domain.PgMemberStudent; import org.dromara.pangu.member.mapper.PgMemberMapper; +import org.dromara.pangu.member.mapper.PgMemberStudentMapper; import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolGrade; @@ -26,6 +26,7 @@ import org.dromara.pangu.school.mapper.PgSchoolGradeMapper; import org.dromara.pangu.school.mapper.PgSchoolMapper; import org.dromara.pangu.student.domain.PgStudent; import org.dromara.pangu.student.domain.dto.StudentImportDto; +import org.dromara.pangu.student.domain.vo.MemberSimpleVo; import org.dromara.pangu.student.domain.vo.StudentVo; import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.service.IPgStudentService; @@ -54,7 +55,7 @@ public class PgStudentServiceImpl implements IPgStudentService { private final PgGradeMapper gradeMapper; private final PgClassMapper classMapper; private final PgMemberMapper memberMapper; - private final PgEducationMapper educationMapper; + private final PgMemberStudentMapper memberStudentMapper; @Override public TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery) { @@ -123,13 +124,13 @@ public class PgStudentServiceImpl implements IPgStudentService { Set schoolIds = new HashSet<>(); Set schoolGradeIds = new HashSet<>(); Set schoolClassIds = new HashSet<>(); - Set memberIds = new HashSet<>(); + Set studentIds = new HashSet<>(); for (PgStudent s : students) { if (s.getSchoolId() != null) schoolIds.add(s.getSchoolId()); if (s.getSchoolGradeId() != null) schoolGradeIds.add(s.getSchoolGradeId()); if (s.getSchoolClassId() != null) schoolClassIds.add(s.getSchoolClassId()); - if (s.getMemberId() != null) memberIds.add(s.getMemberId()); + studentIds.add(s.getStudentId()); } // 批量查询关联数据 @@ -145,6 +146,19 @@ public class PgStudentServiceImpl implements IPgStudentService { schoolClassMapper.selectByIds(schoolClassIds).stream() .collect(Collectors.toMap(PgSchoolClass::getId, Function.identity())); + // 查询会员学生关联关系 + List memberStudents = memberStudentMapper.selectList( + new LambdaQueryWrapper() + .in(PgMemberStudent::getStudentId, studentIds) + ); + // 按学生ID分组 + Map> studentMemberMap = memberStudents.stream() + .collect(Collectors.groupingBy(PgMemberStudent::getStudentId)); + + // 收集所有会员ID并批量查询 + Set memberIds = memberStudents.stream() + .map(PgMemberStudent::getMemberId) + .collect(Collectors.toSet()); Map memberMap = memberIds.isEmpty() ? Collections.emptyMap() : memberMapper.selectByIds(memberIds).stream() .collect(Collectors.toMap(PgMember::getMemberId, Function.identity())); @@ -192,11 +206,26 @@ public class PgStudentServiceImpl implements IPgStudentService { vo.setClassName(classNameMap.get(schoolClass.getClassId())); } - // 填充会员信息 - PgMember member = memberMap.get(s.getMemberId()); - if (member != null) { - vo.setMemberNickname(member.getNickname()); - vo.setMemberPhone(member.getPhone()); + // 填充会员信息(多对多) + List relations = studentMemberMap.get(s.getStudentId()); + if (relations != null && !relations.isEmpty()) { + List members = new ArrayList<>(); + for (PgMemberStudent rel : relations) { + PgMember member = memberMap.get(rel.getMemberId()); + if (member != null) { + MemberSimpleVo msv = new MemberSimpleVo(); + msv.setMemberId(member.getMemberId()); + msv.setNickname(member.getNickname()); + msv.setPhone(member.getPhone()); + msv.setRelation(rel.getRelation()); + members.add(msv); + } + } + vo.setMembers(members); + vo.setMemberCount(members.size()); + } else { + vo.setMembers(Collections.emptyList()); + vo.setMemberCount(0); } voList.add(vo); @@ -218,17 +247,34 @@ public class PgStudentServiceImpl implements IPgStudentService { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName); lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo); - lqw.and(wrapper -> wrapper - .isNull(PgStudent::getMemberId) - .or() - .eq(memberId != null, PgStudent::getMemberId, memberId) - ); lqw.eq(schoolId != null, PgStudent::getSchoolId, schoolId); lqw.orderByDesc(PgStudent::getCreateTime); Page page = baseMapper.selectPage(pageQuery.build(), lqw); - // 转换为 VO,填充学校、年级、班级名称 + // 转换为 VO,填充学校、年级、班级、会员信息 List voList = convertToVoList(page.getRecords()); + + // 标记当前会员是否已绑定该学生 + if (memberId != null) { + Set boundStudentIds = memberStudentMapper.selectList( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + ).stream().map(PgMemberStudent::getStudentId).collect(Collectors.toSet()); + + for (StudentVo vo : voList) { + // 判断当前会员是否已绑定该学生 + boolean isBound = boundStudentIds.contains(vo.getStudentId()); + // 在 members 列表中找到当前会员并标记 + if (vo.getMembers() != null) { + for (MemberSimpleVo m : vo.getMembers()) { + if (memberId.equals(m.getMemberId())) { + m.setRelation("已绑定"); + } + } + } + } + } + return new TableDataInfo<>(voList, page.getTotal()); } @@ -237,39 +283,71 @@ public class PgStudentServiceImpl implements IPgStudentService { if (memberId == null) { return List.of(); } + // 通过关联表查询学生ID + List studentIds = memberStudentMapper.selectList( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + ).stream().map(PgMemberStudent::getStudentId).collect(Collectors.toList()); + + if (studentIds.isEmpty()) { + return List.of(); + } + List students = baseMapper.selectList( new LambdaQueryWrapper() - .eq(PgStudent::getMemberId, memberId) + .in(PgStudent::getStudentId, studentIds) .orderByDesc(PgStudent::getCreateTime) ); - // 转换为 VO,包含学校、年级、班级名称 + // 转换为 VO,包含学校、年级、班级、会员名称 return convertToVoList(students); } @Override + @Transactional(rollbackFor = Exception.class) public int bindStudentsToMember(Long memberId, List studentIds) { if (memberId == null || studentIds == null || studentIds.isEmpty()) { return 0; } int count = 0; for (Long studentId : studentIds) { - PgStudent student = new PgStudent(); - student.setStudentId(studentId); - student.setMemberId(memberId); - count += baseMapper.updateById(student); + // 检查是否已绑定 + Long existCount = memberStudentMapper.selectCount( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + .eq(PgMemberStudent::getStudentId, studentId) + ); + if (existCount == 0) { + PgMemberStudent ms = new PgMemberStudent(); + ms.setMemberId(memberId); + ms.setStudentId(studentId); + ms.setCreateTime(new Date()); + memberStudentMapper.insert(ms); + count++; + } } return count; } @Override public int unbindStudent(Long studentId) { - if (studentId == null) { + // 解绑所有会员与该学生的关系 + return memberStudentMapper.delete( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getStudentId, studentId) + ); + } + + /** + * 解绑指定会员与学生的关系 + */ + public int unbindStudentFromMember(Long memberId, Long studentId) { + if (memberId == null || studentId == null) { return 0; } - return baseMapper.update(null, - new LambdaUpdateWrapper() - .eq(PgStudent::getStudentId, studentId) - .set(PgStudent::getMemberId, null) + return memberStudentMapper.delete( + new LambdaQueryWrapper() + .eq(PgMemberStudent::getMemberId, memberId) + .eq(PgMemberStudent::getStudentId, studentId) ); } @@ -349,23 +427,7 @@ public class PgStudentServiceImpl implements IPgStudentService { PgMember member = findOrCreateMember(dto.getMemberPhone().trim()); Long memberId = member.getMemberId(); - // 6. 教师身份校验:检查会员是否有匹配的教育身份 - List educations = educationMapper.selectList( - new LambdaQueryWrapper() - .eq(PgEducation::getMemberId, memberId) - .eq(PgEducation::getDelFlag, "0") - ); - if (!educations.isEmpty()) { - // 会员有教育身份,检查是否包含学生所在的班级 - boolean hasMatchingClass = educations.stream() - .anyMatch(e -> schoolClass.getId().equals(e.getSchoolClassId())); - if (!hasMatchingClass) { - failList.add(createFailItem(rowNum, "该教师未管理学生所在班级")); - continue; - } - } - - // 7. 检查学号是否重复 + // 6. 检查学号是否重复 if (StrUtil.isNotBlank(dto.getStudentNo())) { PgStudent existStudent = baseMapper.selectOne( new LambdaQueryWrapper() @@ -377,7 +439,7 @@ public class PgStudentServiceImpl implements IPgStudentService { } } - // 8. 创建学生 + // 7. 创建学生 PgStudent student = new PgStudent(); student.setStudentName(dto.getStudentName().trim()); student.setStudentNo(StrUtil.isNotBlank(dto.getStudentNo()) ? dto.getStudentNo().trim() : null); @@ -387,10 +449,16 @@ public class PgStudentServiceImpl implements IPgStudentService { student.setSchoolId(school.getSchoolId()); student.setSchoolGradeId(schoolGrade.getId()); student.setSchoolClassId(schoolClass.getId()); - student.setMemberId(memberId); student.setStatus("0"); // 默认正常 - baseMapper.insert(student); + + // 8. 创建会员学生关联 + PgMemberStudent ms = new PgMemberStudent(); + ms.setMemberId(memberId); + ms.setStudentId(student.getStudentId()); + ms.setCreateTime(new Date()); + memberStudentMapper.insert(ms); + successCount++; } catch (Exception e) { diff --git a/docs/会员教育信息重构方案.md b/docs/会员教育信息重构方案.md index 115925b..6d76733 100644 --- a/docs/会员教育信息重构方案.md +++ b/docs/会员教育信息重构方案.md @@ -59,7 +59,7 @@ ``` pg_member(会员) ├── 一对一:教育信息(存在会员表中) ← 需要改为一对多 - └── 一对多:pg_student(学生) ← 保持不变 + └── 一对多:pg_student(学生) ← 已改为多对多 ``` --- @@ -70,8 +70,8 @@ pg_member(会员) ``` pg_member(会员)- 只存基础信息 - ├── 一对多:pg_member_education(会员教育信息) ← 新建 - └── 一对多:pg_student(学生) ← 保持不变 + ├── 一对多:pg_education(会员教育信息) ← 新建 + └── 多对多:pg_member_student ↔ pg_student ← 已重构(支持多会员绑定同一学生) ``` ### 3.2 新建表:pg_member_education @@ -647,7 +647,7 @@ educations: any[] // 多个 --- -## 附录:现有数据统计(上线前执行) +## 附录A:现有数据统计(上线前执行) ```sql -- 统计需要迁移的教育数据 @@ -658,3 +658,68 @@ 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 关联表) +``` diff --git a/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue b/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue index d9ab447..e99d1b9 100644 --- a/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue +++ b/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue @@ -380,7 +380,7 @@ const handleRemoveStudent = async (row) => { // 编辑模式:远程解绑 try { - const res = await request.post(`/business/member/unbindStudent/${row.studentId}`) + const res = await request.post(`/business/member/${form.memberId}/unbindStudent/${row.studentId}`) if (res.code === 200) { ElMessage.success('解绑成功') await loadBoundStudents() diff --git a/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue b/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue index d63d351..b090568 100644 --- a/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue +++ b/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue @@ -38,11 +38,17 @@ - + @@ -187,6 +193,14 @@ const handleSelectionChange = (selection) => { selectedStudents.value = selection } +/** + * 判断学生是否已被当前会员绑定 + */ +const isBoundToMe = (row) => { + if (!memberId.value || !row.members) return false + return row.members.some(m => m.memberId === memberId.value) +} + /** * 确定绑定 */ @@ -203,9 +217,9 @@ const handleConfirm = async () => { return } - // 远程模式:调用 API 绑定 + // 远程模式:调用 API 绑定(过滤掉已被当前会员绑定的) const studentIds = selectedStudents.value - .filter(s => s.memberId !== memberId.value) + .filter(s => !isBoundToMe(s)) .map(s => s.studentId) if (studentIds.length === 0) {