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")
@NotNull(message = "请选择班级")
private Long schoolClassId;
@Schema(description = "与学生的关系(父亲/母亲/其他)", example = "父亲")
private String relation;
}

View File

@ -52,4 +52,7 @@ public class H5StudentVo {
@Schema(description = "班级名称", example = "1班")
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.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<H5StudentVo> getStudents() {
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(
new LambdaQueryWrapper<PgStudent>()
.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<PgMemberStudent>()
.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());
@ -354,6 +396,12 @@ public class H5MemberServiceImpl implements H5MemberService {
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<PgMemberStudent>()
.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);
}

View File

@ -138,13 +138,13 @@ public class PgMemberController extends BaseController {
}
/**
* 解绑学生
* 解绑学生解绑指定会员与学生的关系
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE)
@PostMapping("/unbindStudent/{studentId}")
public R<Void> unbindStudent(@PathVariable Long studentId) {
return toAjax(studentService.unbindStudent(studentId));
@PostMapping("/{memberId}/unbindStudent/{studentId}")
public R<Void> unbindStudent(@PathVariable Long memberId, @PathVariable Long 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.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<org.dromara.pangu.student.domain.PgStudent>()
.eq(org.dromara.pangu.student.domain.PgStudent::getMemberId, memberId)
Long count = memberStudentMapper.selectCount(
new LambdaQueryWrapper<PgMemberStudent>()
.eq(PgMemberStudent::getMemberId, memberId)
);
return count == 0;
}

View File

@ -44,8 +44,6 @@ public class PgStudent extends BaseEntity {
private Long schoolClassId;
private Long memberId;
private String status;
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 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<MemberSimpleVo> members;
private String status;

View File

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

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.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<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery) {
@ -123,13 +124,13 @@ public class PgStudentServiceImpl implements IPgStudentService {
Set<Long> schoolIds = new HashSet<>();
Set<Long> schoolGradeIds = new HashSet<>();
Set<Long> schoolClassIds = new HashSet<>();
Set<Long> memberIds = new HashSet<>();
Set<Long> 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<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() :
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());
// 填充会员信息多对多
List<PgMemberStudent> relations = studentMemberMap.get(s.getStudentId());
if (relations != null && !relations.isEmpty()) {
List<MemberSimpleVo> members = new ArrayList<>();
for (PgMemberStudent rel : relations) {
PgMember member = memberMap.get(rel.getMemberId());
if (member != null) {
vo.setMemberNickname(member.getNickname());
vo.setMemberPhone(member.getPhone());
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<PgStudent> 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<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw);
// 转换为 VO填充学校年级班级名称
// 转换为 VO填充学校年级班级会员信息
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());
}
@ -237,39 +283,71 @@ public class PgStudentServiceImpl implements IPgStudentService {
if (memberId == null) {
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(
new LambdaQueryWrapper<PgStudent>()
.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<Long> 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<PgMemberStudent>()
.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<PgMemberStudent>()
.eq(PgMemberStudent::getStudentId, studentId)
);
}
/**
* 解绑指定会员与学生的关系
*/
public int unbindStudentFromMember(Long memberId, Long studentId) {
if (memberId == null || studentId == null) {
return 0;
}
return baseMapper.update(null,
new LambdaUpdateWrapper<PgStudent>()
.eq(PgStudent::getStudentId, studentId)
.set(PgStudent::getMemberId, null)
return memberStudentMapper.delete(
new LambdaQueryWrapper<PgMemberStudent>()
.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<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. 检查学号是否重复
// 6. 检查学号是否重复
if (StrUtil.isNotBlank(dto.getStudentNo())) {
PgStudent existStudent = baseMapper.selectOne(
new LambdaQueryWrapper<PgStudent>()
@ -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) {

View File

@ -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<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 {
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()

View File

@ -38,11 +38,17 @@
<el-table-column prop="schoolName" label="学校" min-width="150" show-overflow-tooltip />
<el-table-column prop="gradeName" label="年级" width="80" />
<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 }">
<el-tag v-if="row.memberId === memberId" type="success" size="small">已绑定</el-tag>
<el-tag v-else-if="row.memberId" type="warning" size="small">已被绑定</el-tag>
<el-tag v-else type="info" size="small">未绑定</el-tag>
<template v-if="isBoundToMe(row)">
<el-tag type="success" 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>
</el-table-column>
</el-table>
@ -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) {