feat: 会员教育信息重构 - 支持多教育身份管理

主要变更:
1. 去掉身份类型字段,会员与教育信息改为一对多关系
2. 新增 pg_education 表及相关实体、服务
3. 管理后台支持添加、编辑、删除多个教育身份
4. 新增会员时支持一次性保存会员信息、教育身份、亲子关系
5. 修复学生选择弹窗学校年级班级不显示问题
6. 添加区域路径接口用于级联选择器回显
This commit is contained in:
神码-方晓辉 2026-02-03 16:35:57 +08:00
parent acdade6725
commit 729f2c71f1
29 changed files with 1517 additions and 533 deletions

View File

@ -51,6 +51,15 @@ public class PgRegionController extends BaseController {
return R.ok(regionService.selectById(regionId));
}
/**
* 获取区域路径从根到当前区域的ID列表
* 用于级联选择器回显
*/
@GetMapping("/{regionId}/path")
public R<List<Long>> getRegionPath(@PathVariable Long regionId) {
return R.ok(regionService.getRegionPath(regionId));
}
@SaCheckPermission("business:region:add")
@Log(title = "区域管理", businessType = BusinessType.INSERT)
@PostMapping

View File

@ -0,0 +1,67 @@
package org.dromara.pangu.base.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
/**
* 教育信息表会员教育身份
*
* @author 湖北新华业务中台研发团队
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pg_education")
public class PgEducation 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;
/**
* 是否默认身份0否 1是
*/
private String isDefault;
/**
* 状态0正常 1停用
*/
private String status;
private String tenantId;
@TableLogic
private String delFlag;
// 非数据库字段用于VO展示
@TableField(exist = false)
private String regionName;
@TableField(exist = false)
private String schoolName;
@TableField(exist = false)
private String gradeName;
@TableField(exist = false)
private String className;
@TableField(exist = false)
private String subjectName;
}

View File

@ -0,0 +1,12 @@
package org.dromara.pangu.base.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.pangu.base.domain.PgEducation;
/**
* 教育信息 Mapper 接口
*
* @author 湖北新华业务中台研发团队
*/
public interface PgEducationMapper extends BaseMapperPlus<PgEducation, PgEducation> {
}

View File

@ -14,6 +14,7 @@ public interface IPgRegionService {
List<PgRegion> selectTree();
PgRegion selectById(Long regionId);
List<PgRegion> selectByParentId(Long parentId);
List<Long> getRegionPath(Long regionId);
int insert(PgRegion region);
int update(PgRegion region);
int deleteByIds(Long[] regionIds);

View File

@ -69,6 +69,30 @@ public class PgRegionServiceImpl implements IPgRegionService {
.orderByAsc(PgRegion::getOrderNum));
}
@Override
public List<Long> getRegionPath(Long regionId) {
if (regionId == null) {
return new ArrayList<>();
}
PgRegion region = baseMapper.selectById(regionId);
if (region == null) {
return new ArrayList<>();
}
List<Long> path = new ArrayList<>();
// ancestors 格式: "0,42,4201" 表示从根到父的路径
if (StrUtil.isNotBlank(region.getAncestors())) {
String[] ancestorIds = region.getAncestors().split(",");
for (String id : ancestorIds) {
if (StrUtil.isNotBlank(id) && !"0".equals(id)) {
path.add(Long.parseLong(id));
}
}
}
// 最后加上当前区域ID
path.add(regionId);
return path;
}
@Override
public int insert(PgRegion region) {
return baseMapper.insert(region);

View File

@ -122,12 +122,12 @@ public class H5MemberController {
}
/**
* 添加/修改教育身份
* 添加教育身份
*/
@Operation(
summary = "设置教育身份(教师)",
summary = "添加教育身份",
description = """
设置或修改会员的教育身份信息设置后身份类型变为"教师"
添加一个教育身份一个会员可以有多个教育身份每班一条记录
**必填信息**
- 学校
@ -143,31 +143,52 @@ public class H5MemberController {
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "设置成功"),
@ApiResponse(responseCode = "200", description = "添加成功"),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期"),
@ApiResponse(responseCode = "500", description = "设置失败,可能原因:学校/年级/班级/学科不存在")
@ApiResponse(responseCode = "500", description = "添加失败,可能原因:学校/年级/班级/学科不存在")
})
@PostMapping("/education")
public R<Void> saveEducation(@Valid @RequestBody H5EducationDto dto) {
memberService.saveEducation(dto);
@PostMapping("/educations")
public R<Void> addEducation(@Valid @RequestBody H5EducationDto dto) {
memberService.addEducation(dto);
return R.ok();
}
/**
* 获取教育身份
* 修改教育身份
*/
@Operation(
summary = "获取教育身份信息",
description = "获取当前会员的教育身份信息学校、年级、班级、学科。如果未设置教育身份返回null。"
summary = "修改教育身份",
description = "修改指定的教育身份信息。"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "修改成功"),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期"),
@ApiResponse(responseCode = "500", description = "修改失败,可能原因:教育身份不存在或无权限")
})
@Parameters({
@Parameter(name = "educationId", description = "教育身份ID", required = true, in = ParameterIn.PATH)
})
@PutMapping("/educations/{educationId}")
public R<Void> updateEducation(@PathVariable Long educationId, @Valid @RequestBody H5EducationDto dto) {
memberService.updateEducation(educationId, dto);
return R.ok();
}
/**
* 获取教育身份列表
*/
@Operation(
summary = "获取教育身份列表",
description = "获取当前会员的所有教育身份信息(学校、年级、班级、学科)。"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "获取成功",
content = @Content(schema = @Schema(implementation = H5EducationVo.class))),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期")
})
@GetMapping("/education")
public R<H5EducationVo> getEducation() {
return R.ok(memberService.getEducation());
@GetMapping("/educations")
public R<List<H5EducationVo>> getEducations() {
return R.ok(memberService.getEducations());
}
/**
@ -175,15 +196,40 @@ public class H5MemberController {
*/
@Operation(
summary = "删除教育身份",
description = "清除当前会员的教育身份信息,身份类型将变为空"
description = "删除指定的教育身份"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "删除成功"),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期")
@ApiResponse(responseCode = "401", description = "未登录或Token已过期"),
@ApiResponse(responseCode = "500", description = "删除失败,可能原因:教育身份不存在或无权限")
})
@DeleteMapping("/education")
public R<Void> deleteEducation() {
memberService.deleteEducation();
@Parameters({
@Parameter(name = "educationId", description = "教育身份ID", required = true, in = ParameterIn.PATH)
})
@DeleteMapping("/educations/{educationId}")
public R<Void> deleteEducation(@PathVariable Long educationId) {
memberService.deleteEducation(educationId);
return R.ok();
}
/**
* 设置默认教育身份
*/
@Operation(
summary = "设置默认教育身份",
description = "将指定教育身份设为默认。"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "设置成功"),
@ApiResponse(responseCode = "401", description = "未登录或Token已过期"),
@ApiResponse(responseCode = "500", description = "设置失败,可能原因:教育身份不存在或无权限")
})
@Parameters({
@Parameter(name = "educationId", description = "教育身份ID", required = true, in = ParameterIn.PATH)
})
@PutMapping("/educations/{educationId}/default")
public R<Void> setDefaultEducation(@PathVariable Long educationId) {
memberService.setDefaultEducation(educationId);
return R.ok();
}

View File

@ -13,6 +13,9 @@ import lombok.Data;
@Schema(description = "教育身份设置请求参数(教师身份)")
public class H5EducationDto {
@Schema(description = "教育身份ID新增时不传编辑时必传", example = "1")
private Long educationId;
@Schema(description = "学校ID从/h5/base/schools获取", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "请选择学校")
private Long schoolId;

View File

@ -12,6 +12,9 @@ import lombok.Data;
@Schema(description = "教育身份信息(教师)")
public class H5EducationVo {
@Schema(description = "教育身份ID", example = "1")
private Long educationId;
@Schema(description = "省份ID", example = "420000")
private Long provinceId;
@ -53,4 +56,7 @@ public class H5EducationVo {
@Schema(description = "学科名称", example = "语文")
private String subjectName;
@Schema(description = "是否默认身份0否 1是", example = "1")
private String isDefault;
}

View File

@ -32,7 +32,4 @@ public class H5LoginVo {
@Schema(description = "昵称", example = "user_5678")
private String nickname;
@Schema(description = "身份类型1-家长2-教师null-未设置", example = "1")
private String identityType;
}

View File

@ -39,11 +39,8 @@ public class H5MemberInfoVo {
@Schema(description = "注册时间", example = "2024-01-01 12:00:00")
private Date registerTime;
@Schema(description = "身份类型1-家长2-教师", example = "2")
private String identityType;
@Schema(description = "教育身份信息(仅教师身份有值)")
private H5EducationVo education;
@Schema(description = "教育身份列表")
private List<H5EducationVo> educations;
@Schema(description = "绑定的学生列表")
private List<H5StudentVo> students;

View File

@ -33,19 +33,29 @@ public interface H5MemberService {
void updatePassword(H5PasswordUpdateDto dto);
/**
* 添加/修改教育身份
* 添加教育身份
*/
void saveEducation(H5EducationDto dto);
void addEducation(H5EducationDto dto);
/**
* 获取教育身份
* 修改教育身份
*/
H5EducationVo getEducation();
void updateEducation(Long educationId, H5EducationDto dto);
/**
* 获取教育身份列表
*/
List<H5EducationVo> getEducations();
/**
* 删除教育身份
*/
void deleteEducation();
void deleteEducation(Long educationId);
/**
* 设置默认教育身份
*/
void setDefaultEducation(Long educationId);
/**
* 绑定学生

View File

@ -409,7 +409,6 @@ public class H5AuthServiceImpl implements H5AuthService {
vo.setMemberCode(member.getMemberCode());
vo.setPhone(maskPhone(member.getPhone()));
vo.setNickname(member.getNickname());
vo.setIdentityType(member.getIdentityType());
return vo;
}
@ -671,7 +670,6 @@ public class H5AuthServiceImpl implements H5AuthService {
vo.setMemberCode(loginVo.getMemberCode());
vo.setPhone(loginVo.getPhone());
vo.setNickname(loginVo.getNickname());
vo.setIdentityType(loginVo.getIdentityType());
log.info("微信登录成功: openId={}, memberId={}", openId, member.getMemberId());
} else {
@ -777,7 +775,6 @@ public class H5AuthServiceImpl implements H5AuthService {
vo.setMemberCode(loginVo.getMemberCode());
vo.setPhone(loginVo.getPhone());
vo.setNickname(loginVo.getNickname());
vo.setIdentityType(loginVo.getIdentityType());
return vo;
}

View File

@ -9,10 +9,12 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.pangu.base.domain.PgClass;
import org.dromara.pangu.base.domain.PgEducation;
import org.dromara.pangu.base.domain.PgGrade;
import org.dromara.pangu.base.domain.PgRegion;
import org.dromara.pangu.base.domain.PgSubject;
import org.dromara.pangu.base.mapper.PgClassMapper;
import org.dromara.pangu.base.mapper.PgEducationMapper;
import org.dromara.pangu.base.mapper.PgGradeMapper;
import org.dromara.pangu.base.mapper.PgRegionMapper;
import org.dromara.pangu.base.mapper.PgSubjectMapper;
@ -59,6 +61,7 @@ public class H5MemberServiceImpl implements H5MemberService {
private final PgClassMapper classMapper;
private final PgSubjectMapper subjectMapper;
private final PgRegionMapper regionMapper;
private final PgEducationMapper educationMapper;
@Override
public H5MemberInfoVo getMemberInfo() {
@ -77,12 +80,9 @@ public class H5MemberServiceImpl implements H5MemberService {
vo.setGender(member.getGender());
vo.setBirthday(member.getBirthday());
vo.setRegisterTime(member.getRegisterTime());
vo.setIdentityType(member.getIdentityType());
// 获取教育身份信息
if ("2".equals(member.getIdentityType()) && member.getSchoolId() != null) {
vo.setEducation(buildEducationVo(member));
}
// 获取教育身份列表
vo.setEducations(getEducations());
// 获取绑定的学生列表
vo.setStudents(getStudents());
@ -139,14 +139,125 @@ public class H5MemberServiceImpl implements H5MemberService {
@Override
@Transactional(rollbackFor = Exception.class)
public void saveEducation(H5EducationDto dto) {
public void addEducation(H5EducationDto dto) {
Long memberId = getCurrentMemberId();
PgMember member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
// 校验学校年级班级学科是否存在
PgSchool school = validateAndGetSchool(dto);
// 创建教育身份记录
PgEducation education = new PgEducation();
education.setMemberId(memberId);
education.setRegionId(school.getRegionId());
education.setSchoolId(dto.getSchoolId());
education.setSchoolGradeId(dto.getSchoolGradeId());
education.setSchoolClassId(dto.getSchoolClassId());
education.setSubjectId(dto.getSubjectId());
education.setStatus("0");
// 如果是第一个教育身份设为默认
long count = educationMapper.selectCount(
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getDelFlag, "0")
);
education.setIsDefault(count == 0 ? "1" : "0");
educationMapper.insert(education);
log.info("H5添加教育身份: memberId={}, educationId={}", memberId, education.getEducationId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateEducation(Long educationId, H5EducationDto dto) {
Long memberId = getCurrentMemberId();
// 查询并校验教育身份归属
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限修改");
}
// 校验学校年级班级学科是否存在
PgSchool school = validateAndGetSchool(dto);
// 更新教育身份
education.setRegionId(school.getRegionId());
education.setSchoolId(dto.getSchoolId());
education.setSchoolGradeId(dto.getSchoolGradeId());
education.setSchoolClassId(dto.getSchoolClassId());
education.setSubjectId(dto.getSubjectId());
educationMapper.updateById(education);
log.info("H5修改教育身份: memberId={}, educationId={}", memberId, educationId);
}
@Override
public List<H5EducationVo> getEducations() {
Long memberId = getCurrentMemberId();
List<PgEducation> educations = educationMapper.selectList(
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getDelFlag, "0")
.orderByDesc(PgEducation::getIsDefault)
.orderByDesc(PgEducation::getCreateTime)
);
List<H5EducationVo> voList = new ArrayList<>();
for (PgEducation education : educations) {
voList.add(buildEducationVo(education));
}
return voList;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteEducation(Long educationId) {
Long memberId = getCurrentMemberId();
// 查询并校验教育身份归属
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限删除");
}
// 逻辑删除
educationMapper.deleteById(educationId);
log.info("H5删除教育身份: memberId={}, educationId={}", memberId, educationId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void setDefaultEducation(Long educationId) {
Long memberId = getCurrentMemberId();
// 查询并校验教育身份归属
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限操作");
}
// 取消其他默认
PgEducation updateEntity = new PgEducation();
updateEntity.setIsDefault("0");
educationMapper.update(updateEntity,
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getIsDefault, "1")
);
// 设置当前为默认
education.setIsDefault("1");
educationMapper.updateById(education);
log.info("H5设置默认教育身份: memberId={}, educationId={}", memberId, educationId);
}
/**
* 校验并获取学校信息
*/
private PgSchool validateAndGetSchool(H5EducationDto dto) {
PgSchool school = schoolMapper.selectById(dto.getSchoolId());
if (school == null) {
throw new ServiceException("学校不存在");
@ -167,68 +278,13 @@ public class H5MemberServiceImpl implements H5MemberService {
throw new ServiceException("学科不存在");
}
// 更新会员教育信息
member.setIdentityType("2"); // 教师
member.setSchoolId(dto.getSchoolId());
member.setSchoolGradeId(dto.getSchoolGradeId());
member.setSchoolClassId(dto.getSchoolClassId());
// 从学校获取区域信息并保存
if (school.getRegionId() != null) {
member.setRegionId(school.getRegionId());
}
// 学科信息需要在member表添加字段这里先用remark暂存
member.setRemark("subjectId:" + dto.getSubjectId());
memberMapper.updateById(member);
log.info("H5教育身份保存: memberId={}, schoolId={}", memberId, dto.getSchoolId());
}
@Override
public H5EducationVo getEducation() {
Long memberId = getCurrentMemberId();
PgMember member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
}
if (!"2".equals(member.getIdentityType()) || member.getSchoolId() == null) {
return null;
}
H5EducationVo vo = buildEducationVo(member);
log.info("H5获取教育身份: memberId={}, provinceId={}, cityId={}, districtId={}, schoolId={}",
memberId, vo.getProvinceId(), vo.getCityId(), vo.getDistrictId(), vo.getSchoolId());
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteEducation() {
Long memberId = getCurrentMemberId();
PgMember member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
}
// 清除教育信息
member.setIdentityType(null);
member.setSchoolId(null);
member.setSchoolGradeId(null);
member.setSchoolClassId(null);
member.setRemark(null);
memberMapper.updateById(member);
log.info("H5教育身份删除: memberId={}", memberId);
return school;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void bindStudent(H5StudentBindDto dto) {
Long memberId = getCurrentMemberId();
PgMember member = memberMapper.selectById(memberId);
if (member == null) {
throw new ServiceException("会员不存在");
}
// 校验学校年级班级是否存在
validateSchoolInfo(dto.getSchoolId(), dto.getSchoolGradeId(), dto.getSchoolClassId());
@ -247,12 +303,6 @@ public class H5MemberServiceImpl implements H5MemberService {
student.setStatus("0");
studentMapper.insert(student);
// 如果会员没有身份默认设为家长
if (StringUtils.isBlank(member.getIdentityType())) {
member.setIdentityType("1"); // 家长
memberMapper.updateById(member);
}
log.info("H5绑定学生: memberId={}, studentId={}", memberId, student.getStudentId());
}
@ -348,14 +398,17 @@ public class H5MemberServiceImpl implements H5MemberService {
/**
* 构建教育身份VO
*/
private H5EducationVo buildEducationVo(PgMember member) {
private H5EducationVo buildEducationVo(PgEducation education) {
H5EducationVo vo = new H5EducationVo();
vo.setSchoolId(member.getSchoolId());
vo.setSchoolGradeId(member.getSchoolGradeId());
vo.setSchoolClassId(member.getSchoolClassId());
vo.setEducationId(education.getEducationId());
vo.setSchoolId(education.getSchoolId());
vo.setSchoolGradeId(education.getSchoolGradeId());
vo.setSchoolClassId(education.getSchoolClassId());
vo.setSubjectId(education.getSubjectId());
vo.setIsDefault(education.getIsDefault());
// 获取学校名称和区域信息
PgSchool school = schoolMapper.selectById(member.getSchoolId());
PgSchool school = schoolMapper.selectById(education.getSchoolId());
if (school != null) {
vo.setSchoolName(school.getSchoolName());
@ -366,12 +419,9 @@ public class H5MemberServiceImpl implements H5MemberService {
vo.setDistrictId(district.getRegionId());
vo.setDistrictName(district.getRegionName());
// 根据ancestors获取省市信息格式如 "0,42,4201"
// 根据ancestors获取省市信息
if (StringUtils.isNotBlank(district.getAncestors())) {
String[] ancestorIds = district.getAncestors().split(",");
// ancestorIds[0] = "0" (根节点)
// ancestorIds[1] = 省份ID
// ancestorIds[2] = 城市ID
if (ancestorIds.length >= 2 && !"0".equals(ancestorIds[1])) {
try {
Long provinceId = Long.parseLong(ancestorIds[1].trim());
@ -402,7 +452,7 @@ public class H5MemberServiceImpl implements H5MemberService {
}
// 获取年级名称
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(member.getSchoolGradeId());
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(education.getSchoolGradeId());
if (schoolGrade != null && schoolGrade.getGradeId() != null) {
PgGrade grade = gradeMapper.selectById(schoolGrade.getGradeId());
if (grade != null) {
@ -411,7 +461,7 @@ public class H5MemberServiceImpl implements H5MemberService {
}
// 获取班级名称
PgSchoolClass schoolClass = schoolClassMapper.selectById(member.getSchoolClassId());
PgSchoolClass schoolClass = schoolClassMapper.selectById(education.getSchoolClassId());
if (schoolClass != null && schoolClass.getClassId() != null) {
PgClass cls = classMapper.selectById(schoolClass.getClassId());
if (cls != null) {
@ -419,17 +469,11 @@ public class H5MemberServiceImpl implements H5MemberService {
}
}
// 获取学科信息从remark解析
if (StringUtils.isNotBlank(member.getRemark()) && member.getRemark().startsWith("subjectId:")) {
try {
Long subjectId = Long.parseLong(member.getRemark().replace("subjectId:", ""));
vo.setSubjectId(subjectId);
PgSubject subject = subjectMapper.selectById(subjectId);
if (subject != null) {
vo.setSubjectName(subject.getSubjectName());
}
} catch (Exception e) {
// 忽略解析错误
// 获取学科名称
if (education.getSubjectId() != null) {
PgSubject subject = subjectMapper.selectById(education.getSubjectId());
if (subject != null) {
vo.setSubjectName(subject.getSubjectName());
}
}

View File

@ -9,7 +9,11 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.pangu.member.domain.PgMember;
import org.dromara.pangu.member.domain.dto.EducationDto;
import org.dromara.pangu.member.domain.dto.MemberSaveDto;
import org.dromara.pangu.member.domain.vo.EducationVo;
import org.dromara.pangu.member.service.IPgMemberService;
import org.dromara.pangu.member.service.IPgEducationService;
import org.dromara.pangu.student.domain.vo.StudentVo;
import org.dromara.pangu.student.service.IPgStudentService;
import org.springframework.validation.annotation.Validated;
@ -32,6 +36,7 @@ public class PgMemberController extends BaseController {
private final IPgMemberService memberService;
private final IPgStudentService studentService;
private final IPgEducationService educationService;
/**
* 查询会员列表
@ -52,23 +57,25 @@ public class PgMemberController extends BaseController {
}
/**
* 新增会员
* 新增会员支持同时保存教育身份
*/
@SaCheckPermission("business:member:add")
@Log(title = "会员管理", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Validated @RequestBody PgMember member) {
return toAjax(memberService.insert(member));
public R<Void> add(@Validated @RequestBody MemberSaveDto dto) {
memberService.insertWithEducations(dto);
return R.ok();
}
/**
* 修改会员
* 修改会员支持同时保存教育身份
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Validated @RequestBody PgMember member) {
return toAjax(memberService.update(member));
public R<Void> edit(@Validated @RequestBody MemberSaveDto dto) {
memberService.updateWithEducations(dto);
return R.ok();
}
/**
@ -139,4 +146,59 @@ public class PgMemberController extends BaseController {
public R<Void> unbindStudent(@PathVariable Long studentId) {
return toAjax(studentService.unbindStudent(studentId));
}
// ==================== 教育身份管理 ====================
/**
* 获取会员教育身份列表
*/
@GetMapping("/{memberId}/educations")
public R<List<EducationVo>> getEducations(@PathVariable Long memberId) {
return R.ok(educationService.getEducationsByMemberId(memberId));
}
/**
* 添加教育身份
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理-添加教育身份", businessType = BusinessType.INSERT)
@PostMapping("/{memberId}/educations")
public R<Void> addEducation(@PathVariable Long memberId, @Validated @RequestBody EducationDto dto) {
educationService.addEducation(memberId, dto);
return R.ok();
}
/**
* 修改教育身份
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理-修改教育身份", businessType = BusinessType.UPDATE)
@PutMapping("/{memberId}/educations/{educationId}")
public R<Void> updateEducation(@PathVariable Long memberId, @PathVariable Long educationId,
@Validated @RequestBody EducationDto dto) {
educationService.updateEducation(memberId, educationId, dto);
return R.ok();
}
/**
* 删除教育身份
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理-删除教育身份", businessType = BusinessType.DELETE)
@DeleteMapping("/{memberId}/educations/{educationId}")
public R<Void> deleteEducation(@PathVariable Long memberId, @PathVariable Long educationId) {
educationService.deleteEducation(memberId, educationId);
return R.ok();
}
/**
* 设置默认教育身份
*/
@SaCheckPermission("business:member:edit")
@Log(title = "会员管理-设置默认身份", businessType = BusinessType.UPDATE)
@PutMapping("/{memberId}/educations/{educationId}/default")
public R<Void> setDefaultEducation(@PathVariable Long memberId, @PathVariable Long educationId) {
educationService.setDefaultEducation(memberId, educationId);
return R.ok();
}
}

View File

@ -1,7 +1,6 @@
package org.dromara.pangu.member.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
@ -10,7 +9,6 @@ import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.util.Date;
import java.util.List;
/**
* 会员表
@ -42,29 +40,10 @@ public class PgMember extends BaseEntity {
private Date birthday;
/**
* 身份类型1家长 2教师
*/
private String identityType;
private String openId;
private String unionId;
private Long regionId;
/**
* 区域ID路径非数据库字段用于级联选择器回显
*/
@TableField(exist = false)
private List<Long> regionIds;
private Long schoolId;
private Long schoolGradeId;
private Long schoolClassId;
/**
* 注册来源1小程序 2H5 3后台 4导入
*/

View File

@ -0,0 +1,33 @@
package org.dromara.pangu.member.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 教育身份请求DTO
*
* @author 湖北新华业务中台研发团队
*/
@Data
@Schema(description = "教育身份请求")
public class EducationDto {
@Schema(description = "区域ID")
private Long regionId;
@NotNull(message = "学校不能为空")
@Schema(description = "学校ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long schoolId;
@NotNull(message = "年级不能为空")
@Schema(description = "年级IDschool_grade关联ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long schoolGradeId;
@NotNull(message = "班级不能为空")
@Schema(description = "班级IDschool_class关联ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long schoolClassId;
@Schema(description = "学科ID")
private Long subjectId;
}

View File

@ -0,0 +1,45 @@
package org.dromara.pangu.member.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 会员保存请求DTO支持同时保存教育身份
*
* @author 湖北新华业务中台研发团队
*/
@Data
@Schema(description = "会员保存请求")
public class MemberSaveDto {
@Schema(description = "会员ID新增时不传编辑时必传")
private Long memberId;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED)
private String phone;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "性别0未知 1男 2女")
private String gender;
@Schema(description = "出生日期")
private Date birthday;
@Schema(description = "状态0正常 1停用")
private String status;
@Schema(description = "教育身份列表")
private List<EducationDto> educations;
@Schema(description = "绑定的学生ID列表亲子关系")
private List<Long> studentIds;
}

View File

@ -0,0 +1,50 @@
package org.dromara.pangu.member.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 教育身份响应VO
*
* @author 湖北新华业务中台研发团队
*/
@Data
@Schema(description = "教育身份信息")
public class EducationVo {
@Schema(description = "教育身份ID")
private Long educationId;
@Schema(description = "区域ID")
private Long regionId;
@Schema(description = "区域名称(省/市/区)")
private String regionName;
@Schema(description = "学校ID")
private Long schoolId;
@Schema(description = "学校名称")
private String schoolName;
@Schema(description = "年级IDschool_grade关联ID")
private Long schoolGradeId;
@Schema(description = "年级名称")
private String gradeName;
@Schema(description = "班级IDschool_class关联ID")
private Long schoolClassId;
@Schema(description = "班级名称")
private String className;
@Schema(description = "学科ID")
private Long subjectId;
@Schema(description = "学科名称")
private String subjectName;
@Schema(description = "是否默认身份0否 1是")
private String isDefault;
}

View File

@ -0,0 +1,39 @@
package org.dromara.pangu.member.service;
import org.dromara.pangu.member.domain.dto.EducationDto;
import org.dromara.pangu.member.domain.vo.EducationVo;
import java.util.List;
/**
* 教育身份管理 Service 接口
*
* @author 湖北新华业务中台研发团队
*/
public interface IPgEducationService {
/**
* 获取会员的教育身份列表
*/
List<EducationVo> getEducationsByMemberId(Long memberId);
/**
* 添加教育身份
*/
void addEducation(Long memberId, EducationDto dto);
/**
* 修改教育身份
*/
void updateEducation(Long memberId, Long educationId, EducationDto dto);
/**
* 删除教育身份
*/
void deleteEducation(Long memberId, Long educationId);
/**
* 设置默认教育身份
*/
void setDefaultEducation(Long memberId, Long educationId);
}

View File

@ -3,6 +3,7 @@ package org.dromara.pangu.member.service;
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.dto.MemberSaveDto;
import java.util.List;
@ -33,6 +34,16 @@ public interface IPgMemberService {
*/
int insert(PgMember member);
/**
* 新增会员支持同时保存教育身份
*/
void insertWithEducations(MemberSaveDto dto);
/**
* 修改会员支持同时保存教育身份
*/
void updateWithEducations(MemberSaveDto dto);
/**
* 修改会员
*/

View File

@ -0,0 +1,267 @@
package org.dromara.pangu.member.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.pangu.base.domain.PgClass;
import org.dromara.pangu.base.domain.PgEducation;
import org.dromara.pangu.base.domain.PgGrade;
import org.dromara.pangu.base.domain.PgRegion;
import org.dromara.pangu.base.domain.PgSubject;
import org.dromara.pangu.base.mapper.PgClassMapper;
import org.dromara.pangu.base.mapper.PgEducationMapper;
import org.dromara.pangu.base.mapper.PgGradeMapper;
import org.dromara.pangu.base.mapper.PgRegionMapper;
import org.dromara.pangu.base.mapper.PgSubjectMapper;
import org.dromara.pangu.member.domain.dto.EducationDto;
import org.dromara.pangu.member.domain.vo.EducationVo;
import org.dromara.pangu.member.service.IPgEducationService;
import org.dromara.pangu.school.domain.PgSchool;
import org.dromara.pangu.school.domain.PgSchoolClass;
import org.dromara.pangu.school.domain.PgSchoolGrade;
import org.dromara.pangu.school.mapper.PgSchoolClassMapper;
import org.dromara.pangu.school.mapper.PgSchoolGradeMapper;
import org.dromara.pangu.school.mapper.PgSchoolMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* 教育身份管理 Service 实现
*
* @author 湖北新华业务中台研发团队
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class PgEducationServiceImpl implements IPgEducationService {
private final PgEducationMapper educationMapper;
private final PgSchoolMapper schoolMapper;
private final PgSchoolGradeMapper schoolGradeMapper;
private final PgSchoolClassMapper schoolClassMapper;
private final PgGradeMapper gradeMapper;
private final PgClassMapper classMapper;
private final PgSubjectMapper subjectMapper;
private final PgRegionMapper regionMapper;
@Override
public List<EducationVo> getEducationsByMemberId(Long memberId) {
List<PgEducation> educations = educationMapper.selectList(
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getDelFlag, "0")
.orderByDesc(PgEducation::getIsDefault)
.orderByDesc(PgEducation::getCreateTime)
);
List<EducationVo> voList = new ArrayList<>();
for (PgEducation education : educations) {
voList.add(buildEducationVo(education));
}
return voList;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void addEducation(Long memberId, EducationDto dto) {
// 校验学校信息
PgSchool school = validateAndGetSchool(dto);
// 创建教育身份
PgEducation education = new PgEducation();
education.setMemberId(memberId);
education.setRegionId(school.getRegionId());
education.setSchoolId(dto.getSchoolId());
education.setSchoolGradeId(dto.getSchoolGradeId());
education.setSchoolClassId(dto.getSchoolClassId());
education.setSubjectId(dto.getSubjectId());
education.setStatus("0");
// 如果是第一个教育身份设为默认
long count = educationMapper.selectCount(
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getDelFlag, "0")
);
education.setIsDefault(count == 0 ? "1" : "0");
educationMapper.insert(education);
log.info("添加教育身份: memberId={}, educationId={}", memberId, education.getEducationId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateEducation(Long memberId, Long educationId, EducationDto dto) {
// 查询并校验归属
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限修改");
}
// 校验学校信息
PgSchool school = validateAndGetSchool(dto);
// 更新
education.setRegionId(school.getRegionId());
education.setSchoolId(dto.getSchoolId());
education.setSchoolGradeId(dto.getSchoolGradeId());
education.setSchoolClassId(dto.getSchoolClassId());
education.setSubjectId(dto.getSubjectId());
educationMapper.updateById(education);
log.info("修改教育身份: memberId={}, educationId={}", memberId, educationId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteEducation(Long memberId, Long educationId) {
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限删除");
}
educationMapper.deleteById(educationId);
log.info("删除教育身份: memberId={}, educationId={}", memberId, educationId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void setDefaultEducation(Long memberId, Long educationId) {
PgEducation education = educationMapper.selectById(educationId);
if (education == null || !memberId.equals(education.getMemberId())) {
throw new ServiceException("教育身份不存在或无权限操作");
}
// 取消其他默认
PgEducation updateEntity = new PgEducation();
updateEntity.setIsDefault("0");
educationMapper.update(updateEntity,
new LambdaQueryWrapper<PgEducation>()
.eq(PgEducation::getMemberId, memberId)
.eq(PgEducation::getIsDefault, "1")
);
// 设置当前为默认
education.setIsDefault("1");
educationMapper.updateById(education);
log.info("设置默认教育身份: memberId={}, educationId={}", memberId, educationId);
}
/**
* 校验学校信息
*/
private PgSchool validateAndGetSchool(EducationDto dto) {
PgSchool school = schoolMapper.selectById(dto.getSchoolId());
if (school == null) {
throw new ServiceException("学校不存在");
}
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(dto.getSchoolGradeId());
if (schoolGrade == null || !schoolGrade.getSchoolId().equals(dto.getSchoolId())) {
throw new ServiceException("年级不存在或不属于该学校");
}
PgSchoolClass schoolClass = schoolClassMapper.selectById(dto.getSchoolClassId());
if (schoolClass == null || !schoolClass.getSchoolGradeId().equals(dto.getSchoolGradeId())) {
throw new ServiceException("班级不存在或不属于该年级");
}
if (dto.getSubjectId() != null) {
PgSubject subject = subjectMapper.selectById(dto.getSubjectId());
if (subject == null) {
throw new ServiceException("学科不存在");
}
}
return school;
}
/**
* 构建教育身份VO
*/
private EducationVo buildEducationVo(PgEducation education) {
EducationVo vo = new EducationVo();
vo.setEducationId(education.getEducationId());
vo.setSchoolId(education.getSchoolId());
vo.setSchoolGradeId(education.getSchoolGradeId());
vo.setSchoolClassId(education.getSchoolClassId());
vo.setSubjectId(education.getSubjectId());
vo.setIsDefault(education.getIsDefault());
// 获取学校和区域信息
PgSchool school = schoolMapper.selectById(education.getSchoolId());
if (school != null) {
vo.setSchoolName(school.getSchoolName());
vo.setRegionId(school.getRegionId());
// 构建区域全名
if (school.getRegionId() != null) {
vo.setRegionName(buildRegionFullName(school.getRegionId()));
}
}
// 获取年级名称
PgSchoolGrade schoolGrade = schoolGradeMapper.selectById(education.getSchoolGradeId());
if (schoolGrade != null && schoolGrade.getGradeId() != null) {
PgGrade grade = gradeMapper.selectById(schoolGrade.getGradeId());
if (grade != null) {
vo.setGradeName(grade.getGradeName());
}
}
// 获取班级名称
PgSchoolClass schoolClass = schoolClassMapper.selectById(education.getSchoolClassId());
if (schoolClass != null && schoolClass.getClassId() != null) {
PgClass cls = classMapper.selectById(schoolClass.getClassId());
if (cls != null) {
vo.setClassName(cls.getClassName());
}
}
// 获取学科名称
if (education.getSubjectId() != null) {
PgSubject subject = subjectMapper.selectById(education.getSubjectId());
if (subject != null) {
vo.setSubjectName(subject.getSubjectName());
}
}
return vo;
}
/**
* 构建区域全名//
*/
private String buildRegionFullName(Long regionId) {
PgRegion district = regionMapper.selectById(regionId);
if (district == null) {
return null;
}
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(district.getAncestors())) {
String[] ancestorIds = district.getAncestors().split(",");
for (String idStr : ancestorIds) {
if (!"0".equals(idStr.trim())) {
try {
PgRegion ancestor = regionMapper.selectById(Long.parseLong(idStr.trim()));
if (ancestor != null) {
if (sb.length() > 0) sb.append("/");
sb.append(ancestor.getRegionName());
}
} catch (NumberFormatException ignored) {
}
}
}
}
if (sb.length() > 0) sb.append("/");
sb.append(district.getRegionName());
return sb.toString();
}
}

View File

@ -5,22 +5,23 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.base.domain.PgRegion;
import org.dromara.pangu.base.mapper.PgRegionMapper;
import org.dromara.pangu.member.domain.PgMember;
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.service.IPgEducationService;
import org.dromara.pangu.member.service.IPgMemberService;
import org.dromara.pangu.student.mapper.PgStudentMapper;
import org.dromara.pangu.student.service.IPgStudentService;
import cn.hutool.crypto.digest.BCrypt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -29,13 +30,15 @@ import java.util.List;
*
* @author pangu
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class PgMemberServiceImpl implements IPgMemberService {
private final PgMemberMapper baseMapper;
private final PgStudentMapper studentMapper;
private final PgRegionMapper regionMapper;
private final IPgEducationService educationService;
private final IPgStudentService studentService;
private static final String DEFAULT_PASSWORD = "123456";
@ -53,29 +56,7 @@ public class PgMemberServiceImpl implements IPgMemberService {
@Override
public PgMember selectById(Long memberId) {
PgMember member = baseMapper.selectById(memberId);
if (member != null && member.getRegionId() != null) {
// 查询区域的完整路径用于级联选择器回显
member.setRegionIds(getRegionPath(member.getRegionId()));
}
return member;
}
/**
* 获取区域的完整路径从根到当前节点的ID列表
*/
private List<Long> getRegionPath(Long regionId) {
List<Long> path = new ArrayList<>();
Long currentId = regionId;
while (currentId != null && currentId > 0) {
path.add(0, currentId);
PgRegion region = regionMapper.selectById(currentId);
if (region == null) {
break;
}
currentId = region.getParentId();
}
return path;
return baseMapper.selectById(memberId);
}
@Override
@ -112,12 +93,63 @@ public class PgMemberServiceImpl implements IPgMemberService {
member.setStatus("0");
}
// 教师身份校验必填字段
validateTeacherInfo(member);
return baseMapper.insert(member);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void insertWithEducations(MemberSaveDto dto) {
log.info("新增会员: phone={}, educations={}, studentIds={}",
dto.getPhone(),
dto.getEducations() != null ? dto.getEducations().size() : 0,
dto.getStudentIds() != null ? dto.getStudentIds() : "null");
// 创建会员
PgMember member = new PgMember();
member.setPhone(dto.getPhone());
member.setNickname(dto.getNickname());
member.setGender(dto.getGender());
member.setBirthday(dto.getBirthday());
member.setStatus(dto.getStatus());
insert(member);
Long memberId = member.getMemberId();
// 保存教育身份
if (dto.getEducations() != null && !dto.getEducations().isEmpty()) {
log.info("保存教育身份: memberId={}, count={}", memberId, dto.getEducations().size());
for (EducationDto eduDto : dto.getEducations()) {
educationService.addEducation(memberId, eduDto);
}
}
// 绑定学生亲子关系
if (dto.getStudentIds() != null && !dto.getStudentIds().isEmpty()) {
log.info("绑定学生: memberId={}, studentIds={}", memberId, dto.getStudentIds());
studentService.bindStudentsToMember(memberId, dto.getStudentIds());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateWithEducations(MemberSaveDto dto) {
if (dto.getMemberId() == null) {
throw new ServiceException("会员ID不能为空");
}
// 更新会员基本信息
PgMember member = new PgMember();
member.setMemberId(dto.getMemberId());
member.setPhone(dto.getPhone());
member.setNickname(dto.getNickname());
member.setGender(dto.getGender());
member.setBirthday(dto.getBirthday());
member.setStatus(dto.getStatus());
update(member);
// 注意编辑时教育身份通过单独的接口管理这里不处理
}
@Override
@Transactional(rollbackFor = Exception.class)
public int update(PgMember member) {
@ -126,17 +158,6 @@ public class PgMemberServiceImpl implements IPgMemberService {
throw new ServiceException("手机号已存在");
}
// 教师身份校验必填字段
validateTeacherInfo(member);
// 家长身份清空学校信息
if ("1".equals(member.getIdentityType())) {
member.setRegionId(null);
member.setSchoolId(null);
member.setSchoolGradeId(null);
member.setSchoolClassId(null);
}
// 不更新密码密码通过重置接口更新
member.setPassword(null);
@ -223,31 +244,10 @@ public class PgMemberServiceImpl implements IPgMemberService {
);
}
/**
* 校验教师身份必填字段
*/
private void validateTeacherInfo(PgMember member) {
if ("2".equals(member.getIdentityType())) {
if (member.getRegionId() == null) {
throw new ServiceException("教师身份必须选择所属区域");
}
if (member.getSchoolId() == null) {
throw new ServiceException("教师身份必须选择所属学校");
}
if (member.getSchoolGradeId() == null) {
throw new ServiceException("教师身份必须选择所属年级");
}
if (member.getSchoolClassId() == null) {
throw new ServiceException("教师身份必须选择所属班级");
}
}
}
private LambdaQueryWrapper<PgMember> buildQueryWrapper(PgMember member) {
LambdaQueryWrapper<PgMember> lqw = new LambdaQueryWrapper<>();
lqw.like(StrUtil.isNotBlank(member.getNickname()), PgMember::getNickname, member.getNickname());
lqw.like(StrUtil.isNotBlank(member.getPhone()), PgMember::getPhone, member.getPhone());
lqw.eq(StrUtil.isNotBlank(member.getIdentityType()), PgMember::getIdentityType, member.getIdentityType());
lqw.eq(StrUtil.isNotBlank(member.getStatus()), PgMember::getStatus, member.getStatus());
lqw.orderByDesc(PgMember::getCreateTime);
return lqw;

View File

@ -89,7 +89,7 @@ public class PgStudentController extends BaseController {
* @param schoolId 学校ID教师身份时传入限制只能选本校学生
*/
@GetMapping("/available")
public TableDataInfo<PgStudent> availableStudents(
public TableDataInfo<StudentVo> availableStudents(
@RequestParam(required = false) String studentName,
@RequestParam(required = false) String studentNo,
@RequestParam(required = false) Long memberId,

View File

@ -30,7 +30,7 @@ public interface IPgStudentService {
* @param schoolId 学校ID教师身份时必传限制只能选本校学生
* @param pageQuery 分页参数
*/
TableDataInfo<PgStudent> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery);
TableDataInfo<StudentVo> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery);
/**
* 查询会员已绑定的学生列表包含学校年级班级名称

View File

@ -14,6 +14,8 @@ 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.mapper.PgMemberMapper;
import org.dromara.pangu.school.domain.PgSchool;
@ -52,6 +54,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
private final PgGradeMapper gradeMapper;
private final PgClassMapper classMapper;
private final PgMemberMapper memberMapper;
private final PgEducationMapper educationMapper;
@Override
public TableDataInfo<StudentVo> selectPageList(PgStudent student, PageQuery pageQuery) {
@ -211,7 +214,7 @@ public class PgStudentServiceImpl implements IPgStudentService {
}
@Override
public TableDataInfo<PgStudent> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery) {
public TableDataInfo<StudentVo> selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery) {
LambdaQueryWrapper<PgStudent> lqw = new LambdaQueryWrapper<>();
lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName);
lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo);
@ -224,7 +227,9 @@ public class PgStudentServiceImpl implements IPgStudentService {
lqw.orderByDesc(PgStudent::getCreateTime);
Page<PgStudent> page = baseMapper.selectPage(pageQuery.build(), lqw);
return TableDataInfo.build(page);
// 转换为 VO填充学校年级班级名称
List<StudentVo> voList = convertToVoList(page.getRecords());
return new TableDataInfo<>(voList, page.getTotal());
}
@Override
@ -344,11 +349,18 @@ public class PgStudentServiceImpl implements IPgStudentService {
PgMember member = findOrCreateMember(dto.getMemberPhone().trim());
Long memberId = member.getMemberId();
// 6. 教师身份校验教师的区域/学校/年级/班级必须与学生一致
if ("2".equals(member.getIdentityType())) {
String teacherError = validateTeacherStudent(member, school, schoolGrade, schoolClass);
if (teacherError != null) {
failList.add(createFailItem(rowNum, teacherError));
// 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;
}
}
@ -445,12 +457,11 @@ public class PgStudentServiceImpl implements IPgStudentService {
return member;
}
// 不存在则创建新会员身份为家长初始密码123456
// 不存在则创建新会员初始密码123456
PgMember newMember = new PgMember();
newMember.setMemberCode(generateMemberCode()); // 生成会员编码
newMember.setPhone(phone);
newMember.setNickname("家长" + phone.substring(7)); // 默认昵称
newMember.setIdentityType("1"); // 家长
newMember.setPassword(BCrypt.hashpw("123456")); // 初始密码
newMember.setStatus("0"); // 正常
newMember.setRegisterSource("4"); // 批量导入
@ -460,44 +471,6 @@ public class PgStudentServiceImpl implements IPgStudentService {
return newMember;
}
/**
* 校验教师与学生的归属关系
* 教师的区域/学校/年级/班级必须与学生一致
*/
private String validateTeacherStudent(PgMember teacher, PgSchool studentSchool,
PgSchoolGrade studentGrade, PgSchoolClass studentClass) {
String teacherInfo = "教师\"" + (teacher.getNickname() != null ? teacher.getNickname() : "未知")
+ "\"(" + teacher.getPhone() + ")";
// 检查教师是否设置了学校信息
if (teacher.getSchoolId() == null || teacher.getSchoolGradeId() == null || teacher.getSchoolClassId() == null) {
return teacherInfo + "未设置学校信息,无法绑定学生";
}
// 校验区域通过学校的区域ID间接校验
if (teacher.getRegionId() != null && studentSchool.getRegionId() != null) {
if (!teacher.getRegionId().equals(studentSchool.getRegionId())) {
return teacherInfo + "所属区域与学生不一致";
}
}
// 校验学校
if (!teacher.getSchoolId().equals(studentSchool.getSchoolId())) {
return teacherInfo + "所属学校与学生不一致";
}
// 校验年级
if (!teacher.getSchoolGradeId().equals(studentGrade.getId())) {
return teacherInfo + "所属年级与学生不一致";
}
// 校验班级
if (!teacher.getSchoolClassId().equals(studentClass.getId())) {
return teacherInfo + "所属班级与学生不一致";
}
return null; // 校验通过
}
/**
* 生成会员编码M + 年月日时分秒毫秒 + 4位随机数

View File

@ -0,0 +1,347 @@
<!--
教育身份编辑弹窗
@author 湖北新华业务中台研发团队
-->
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑教育身份' : '添加教育身份'"
width="550px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item label="区域" prop="regionId">
<el-cascader
v-model="regionIds"
:options="regionTree"
:props="{ value: 'regionId', label: 'regionName', checkStrictly: true }"
placeholder="请选择区域"
clearable
style="width: 100%"
@change="handleRegionChange"
/>
</el-form-item>
<el-form-item label="学校" prop="schoolId">
<el-select v-model="form.schoolId" placeholder="请选择学校" clearable style="width: 100%" @change="handleSchoolChange">
<el-option v-for="item in schoolList" :key="item.schoolId" :label="item.schoolName" :value="item.schoolId" />
</el-select>
</el-form-item>
<el-form-item label="年级" prop="schoolGradeId">
<el-select v-model="form.schoolGradeId" placeholder="请选择年级" clearable style="width: 100%" @change="handleGradeChange">
<el-option v-for="item in gradeList" :key="item.id" :label="item.gradeName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="班级" prop="schoolClassId">
<el-select v-model="form.schoolClassId" placeholder="请选择班级" clearable style="width: 100%">
<el-option v-for="item in classList" :key="item.id" :label="item.className" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="学科" prop="subjectId">
<el-select v-model="form.subjectId" placeholder="请选择学科(可选)" clearable style="width: 100%">
<el-option v-for="item in subjectList" :key="item.subjectId" :label="item.subjectName" :value="item.subjectId" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
const emit = defineEmits(['success', 'add', 'update'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref(null)
const submitLoading = ref(false)
// IDIDmemberId null
const memberId = ref(null)
const educationId = ref(null)
//
const localIndex = ref(null)
// ID
const regionIds = ref([])
//
const form = reactive({
regionId: null,
schoolId: null,
schoolGradeId: null,
schoolClassId: null,
subjectId: null
})
//
const rules = {
schoolId: [{ required: true, message: '请选择学校', trigger: 'change' }],
schoolGradeId: [{ required: true, message: '请选择年级', trigger: 'change' }],
schoolClassId: [{ required: true, message: '请选择班级', trigger: 'change' }]
}
//
const regionTree = ref([])
const schoolList = ref([])
const gradeList = ref([])
const classList = ref([])
const subjectList = ref([])
/**
* 打开弹窗
* @param {Long} mId 会员IDnull 表示本地模式
* @param {Object} row 编辑时传入教育身份数据
* @param {Number} index 本地模式下的数组索引
*/
const open = async (mId, row, index) => {
resetForm()
memberId.value = mId
isEdit.value = !!row
localIndex.value = index ?? null
visible.value = true
//
await Promise.all([loadRegionTree(), loadSubjectList()])
//
if (row) {
educationId.value = row.educationId
form.schoolId = row.schoolId
form.schoolGradeId = row.schoolGradeId
form.schoolClassId = row.schoolClassId
form.subjectId = row.subjectId
//
if (row.regionId) {
// regionIds
if (row.regionIds) {
regionIds.value = row.regionIds
} else {
regionIds.value = await getRegionPath(row.regionId)
}
form.regionId = row.regionId
await loadSchoolList(row.regionId)
}
if (row.schoolId) {
await loadGradeList(row.schoolId)
}
if (row.schoolGradeId) {
await loadClassList(row.schoolGradeId)
}
}
}
/**
* 重置表单
*/
const resetForm = () => {
educationId.value = null
regionIds.value = []
form.regionId = null
form.schoolId = null
form.schoolGradeId = null
form.schoolClassId = null
form.subjectId = null
schoolList.value = []
gradeList.value = []
classList.value = []
}
/**
* 加载区域树
*/
const loadRegionTree = async () => {
try {
const res = await request.get('/business/region/tree')
regionTree.value = res.data || []
} catch (e) {
regionTree.value = []
}
}
/**
* 加载学科列表
*/
const loadSubjectList = async () => {
try {
const res = await request.get('/business/subject/listAll')
subjectList.value = res.data || []
} catch (e) {
subjectList.value = []
}
}
/**
* 加载学校列表
*/
const loadSchoolList = async (regionId) => {
try {
const res = await request.get('/business/school/listAll', { params: { regionId } })
schoolList.value = res.data || []
} catch (e) {
schoolList.value = []
}
}
/**
* 加载年级列表
*/
const loadGradeList = async (schoolId) => {
try {
const res = await request.get(`/business/school/${schoolId}/grades`)
gradeList.value = res.data || []
} catch (e) {
gradeList.value = []
}
}
/**
* 加载班级列表
*/
const loadClassList = async (schoolGradeId) => {
try {
const res = await request.get(`/business/school/grade/${schoolGradeId}/classes`)
classList.value = res.data || []
} catch (e) {
classList.value = []
}
}
/**
* 获取区域路径用于回显
*/
const getRegionPath = async (regionId) => {
try {
const res = await request.get(`/business/region/${regionId}/path`)
return res.data || []
} catch (e) {
return []
}
}
/**
* 区域变更
*/
const handleRegionChange = (val) => {
form.regionId = val && val.length ? val[val.length - 1] : null
form.schoolId = null
form.schoolGradeId = null
form.schoolClassId = null
schoolList.value = []
gradeList.value = []
classList.value = []
if (form.regionId) {
loadSchoolList(form.regionId)
}
}
/**
* 学校变更
*/
const handleSchoolChange = () => {
form.schoolGradeId = null
form.schoolClassId = null
gradeList.value = []
classList.value = []
if (form.schoolId) {
loadGradeList(form.schoolId)
}
}
/**
* 年级变更
*/
const handleGradeChange = () => {
form.schoolClassId = null
classList.value = []
if (form.schoolGradeId) {
loadClassList(form.schoolGradeId)
}
}
/**
* 提交表单
*/
const handleSubmit = async () => {
try {
await formRef.value?.validate()
} catch (e) {
return
}
//
const data = {
regionId: form.regionId,
regionIds: [...regionIds.value],
schoolId: form.schoolId,
schoolGradeId: form.schoolGradeId,
schoolClassId: form.schoolClassId,
subjectId: form.subjectId,
//
schoolName: schoolList.value.find(s => s.schoolId === form.schoolId)?.schoolName || '',
gradeName: gradeList.value.find(g => g.id === form.schoolGradeId)?.gradeName || '',
className: classList.value.find(c => c.id === form.schoolClassId)?.className || '',
subjectName: subjectList.value.find(s => s.subjectId === form.subjectId)?.subjectName || ''
}
//
if (!memberId.value) {
visible.value = false
if (isEdit.value) {
emit('update', data, localIndex.value)
} else {
emit('add', data)
}
return
}
// API
submitLoading.value = true
try {
const apiData = {
regionId: form.regionId,
schoolId: form.schoolId,
schoolGradeId: form.schoolGradeId,
schoolClassId: form.schoolClassId,
subjectId: form.subjectId
}
if (isEdit.value) {
const res = await request.put(`/business/member/${memberId.value}/educations/${educationId.value}`, apiData)
if (res.code === 200) {
ElMessage.success('修改成功')
visible.value = false
emit('success')
}
} else {
const res = await request.post(`/business/member/${memberId.value}/educations`, apiData)
if (res.code === 200) {
ElMessage.success('添加成功')
visible.value = false
emit('success')
}
}
} finally {
submitLoading.value = false
}
}
defineExpose({ open })
</script>

View File

@ -53,14 +53,6 @@
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份类型" prop="identityType">
<el-radio-group v-model="form.identityType" @change="handleIdentityChange">
<el-radio value="1">家长</el-radio>
<el-radio value="2">教师</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-switch
@ -74,65 +66,45 @@
</el-col>
</el-row>
<!-- 教师身份时显示学校信息 -->
<template v-if="form.identityType === '2'">
<el-divider content-position="left">学校信息教师必填</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="区域" prop="regionId" :rules="teacherRules.regionId">
<el-cascader
v-model="form.regionIds"
:options="regionTree"
:props="{ value: 'regionId', label: 'regionName', checkStrictly: true }"
placeholder="请选择区域"
clearable
style="width: 100%"
@change="handleRegionChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学校" prop="schoolId" :rules="teacherRules.schoolId">
<el-select v-model="form.schoolId" placeholder="请选择学校" clearable style="width: 100%" @change="handleSchoolChange">
<el-option v-for="item in schoolList" :key="item.schoolId" :label="item.schoolName" :value="item.schoolId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年级" prop="schoolGradeId" :rules="teacherRules.schoolGradeId">
<el-select v-model="form.schoolGradeId" placeholder="请选择年级" clearable style="width: 100%" @change="handleGradeChange">
<el-option v-for="item in gradeList" :key="item.id" :label="item.gradeName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="schoolClassId" :rules="teacherRules.schoolClassId">
<el-select v-model="form.schoolClassId" placeholder="请选择班级" clearable style="width: 100%">
<el-option v-for="item in classList" :key="item.id" :label="item.className" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 绑定学生 -->
<el-divider content-position="left">绑定学生</el-divider>
<el-alert
v-if="form.identityType === '2'"
title="教师只能绑定本校学生"
type="info"
:closable="false"
show-icon
style="margin-bottom: 12px"
/>
<!-- 教育身份 -->
<el-divider content-position="left">教育身份</el-divider>
<el-row style="margin-bottom: 12px;">
<el-button type="primary" size="small" :icon="Plus" @click="handleAddStudent">添加学生</el-button>
<el-button type="primary" size="small" :icon="Plus" @click="handleAddEducation">添加教育身份</el-button>
</el-row>
<el-table :data="form.students" border size="small" max-height="200">
<el-table :data="educations" border size="small" max-height="180">
<template #empty>
<el-empty description="暂无绑定学生" :image-size="60" />
<el-empty description="暂无教育身份" :image-size="60" />
</template>
<el-table-column prop="schoolName" label="学校" min-width="140" show-overflow-tooltip />
<el-table-column prop="gradeName" label="年级" width="70" />
<el-table-column prop="className" label="班级" width="60" />
<el-table-column prop="subjectName" label="学科" width="60" />
<el-table-column label="默认" width="60" align="center">
<template #default="{ row }">
<el-tag v-if="row.isDefault === '1'" type="success" size="small">默认</el-tag>
<el-button v-else link type="primary" size="small" @click="handleSetDefault(row)">设为</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleEditEducation(row)">编辑</el-button>
<el-popconfirm title="确定删除该教育身份?" @confirm="handleDeleteEducation(row)">
<template #reference>
<el-button link type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 亲子关系 -->
<el-divider content-position="left">亲子关系</el-divider>
<el-row style="margin-bottom: 12px;">
<el-button type="primary" size="small" :icon="Plus" @click="handleAddStudent">添加亲子关系</el-button>
</el-row>
<el-table :data="form.students" border size="small" max-height="180">
<template #empty>
<el-empty description="暂无亲子关系" :image-size="60" />
</template>
<el-table-column prop="studentName" label="姓名" min-width="80" />
<el-table-column prop="studentNo" label="学号" width="120" />
@ -141,9 +113,9 @@
<el-table-column prop="className" label="班级" width="60" />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-popconfirm title="确定解绑该学生?" @confirm="handleRemoveStudent(row)">
<el-popconfirm :title="isEdit ? '确定解绑该学生?' : '确定移除该学生?'" @confirm="handleRemoveStudent(row)">
<template #reference>
<el-button link type="danger" size="small">解绑</el-button>
<el-button link type="danger" size="small">{{ isEdit ? '解绑' : '移除' }}</el-button>
</template>
</el-popconfirm>
</template>
@ -156,7 +128,10 @@
</template>
<!-- 学生选择器 -->
<StudentSelectDialog ref="studentSelectRef" @success="loadBoundStudents" />
<StudentSelectDialog ref="studentSelectRef" @success="loadBoundStudents" @add="handleLocalAddStudents" />
<!-- 教育身份编辑弹窗 -->
<EducationDialog ref="educationDialogRef" @success="loadEducations" @add="handleLocalAddEducation" @update="handleLocalUpdateEducation" />
</el-dialog>
</template>
@ -166,6 +141,7 @@ import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import StudentSelectDialog from './StudentSelectDialog.vue'
import EducationDialog from './EducationDialog.vue'
const emit = defineEmits(['success'])
@ -174,6 +150,10 @@ const isEdit = ref(false)
const formRef = ref(null)
const submitLoading = ref(false)
const studentSelectRef = ref(null)
const educationDialogRef = ref(null)
//
const educations = ref([])
//
const form = reactive({
@ -182,13 +162,7 @@ const form = reactive({
nickname: '',
gender: '0',
birthday: '',
identityType: '1',
status: '0',
regionIds: [],
regionId: null,
schoolId: null,
schoolGradeId: null,
schoolClassId: null,
students: []
})
@ -197,26 +171,9 @@ const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
identityType: [
{ required: true, message: '请选择身份类型', trigger: 'change' }
]
}
//
const teacherRules = {
regionId: [{ required: true, message: '请选择所属区域', trigger: 'change' }],
schoolId: [{ required: true, message: '请选择所属学校', trigger: 'change' }],
schoolGradeId: [{ required: true, message: '请选择所属年级', trigger: 'change' }],
schoolClassId: [{ required: true, message: '请选择所属班级', trigger: 'change' }]
}
//
const regionTree = ref([])
const schoolList = ref([])
const gradeList = ref([])
const classList = ref([])
/**
* 打开弹窗
*/
@ -225,28 +182,14 @@ const open = async (row) => {
isEdit.value = !!row
visible.value = true
//
await loadRegionTree()
//
if (row && row.memberId) {
try {
const res = await request.get(`/business/member/${row.memberId}`)
if (res.code === 200 && res.data) {
Object.assign(form, res.data)
// regionIds
//
if (form.regionId) {
await loadSchoolList(form.regionId)
}
if (form.schoolId) {
await loadGradeList(form.schoolId)
}
if (form.schoolGradeId) {
await loadClassList(form.schoolGradeId)
}
//
await loadBoundStudents()
//
await Promise.all([loadEducations(), loadBoundStudents()])
}
} catch (e) {
console.error('加载会员数据失败', e)
@ -254,6 +197,22 @@ const open = async (row) => {
}
}
/**
* 加载教育身份列表
*/
const loadEducations = async () => {
if (!form.memberId) {
educations.value = []
return
}
try {
const res = await request.get(`/business/member/${form.memberId}/educations`)
educations.value = res.data || []
} catch (e) {
educations.value = []
}
}
/**
* 加载已绑定的学生列表
*/
@ -279,143 +238,147 @@ const resetForm = () => {
form.nickname = ''
form.gender = '0'
form.birthday = ''
form.identityType = '1'
form.status = '0'
form.regionIds = []
form.regionId = null
form.schoolId = null
form.schoolGradeId = null
form.schoolClassId = null
form.students = []
schoolList.value = []
gradeList.value = []
classList.value = []
educations.value = []
}
/**
* 加载区域树
* 添加教育身份
*/
const loadRegionTree = async () => {
try {
const res = await request.get('/business/region/tree')
regionTree.value = res.data || []
} catch (e) {
regionTree.value = []
const handleAddEducation = () => {
// memberId null
// memberId
educationDialogRef.value?.open(isEdit.value ? form.memberId : null)
}
/**
* 编辑教育身份
*/
const handleEditEducation = (row) => {
const index = educations.value.indexOf(row)
educationDialogRef.value?.open(isEdit.value ? form.memberId : null, row, index)
}
/**
* 本地添加教育身份新增会员模式
*/
const handleLocalAddEducation = (data) => {
//
if (educations.value.length === 0) {
data.isDefault = '1'
} else {
data.isDefault = '0'
}
educations.value.push(data)
}
/**
* 本地更新教育身份新增会员模式
*/
const handleLocalUpdateEducation = (data, index) => {
if (index !== null && index >= 0) {
//
data.isDefault = educations.value[index].isDefault
educations.value[index] = data
}
}
/**
* 加载学校列表
* 删除教育身份
*/
const loadSchoolList = async (regionId) => {
try {
const res = await request.get('/business/school/listAll', { params: { regionId } })
schoolList.value = res.data || []
} catch (e) {
schoolList.value = []
}
}
/**
* 加载年级列表
*/
const loadGradeList = async (schoolId) => {
try {
const res = await request.get(`/business/school/${schoolId}/grades`)
gradeList.value = res.data || []
} catch (e) {
gradeList.value = []
}
}
/**
* 加载班级列表
*/
const loadClassList = async (schoolGradeId) => {
try {
const res = await request.get(`/business/school/grade/${schoolGradeId}/classes`)
classList.value = res.data || []
} catch (e) {
classList.value = []
}
}
/**
* 身份类型变更
*/
const handleIdentityChange = () => {
//
if (form.identityType === '1') {
form.regionIds = []
form.regionId = null
form.schoolId = null
form.schoolGradeId = null
form.schoolClassId = null
schoolList.value = []
gradeList.value = []
classList.value = []
}
}
/**
* 区域变更
*/
const handleRegionChange = (val) => {
form.regionId = val && val.length ? val[val.length - 1] : null
form.schoolId = null
form.schoolGradeId = null
form.schoolClassId = null
schoolList.value = []
gradeList.value = []
classList.value = []
if (form.regionId) {
loadSchoolList(form.regionId)
}
}
/**
* 学校变更
*/
const handleSchoolChange = () => {
form.schoolGradeId = null
form.schoolClassId = null
gradeList.value = []
classList.value = []
if (form.schoolId) {
loadGradeList(form.schoolId)
}
}
/**
* 年级变更
*/
const handleGradeChange = () => {
form.schoolClassId = null
classList.value = []
if (form.schoolGradeId) {
loadClassList(form.schoolGradeId)
}
}
/**
* 添加学生
*/
const handleAddStudent = () => {
if (!isEdit.value || !form.memberId) {
ElMessage.warning('请先保存会员信息后再绑定学生')
const handleDeleteEducation = async (row) => {
//
if (!isEdit.value) {
const index = educations.value.indexOf(row)
if (index > -1) {
educations.value.splice(index, 1)
//
if (row.isDefault === '1' && educations.value.length > 0) {
educations.value[0].isDefault = '1'
}
}
return
}
studentSelectRef.value?.open({
memberId: form.memberId,
identityType: form.identityType,
schoolId: form.schoolId
})
//
try {
const res = await request.delete(`/business/member/${form.memberId}/educations/${row.educationId}`)
if (res.code === 200) {
ElMessage.success('删除成功')
await loadEducations()
}
} catch (e) {
console.error('删除失败', e)
}
}
/**
* 解绑学生
* 设为默认教育身份
*/
const handleSetDefault = async (row) => {
//
if (!isEdit.value) {
educations.value.forEach(e => e.isDefault = '0')
row.isDefault = '1'
return
}
//
try {
const res = await request.put(`/business/member/${form.memberId}/educations/${row.educationId}/default`)
if (res.code === 200) {
ElMessage.success('设置成功')
await loadEducations()
}
} catch (e) {
console.error('设置失败', e)
}
}
/**
* 添加亲子关系
*/
const handleAddStudent = () => {
// excludeIds
// memberId
if (isEdit.value) {
studentSelectRef.value?.open({
memberId: form.memberId
})
} else {
studentSelectRef.value?.open({
excludeIds: form.students.map(s => s.studentId)
})
}
}
/**
* 本地添加学生新增会员模式
*/
const handleLocalAddStudents = (students) => {
//
for (const student of students) {
if (!form.students.some(s => s.studentId === student.studentId)) {
form.students.push(student)
}
}
}
/**
* 解绑/移除学生
*/
const handleRemoveStudent = async (row) => {
//
if (!isEdit.value) {
const index = form.students.findIndex(s => s.studentId === row.studentId)
if (index > -1) {
form.students.splice(index, 1)
}
return
}
//
try {
const res = await request.post(`/business/member/unbindStudent/${row.studentId}`)
if (res.code === 200) {
@ -439,10 +402,14 @@ const handleSubmit = async () => {
submitLoading.value = true
try {
const data = { ...form }
//
delete data.regionIds
delete data.students
const data = {
memberId: form.memberId,
phone: form.phone,
nickname: form.nickname,
gender: form.gender,
birthday: form.birthday,
status: form.status
}
if (isEdit.value) {
const res = await request.put('/business/member', data)
@ -452,6 +419,15 @@ const handleSubmit = async () => {
emit('success')
}
} else {
//
data.educations = educations.value.map(e => ({
regionId: e.regionId,
schoolId: e.schoolId,
schoolGradeId: e.schoolGradeId,
schoolClassId: e.schoolClassId,
subjectId: e.subjectId
}))
data.studentIds = form.students.map(s => s.studentId)
const res = await request.post('/business/member', data)
if (res.code === 200) {
ElMessage.success('新增成功')

View File

@ -24,16 +24,6 @@
</el-form-item>
</el-form>
<!-- 教师身份提示 -->
<el-alert
v-if="isTeacher"
title="教师只能绑定本校学生"
type="info"
:closable="false"
show-icon
style="margin-bottom: 12px"
/>
<!-- 学生列表 -->
<el-table
ref="tableRef"
@ -74,7 +64,7 @@
<div class="dialog-footer">
<span style="margin-right: 16px; color: #909399">已选择 {{ selectedStudents.length }} 名学生</span>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleConfirm">确定绑定</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleConfirm">确定</el-button>
</div>
</template>
</el-dialog>
@ -86,7 +76,7 @@ import { Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
const emit = defineEmits(['success'])
const emit = defineEmits(['success', 'add'])
const visible = ref(false)
const loading = ref(false)
@ -96,10 +86,12 @@ const total = ref(0)
const selectedStudents = ref([])
const tableRef = ref(null)
//
// memberId null
const memberId = ref(null)
const isTeacher = ref(false)
const schoolId = ref(null)
// ID
const excludeStudentIds = ref([])
//
const queryParams = reactive({
@ -111,12 +103,13 @@ const queryParams = reactive({
/**
* 打开弹窗
* @param options { memberId: Long, identityType: String, schoolId: Long }
* @param options { memberId: Long, excludeIds: Long[] } - memberId null 表示本地模式
*/
const open = (options = {}) => {
memberId.value = options.memberId
isTeacher.value = options.identityType === '2'
schoolId.value = options.schoolId
memberId.value = options.memberId || null
isTeacher.value = false
schoolId.value = null
excludeStudentIds.value = options.excludeIds || []
resetQuery()
visible.value = true
@ -134,11 +127,15 @@ const getList = async () => {
pageSize: queryParams.pageSize,
studentName: queryParams.studentName || undefined,
studentNo: queryParams.studentNo || undefined,
memberId: memberId.value,
schoolId: isTeacher.value ? schoolId.value : undefined
memberId: memberId.value || undefined
}
const res = await request.get('/business/student/available', { params })
studentList.value = res.rows || []
let list = res.rows || []
//
if (!memberId.value && excludeStudentIds.value.length > 0) {
list = list.filter(s => !excludeStudentIds.value.includes(s.studentId))
}
studentList.value = list
total.value = res.total || 0
} catch (e) {
console.error('获取学生列表失败', e)
@ -195,11 +192,18 @@ const handleSelectionChange = (selection) => {
*/
const handleConfirm = async () => {
if (selectedStudents.value.length === 0) {
ElMessage.warning('请选择要绑定的学生')
ElMessage.warning('请选择要添加的学生')
return
}
//
//
if (!memberId.value) {
visible.value = false
emit('add', selectedStudents.value)
return
}
// API
const studentIds = selectedStudents.value
.filter(s => s.memberId !== memberId.value)
.map(s => s.studentId)

View File

@ -9,12 +9,6 @@
<el-form-item label="昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="身份类型">
<el-select v-model="queryParams.identityType" placeholder="全部" clearable style="width: 120px">
<el-option label="家长" value="1" />
<el-option label="教师" value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
@ -72,13 +66,6 @@
{{ formatBirthday(row.birthday) }}
</template>
</el-table-column>
<el-table-column prop="identityType" label="身份类型" width="85" align="center">
<template #default="{ row }">
<el-tag :type="row.identityType === '1' ? 'success' : 'warning'">
{{ row.identityType === '1' ? '家长' : '教师' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="registerTime" label="注册时间" width="165" />
<el-table-column prop="registerSource" label="注册来源" width="85" align="center">
<template #default="{ row }">
@ -157,7 +144,6 @@ const queryParams = ref({
pageSize: 10,
phone: '',
nickname: '',
identityType: '',
status: ''
})
@ -235,7 +221,6 @@ const resetQuery = () => {
pageSize: 10,
phone: '',
nickname: '',
identityType: '',
status: ''
}
dateRange.value = []