feat: 学生多会员绑定重构 - 支持一个学生被多个会员绑定

主要变更:
1. 新建 pg_member_student 关联表,实现学生与会员的多对多关系
2. 移除 pg_student.member_id 字段
3. 后端服务层全部改用关联表查询和操作
4. H5 接口支持 relation 字段(父亲/母亲/其他)
5. 前端学生选择弹窗显示已绑定会员数量
6. 更新需求文档
This commit is contained in:
神码-方晓辉 2026-02-03 16:56:18 +08:00
parent 729f2c71f1
commit df1f2932c4
15 changed files with 381 additions and 87 deletions

View File

@ -47,4 +47,7 @@ public class H5StudentBindDto {
@Schema(description = "学校班级关联ID从/h5/base/classes获取的schoolClassId", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "学校班级关联ID从/h5/base/classes获取的schoolClassId", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "请选择班级") @NotNull(message = "请选择班级")
private Long schoolClassId; private Long schoolClassId;
@Schema(description = "与学生的关系(父亲/母亲/其他)", example = "父亲")
private String relation;
} }

View File

@ -52,4 +52,7 @@ public class H5StudentVo {
@Schema(description = "班级名称", example = "1班") @Schema(description = "班级名称", example = "1班")
private String className; private String className;
@Schema(description = "与学生的关系(父亲/母亲/其他)", example = "父亲")
private String relation;
} }

View File

@ -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.domain.vo.H5StudentVo;
import org.dromara.pangu.h5.service.H5MemberService; import org.dromara.pangu.h5.service.H5MemberService;
import org.dromara.pangu.member.domain.PgMember; 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.PgMemberMapper;
import org.dromara.pangu.member.mapper.PgMemberStudentMapper;
import org.dromara.pangu.school.domain.PgSchool; import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade; import org.dromara.pangu.school.domain.PgSchoolGrade;
@ -62,6 +64,7 @@ public class H5MemberServiceImpl implements H5MemberService {
private final PgSubjectMapper subjectMapper; private final PgSubjectMapper subjectMapper;
private final PgRegionMapper regionMapper; private final PgRegionMapper regionMapper;
private final PgEducationMapper educationMapper; private final PgEducationMapper educationMapper;
private final PgMemberStudentMapper memberStudentMapper;
@Override @Override
public H5MemberInfoVo getMemberInfo() { public H5MemberInfoVo getMemberInfo() {
@ -299,10 +302,16 @@ public class H5MemberServiceImpl implements H5MemberService {
student.setSchoolId(dto.getSchoolId()); student.setSchoolId(dto.getSchoolId());
student.setSchoolGradeId(dto.getSchoolGradeId()); student.setSchoolGradeId(dto.getSchoolGradeId());
student.setSchoolClassId(dto.getSchoolClassId()); student.setSchoolClassId(dto.getSchoolClassId());
student.setMemberId(memberId);
student.setStatus("0"); student.setStatus("0");
studentMapper.insert(student); 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()); log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId());
} }
@ -310,9 +319,31 @@ public class H5MemberServiceImpl implements H5MemberService {
public List<H5StudentVo> getStudents() { public List<H5StudentVo> getStudents() {
Long memberId = getCurrentMemberId(); Long memberId = getCurrentMemberId();
// 通过关联表查询学生ID
List<PgMemberStudent> relations = memberStudentMapper.selectList(
new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getMemberId, memberId)
);
if (relations.isEmpty()) {
return new ArrayList<>();
}
List<Long> studentIds = relations.stream()
.map(PgMemberStudent::getStudentId)
.toList();
// 构建关系映射
java.util.Map<Long, String> relationMap = relations.stream()
.collect(java.util.stream.Collectors.toMap(
PgMemberStudent::getStudentId,
ms -> ms.getRelation() != null ? ms.getRelation() : "",
(a, b) -> a
));
List<PgStudent> students = studentMapper.selectList( List<PgStudent> students = studentMapper.selectList(
new LambdaQueryWrapper<PgStudent>() new LambdaQueryWrapper<PgStudent>()
.eq(PgStudent::getMemberId, memberId) .in(PgStudent::getStudentId, studentIds)
.eq(PgStudent::getDelFlag, "0") .eq(PgStudent::getDelFlag, "0")
); );
@ -320,6 +351,7 @@ public class H5MemberServiceImpl implements H5MemberService {
for (PgStudent student : students) { for (PgStudent student : students) {
H5StudentVo vo = new H5StudentVo(); H5StudentVo vo = new H5StudentVo();
BeanUtil.copyProperties(student, vo); BeanUtil.copyProperties(student, vo);
vo.setRelation(relationMap.get(student.getStudentId()));
// 填充学校年级班级名称 // 填充学校年级班级名称
fillStudentNames(vo, student); fillStudentNames(vo, student);
@ -334,12 +366,22 @@ public class H5MemberServiceImpl implements H5MemberService {
public void updateStudent(Long studentId, H5StudentBindDto dto) { public void updateStudent(Long studentId, H5StudentBindDto dto) {
Long memberId = getCurrentMemberId(); Long memberId = getCurrentMemberId();
// 查询学生并校验归属 // 校验会员是否绑定了该学生
PgStudent student = studentMapper.selectById(studentId); PgMemberStudent relation = memberStudentMapper.selectOne(
if (student == null || !memberId.equals(student.getMemberId())) { new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getMemberId, memberId)
.eq(PgMemberStudent::getStudentId, studentId)
);
if (relation == null) {
throw new ServiceException("学生不存在或无权限修改"); throw new ServiceException("学生不存在或无权限修改");
} }
// 查询学生
PgStudent student = studentMapper.selectById(studentId);
if (student == null) {
throw new ServiceException("学生不存在");
}
// 校验学校年级班级是否存在 // 校验学校年级班级是否存在
validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId()); validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId());
@ -353,6 +395,12 @@ public class H5MemberServiceImpl implements H5MemberService {
student.setSchoolGradeId(dto.getSchoolGradeId()); student.setSchoolGradeId(dto.getSchoolGradeId());
student.setSchoolClassId(dto.getSchoolClassId()); student.setSchoolClassId(dto.getSchoolClassId());
studentMapper.updateById(student); studentMapper.updateById(student);
// 更新关系
if (dto.getRelation() != null) {
relation.setRelation(dto.getRelation());
memberStudentMapper.updateById(relation);
}
log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId); log.info("H5修改学生: memberId={}, studentId={}", memberId, studentId);
} }
@ -362,15 +410,18 @@ public class H5MemberServiceImpl implements H5MemberService {
public void unbindStudent(Long studentId) { public void unbindStudent(Long studentId) {
Long memberId = getCurrentMemberId(); Long memberId = getCurrentMemberId();
// 查询学生并校验归属 // 校验会员是否绑定了该学生
PgStudent student = studentMapper.selectById(studentId); PgMemberStudent relation = memberStudentMapper.selectOne(
if (student == null || !memberId.equals(student.getMemberId())) { new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getMemberId, memberId)
.eq(PgMemberStudent::getStudentId, studentId)
);
if (relation == null) {
throw new ServiceException("学生不存在或无权限操作"); throw new ServiceException("学生不存在或无权限操作");
} }
// 解绑清除会员关联 // 删除关联关系
student.setMemberId(null); memberStudentMapper.deleteById(relation.getId());
studentMapper.updateById(student);
log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId); log.info("H5解绑学生: memberId={}, studentId={}", memberId, studentId);
} }

View File

@ -138,13 +138,13 @@ public class PgMemberController extends BaseController {
} }
/** /**
* 解绑学生 * 解绑学生解绑指定会员与学生的关系
*/ */
@SaCheckPermission("business:member:edit") @SaCheckPermission("business:member:edit")
@Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE) @Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE)
@PostMapping("/unbindStudent/{studentId}") @PostMapping("/{memberId}/unbindStudent/{studentId}")
public R<Void> unbindStudent(@PathVariable Long studentId) { public R<Void> unbindStudent(@PathVariable Long memberId, @PathVariable Long studentId) {
return toAjax(studentService.unbindStudent(studentId)); return toAjax(studentService.unbindStudentFromMember(memberId, studentId));
} }
// ==================== 教育身份管理 ==================== // ==================== 教育身份管理 ====================

View File

@ -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;
}

View File

@ -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<PgMemberStudent, PgMemberStudent> {
}

View File

@ -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.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.pangu.member.domain.PgMember; 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.EducationDto;
import org.dromara.pangu.member.domain.dto.MemberSaveDto; import org.dromara.pangu.member.domain.dto.MemberSaveDto;
import org.dromara.pangu.member.mapper.PgMemberMapper; 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.IPgEducationService;
import org.dromara.pangu.member.service.IPgMemberService; import org.dromara.pangu.member.service.IPgMemberService;
import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.mapper.PgStudentMapper;
@ -37,6 +39,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
private final PgMemberMapper baseMapper; private final PgMemberMapper baseMapper;
private final PgStudentMapper studentMapper; private final PgStudentMapper studentMapper;
private final PgMemberStudentMapper memberStudentMapper;
private final IPgEducationService educationService; private final IPgEducationService educationService;
private final IPgStudentService studentService; private final IPgStudentService studentService;
@ -230,9 +233,9 @@ public class PgMemberServiceImpl implements IPgMemberService {
@Override @Override
public boolean checkCanDelete(Long memberId) { public boolean checkCanDelete(Long memberId) {
// 检查是否有绑定的学生 // 检查是否有绑定的学生
Long count = studentMapper.selectCount( Long count = memberStudentMapper.selectCount(
new LambdaQueryWrapper<org.dromara.pangu.student.domain.PgStudent>() new LambdaQueryWrapper<PgMemberStudent>()
.eq(org.dromara.pangu.student.domain.PgStudent::getMemberId, memberId) .eq(PgMemberStudent::getMemberId, memberId)
); );
return count == 0; return count == 0;
} }

View File

@ -44,8 +44,6 @@ public class PgStudent extends BaseEntity {
private Long schoolClassId; private Long schoolClassId;
private Long memberId;
private String status; private String status;
private String tenantId; private String tenantId;

View File

@ -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;
}

View File

@ -3,6 +3,7 @@ package org.dromara.pangu.student.domain.vo;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 学生视图对象包含关联数据 * 学生视图对象包含关联数据
@ -50,17 +51,15 @@ public class StudentVo {
*/ */
private String className; private String className;
private Long memberId; /**
* 绑定的会员数量
*/
private Integer memberCount;
/** /**
* 会员昵称 * 绑定的会员列表
*/ */
private String memberNickname; private List<MemberSimpleVo> members;
/**
* 会员手机号
*/
private String memberPhone;
private String status; private String status;

View File

@ -43,9 +43,14 @@ public interface IPgStudentService {
int bindStudentsToMember(Long memberId, List<Long> studentIds); int bindStudentsToMember(Long memberId, List<Long> studentIds);
/** /**
* 解绑学生 * 解绑学生解绑所有会员与该学生的关系
*/ */
int unbindStudent(Long studentId); int unbindStudent(Long studentId);
/**
* 解绑指定会员与学生的关系
*/
int unbindStudentFromMember(Long memberId, Long studentId);
/** /**
* 批量导入学生 * 批量导入学生

View File

@ -14,10 +14,10 @@ import org.dromara.pangu.base.domain.PgClass;
import org.dromara.pangu.base.domain.PgGrade; import org.dromara.pangu.base.domain.PgGrade;
import org.dromara.pangu.base.mapper.PgClassMapper; import org.dromara.pangu.base.mapper.PgClassMapper;
import org.dromara.pangu.base.mapper.PgGradeMapper; 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.PgMember;
import org.dromara.pangu.member.domain.PgMemberStudent;
import org.dromara.pangu.member.mapper.PgMemberMapper; 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.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass; import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade; 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.school.mapper.PgSchoolMapper;
import org.dromara.pangu.student.domain.PgStudent; import org.dromara.pangu.student.domain.PgStudent;
import org.dromara.pangu.student.domain.dto.StudentImportDto; 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.domain.vo.StudentVo;
import org.dromara.pangu.student.mapper.PgStudentMapper; import org.dromara.pangu.student.mapper.PgStudentMapper;
import org.dromara.pangu.student.service.IPgStudentService; import org.dromara.pangu.student.service.IPgStudentService;
@ -54,7 +55,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
private final PgGradeMapper gradeMapper; private final PgGradeMapper gradeMapper;
private final PgClassMapper classMapper; private final PgClassMapper classMapper;
private final PgMemberMapper memberMapper; private final PgMemberMapper memberMapper;
private final PgEducationMapper educationMapper; private final PgMemberStudentMapper memberStudentMapper;
@Override @Override
public TableDataInfo<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery) { public TableDataInfo<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery) {
@ -123,13 +124,13 @@ public class PgStudentServiceImpl implements IPgStudentService {
Set<Long> schoolIds = new HashSet<>(); Set<Long> schoolIds = new HashSet<>();
Set<Long> schoolGradeIds = new HashSet<>(); Set<Long> schoolGradeIds = new HashSet<>();
Set<Long> schoolClassIds = new HashSet<>(); Set<Long> schoolClassIds = new HashSet<>();
Set<Long> memberIds = new HashSet<>(); Set<Long> studentIds = new HashSet<>();
for (PgStudent s : students) { for (PgStudent s : students) {
if (s.getSchoolId() != null) schoolIds.add(s.getSchoolId()); if (s.getSchoolId() != null) schoolIds.add(s.getSchoolId());
if (s.getSchoolGradeId() != null) schoolGradeIds.add(s.getSchoolGradeId()); if (s.getSchoolGradeId() != null) schoolGradeIds.add(s.getSchoolGradeId());
if (s.getSchoolClassId() != null) schoolClassIds.add(s.getSchoolClassId()); 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() schoolClassMapper.selectByIds(schoolClassIds).stream()
.collect(Collectors.toMap(PgSchoolClass::getId, Function.identity())); .collect(Collectors.toMap(PgSchoolClass::getId, Function.identity()));
// 查询会员学生关联关系
List<PgMemberStudent> memberStudents = memberStudentMapper.selectList(
new LambdaQueryWrapper<PgMemberStudent>()
.in(PgMemberStudent::getStudentId, studentIds)
);
// 按学生ID分组
Map<Long, List<PgMemberStudent>> studentMemberMap = memberStudents.stream()
.collect(Collectors.groupingBy(PgMemberStudent::getStudentId));
// 收集所有会员ID并批量查询
Set<Long> memberIds = memberStudents.stream()
.map(PgMemberStudent::getMemberId)
.collect(Collectors.toSet());
Map<Long, PgMember> memberMap = memberIds.isEmpty() ? Collections.emptyMap() : Map<Long, PgMember> memberMap = memberIds.isEmpty() ? Collections.emptyMap() :
memberMapper.selectByIds(memberIds).stream() memberMapper.selectByIds(memberIds).stream()
.collect(Collectors.toMap(PgMember::getMemberId, Function.identity())); .collect(Collectors.toMap(PgMember::getMemberId, Function.identity()));
@ -192,11 +206,26 @@ public class PgStudentServiceImpl implements IPgStudentService {
vo.setClassName(classNameMap.get(schoolClass.getClassId())); vo.setClassName(classNameMap.get(schoolClass.getClassId()));
} }
// 填充会员信息 // 填充会员信息多对多
PgMember member = memberMap.get(s.getMemberId()); List<PgMemberStudent> relations = studentMemberMap.get(s.getStudentId());
if (member != null) { if (relations != null && !relations.isEmpty()) {
vo.setMemberNickname(member.getNickname()); List<MemberSimpleVo> members = new ArrayList<>();
vo.setMemberPhone(member.getPhone()); 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); voList.add(vo);
@ -218,17 +247,34 @@ public class PgStudentServiceImpl implements IPgStudentService {
LambdaQueryWrapper<PgStudent> lqw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<PgStudent> lqw = new LambdaQueryWrapper<>();
lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName); lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName);
lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo); 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.eq(schoolId != null, PgStudent::getSchoolId, schoolId);
lqw.orderByDesc(PgStudent::getCreateTime); lqw.orderByDesc(PgStudent::getCreateTime);
Page<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw); Page<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw);
// 转换为 VO填充学校年级班级名称 // 转换为 VO填充学校年级班级会员信息
List<StudentVo> voList = convertToVoList(page.getRecords()); List<StudentVo> voList = convertToVoList(page.getRecords());
// 标记当前会员是否已绑定该学生
if (memberId != null) {
Set<Long> boundStudentIds = memberStudentMapper.selectList(
new LambdaQueryWrapper<PgMemberStudent>()
.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()); return new TableDataInfo<>(voList, page.getTotal());
} }
@ -237,39 +283,71 @@ public class PgStudentServiceImpl implements IPgStudentService {
if (memberId == null) { if (memberId == null) {
return List.of(); return List.of();
} }
// 通过关联表查询学生ID
List<Long> studentIds = memberStudentMapper.selectList(
new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getMemberId, memberId)
).stream().map(PgMemberStudent::getStudentId).collect(Collectors.toList());
if (studentIds.isEmpty()) {
return List.of();
}
List<PgStudent> students = baseMapper.selectList( List<PgStudent> students = baseMapper.selectList(
new LambdaQueryWrapper<PgStudent>() new LambdaQueryWrapper<PgStudent>()
.eq(PgStudent::getMemberId, memberId) .in(PgStudent::getStudentId, studentIds)
.orderByDesc(PgStudent::getCreateTime) .orderByDesc(PgStudent::getCreateTime)
); );
// 转换为 VO包含学校年级班级名称 // 转换为 VO包含学校年级班级会员名称
return convertToVoList(students); return convertToVoList(students);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public int bindStudentsToMember(Long memberId, List<Long> studentIds) { public int bindStudentsToMember(Long memberId, List<Long> studentIds) {
if (memberId == null || studentIds == null || studentIds.isEmpty()) { if (memberId == null || studentIds == null || studentIds.isEmpty()) {
return 0; return 0;
} }
int count = 0; int count = 0;
for (Long studentId : studentIds) { for (Long studentId : studentIds) {
PgStudent student = new PgStudent(); // 检查是否已绑定
student.setStudentId(studentId); Long existCount = memberStudentMapper.selectCount(
student.setMemberId(memberId); new LambdaQueryWrapper<PgMemberStudent>()
count += baseMapper.updateById(student); .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; return count;
} }
@Override @Override
public int unbindStudent(Long studentId) { public int unbindStudent(Long studentId) {
if (studentId == null) { // 解绑所有会员与该学生的关系
return memberStudentMapper.delete(
new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getStudentId, studentId)
);
}
/**
* 解绑指定会员与学生的关系
*/
public int unbindStudentFromMember(Long memberId, Long studentId) {
if (memberId == null || studentId == null) {
return 0; return 0;
} }
return baseMapper.update(null, return memberStudentMapper.delete(
new LambdaUpdateWrapper<PgStudent>() new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgStudent::getStudentId, studentId) .eq(PgMemberStudent::getMemberId, memberId)
.set(PgStudent::getMemberId, null) .eq(PgMemberStudent::getStudentId, studentId)
); );
} }
@ -349,23 +427,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
PgMember member = findOrCreateMember(dto.getMemberPhone().trim()); PgMember member = findOrCreateMember(dto.getMemberPhone().trim());
Long memberId = member.getMemberId(); Long memberId = member.getMemberId();
// 6. 教师身份校验检查会员是否有匹配的教育身份 // 6. 检查学号是否重复
List<PgEducation> educations = educationMapper.selectList(
new LambdaQueryWrapper<PgEducation>()
.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. 检查学号是否重复
if (StrUtil.isNotBlank(dto.getStudentNo())) { if (StrUtil.isNotBlank(dto.getStudentNo())) {
PgStudent existStudent = baseMapper.selectOne( PgStudent existStudent = baseMapper.selectOne(
new LambdaQueryWrapper<PgStudent>() new LambdaQueryWrapper<PgStudent>()
@ -377,7 +439,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
} }
} }
// 8. 创建学生 // 7. 创建学生
PgStudent student = new PgStudent(); PgStudent student = new PgStudent();
student.setStudentName(dto.getStudentName().trim()); student.setStudentName(dto.getStudentName().trim());
student.setStudentNo(StrUtil.isNotBlank(dto.getStudentNo()) ? dto.getStudentNo().trim() : null); student.setStudentNo(StrUtil.isNotBlank(dto.getStudentNo()) ? dto.getStudentNo().trim() : null);
@ -387,10 +449,16 @@ public class PgStudentServiceImpl implements IPgStudentService {
student.setSchoolId(school.getSchoolId()); student.setSchoolId(school.getSchoolId());
student.setSchoolGradeId(schoolGrade.getId()); student.setSchoolGradeId(schoolGrade.getId());
student.setSchoolClassId(schoolClass.getId()); student.setSchoolClassId(schoolClass.getId());
student.setMemberId(memberId);
student.setStatus("0"); // 默认正常 student.setStatus("0"); // 默认正常
baseMapper.insert(student); baseMapper.insert(student);
// 8. 创建会员学生关联
PgMemberStudent ms = new PgMemberStudent();
ms.setMemberId(memberId);
ms.setStudentId(student.getStudentId());
ms.setCreateTime(new Date());
memberStudentMapper.insert(ms);
successCount++; successCount++;
} catch (Exception e) { } catch (Exception e) {

View File

@ -59,7 +59,7 @@
``` ```
pg_member会员 pg_member会员
├── 一对一:教育信息(存在会员表中) ← 需要改为一对多 ├── 一对一:教育信息(存在会员表中) ← 需要改为一对多
└── 一对多pg_student学生保持不变 └── 一对多pg_student学生已改为多对多
``` ```
--- ---
@ -70,8 +70,8 @@ pg_member会员
``` ```
pg_member会员- 只存基础信息 pg_member会员- 只存基础信息
├── 一对多pg_member_education会员教育信息 ← 新建 ├── 一对多pg_education会员教育信息 ← 新建
└── 一对多pg_student学生 ← 保持不变 └── 多对多pg_member_student ↔ pg_student ← 已重构(支持多会员绑定同一学生)
``` ```
### 3.2 新建表pg_member_education ### 3.2 新建表pg_member_education
@ -647,7 +647,7 @@ educations: any[] // 多个
--- ---
## 附录:现有数据统计(上线前执行) ## 附录A:现有数据统计(上线前执行)
```sql ```sql
-- 统计需要迁移的教育数据 -- 统计需要迁移的教育数据
@ -658,3 +658,68 @@ WHERE identity_type = '2' AND school_id IS NOT NULL AND del_flag = '0';
SELECT COUNT(*) FROM pg_member SELECT COUNT(*) FROM pg_member
WHERE identity_type = '2' AND school_class_id IS NOT NULL AND del_flag = '0'; 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<MemberSimpleVo> 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 关联表)
```

View File

@ -380,7 +380,7 @@ const handleRemoveStudent = async (row) => {
// //
try { 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) { if (res.code === 200) {
ElMessage.success('解绑成功') ElMessage.success('解绑成功')
await loadBoundStudents() await loadBoundStudents()

View File

@ -38,11 +38,17 @@
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip /> <el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
<el-table-column prop="gradeName" label="年级" width="80" /> <el-table-column prop="gradeName" label="年级" width="80" />
<el-table-column prop="className" label="班级" width="60" /> <el-table-column prop="className" label="班级" width="60" />
<el-table-column label="绑定状态" width="100" align="center"> <el-table-column label="绑定状态" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.memberId === memberId" type="success" size="small">已绑定</el-tag> <template v-if="isBoundToMe(row)">
<el-tag v-else-if="row.memberId" type="warning" size="small">已被绑定</el-tag> <el-tag type="success" size="small">已绑定</el-tag>
<el-tag v-else type="info" size="small">未绑定</el-tag> </template>
<template v-else-if="row.memberCount > 0">
<el-tag type="warning" size="small">{{ row.memberCount }}人已绑</el-tag>
</template>
<template v-else>
<el-tag type="info" size="small">未绑定</el-tag>
</template>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -187,6 +193,14 @@ const handleSelectionChange = (selection) => {
selectedStudents.value = 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 return
} }
// API // API
const studentIds = selectedStudents.value const studentIds = selectedStudents.value
.filter(s => s.memberId !== memberId.value) .filter(s => !isBoundToMe(s))
.map(s => s.studentId) .map(s => s.studentId)
if (studentIds.length === 0) { if (studentIds.length === 0) {