diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java index 6ccd881..8a09e79 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/controller/PgMemberController.java @@ -10,10 +10,13 @@ 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.service.IPgMemberService; +import org.dromara.pangu.student.domain.PgStudent; +import org.dromara.pangu.student.service.IPgStudentService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -28,6 +31,7 @@ import java.util.Map; public class PgMemberController extends BaseController { private final IPgMemberService memberService; + private final IPgStudentService studentService; /** * 查询会员列表 @@ -107,4 +111,32 @@ public class PgMemberController extends BaseController { public R checkPhoneUnique(@RequestParam String phone, @RequestParam(required = false) Long memberId) { return R.ok(memberService.checkPhoneUnique(phone, memberId)); } + + /** + * 获取会员已绑定的学生列表 + */ + @GetMapping("/{memberId}/students") + public R> getMemberStudents(@PathVariable Long memberId) { + return R.ok(studentService.selectByMemberId(memberId)); + } + + /** + * 批量绑定学生到会员 + */ + @SaCheckPermission("business:member:edit") + @Log(title = "会员管理-绑定学生", businessType = BusinessType.UPDATE) + @PostMapping("/{memberId}/bindStudents") + public R bindStudents(@PathVariable Long memberId, @RequestBody List studentIds) { + return toAjax(studentService.bindStudentsToMember(memberId, studentIds)); + } + + /** + * 解绑学生 + */ + @SaCheckPermission("business:member:edit") + @Log(title = "会员管理-解绑学生", businessType = BusinessType.UPDATE) + @PostMapping("/unbindStudent/{studentId}") + public R unbindStudent(@PathVariable Long studentId) { + return toAjax(studentService.unbindStudent(studentId)); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMember.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMember.java index 177b04c..d24fa45 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMember.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/domain/PgMember.java @@ -1,6 +1,7 @@ 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; @@ -9,6 +10,7 @@ import lombok.EqualsAndHashCode; import org.dromara.common.mybatis.core.domain.BaseEntity; import java.util.Date; +import java.util.List; /** * 会员表 @@ -51,6 +53,12 @@ public class PgMember extends BaseEntity { private Long regionId; + /** + * 区域ID路径(非数据库字段,用于级联选择器回显) + */ + @TableField(exist = false) + private List regionIds; + private Long schoolId; private Long schoolGradeId; diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java index 1fd2003..23545c8 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/impl/PgMemberServiceImpl.java @@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor; 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.mapper.PgMemberMapper; import org.dromara.pangu.member.service.IPgMemberService; @@ -16,7 +18,9 @@ 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; @@ -31,6 +35,7 @@ public class PgMemberServiceImpl implements IPgMemberService { private final PgMemberMapper baseMapper; private final PgStudentMapper studentMapper; + private final PgRegionMapper regionMapper; private static final String DEFAULT_PASSWORD = "123456"; @@ -48,7 +53,29 @@ public class PgMemberServiceImpl implements IPgMemberService { @Override public PgMember selectById(Long memberId) { - return baseMapper.selectById(memberId); + PgMember member = baseMapper.selectById(memberId); + if (member != null && member.getRegionId() != null) { + // 查询区域的完整路径用于级联选择器回显 + member.setRegionIds(getRegionPath(member.getRegionId())); + } + return member; + } + + /** + * 获取区域的完整路径(从根到当前节点的ID列表) + */ + private List getRegionPath(Long regionId) { + List 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; } @Override diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/controller/PgSchoolController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/controller/PgSchoolController.java index 54cea20..a71d575 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/controller/PgSchoolController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/controller/PgSchoolController.java @@ -87,6 +87,14 @@ public class PgSchoolController extends BaseController { return R.ok(schoolService.selectGradesBySchoolId(schoolId)); } + /** + * 获取年级下的班级列表 + */ + @GetMapping("/grade/{schoolGradeId}/classes") + public R> getGradeClasses(@PathVariable Long schoolGradeId) { + return R.ok(schoolService.selectClassesBySchoolGradeId(schoolGradeId)); + } + /** * 为学校添加年级(批量挂载) */ diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java index 47a1454..6d6e32a 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/domain/PgSchoolClass.java @@ -1,6 +1,7 @@ package org.dromara.pangu.school.domain; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -33,4 +34,10 @@ public class PgSchoolClass implements Serializable { private Long createBy; private Date createTime; + + /** + * 班级名称(非数据库字段,用于前端显示) + */ + @TableField(exist = false) + private String className; } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/IPgSchoolService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/IPgSchoolService.java index 6c19f34..09c0073 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/IPgSchoolService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/IPgSchoolService.java @@ -50,4 +50,9 @@ public interface IPgSchoolService { * 删除年级下的班级 */ int removeGradeClass(Long schoolClassId); + + /** + * 获取年级下的班级列表 + */ + List selectClassesBySchoolGradeId(Long schoolGradeId); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java index 6a7a611..bbb7dfa 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/school/service/impl/PgSchoolServiceImpl.java @@ -230,6 +230,22 @@ public class PgSchoolServiceImpl implements IPgSchoolService { return schoolClassMapper.deleteById(schoolClassId); } + @Override + public List selectClassesBySchoolGradeId(Long schoolGradeId) { + List schoolClasses = schoolClassMapper.selectList( + new LambdaQueryWrapper() + .eq(PgSchoolClass::getSchoolGradeId, schoolGradeId) + ); + // 关联查询班级名称 + for (PgSchoolClass sc : schoolClasses) { + PgClass cls = classMapper.selectById(sc.getClassId()); + if (cls != null) { + sc.setClassName(cls.getClassName()); + } + } + return schoolClasses; + } + private LambdaQueryWrapper buildQueryWrapper(PgSchool school) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.like(StrUtil.isNotBlank(school.getSchoolName()), PgSchool::getSchoolName, school.getSchoolName()); diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java index 5e26839..96593b3 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/controller/PgStudentController.java @@ -58,4 +58,29 @@ public class PgStudentController extends BaseController { public R remove(@PathVariable Long[] studentIds) { return toAjax(studentService.deleteByIds(studentIds)); } + + /** + * 查询可绑定的学生列表(用于会员绑定学生) + * @param studentName 学生姓名(模糊查询) + * @param studentNo 学号(模糊查询) + * @param memberId 当前会员ID + * @param schoolId 学校ID(教师身份时传入,限制只能选本校学生) + */ + @GetMapping("/available") + public TableDataInfo availableStudents( + @RequestParam(required = false) String studentName, + @RequestParam(required = false) String studentNo, + @RequestParam(required = false) Long memberId, + @RequestParam(required = false) Long schoolId, + PageQuery pageQuery) { + return studentService.selectAvailableStudents(studentName, studentNo, memberId, schoolId, pageQuery); + } + + /** + * 查询会员已绑定的学生列表 + */ + @GetMapping("/byMember/{memberId}") + public R> listByMemberId(@PathVariable Long memberId) { + return R.ok(studentService.selectByMemberId(memberId)); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java index a4e46fc..3cd0363 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/IPgStudentService.java @@ -18,4 +18,29 @@ public interface IPgStudentService { int insert(PgStudent student); int update(PgStudent student); int deleteByIds(Long[] studentIds); + + /** + * 查询可绑定的学生列表(分页) + * @param studentName 学生姓名(模糊查询) + * @param studentNo 学号(模糊查询) + * @param memberId 当前会员ID(排除已绑定其他会员的学生) + * @param schoolId 学校ID(教师身份时必传,限制只能选本校学生) + * @param pageQuery 分页参数 + */ + TableDataInfo selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery); + + /** + * 查询会员已绑定的学生列表 + */ + List selectByMemberId(Long memberId); + + /** + * 批量绑定学生到会员 + */ + int bindStudentsToMember(Long memberId, List studentIds); + + /** + * 解绑学生 + */ + int unbindStudent(Long studentId); } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java index 16acf52..d4754f1 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/student/service/impl/PgStudentServiceImpl.java @@ -68,4 +68,65 @@ public class PgStudentServiceImpl implements IPgStudentService { lqw.orderByDesc(PgStudent::getCreateTime); return lqw; } + + @Override + public TableDataInfo selectAvailableStudents(String studentName, String studentNo, Long memberId, Long schoolId, PageQuery pageQuery) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + // 姓名模糊查询 + lqw.like(StrUtil.isNotBlank(studentName), PgStudent::getStudentName, studentName); + // 学号模糊查询 + lqw.like(StrUtil.isNotBlank(studentNo), PgStudent::getStudentNo, studentNo); + // 可绑定条件:未被绑定 或 已绑定当前会员 + lqw.and(wrapper -> wrapper + .isNull(PgStudent::getMemberId) + .or() + .eq(memberId != null, PgStudent::getMemberId, memberId) + ); + // 教师身份限制:只能选本校学生 + lqw.eq(schoolId != null, PgStudent::getSchoolId, schoolId); + lqw.orderByDesc(PgStudent::getCreateTime); + + Page page = baseMapper.selectPage(pageQuery.build(), lqw); + return TableDataInfo.build(page); + } + + @Override + public List selectByMemberId(Long memberId) { + if (memberId == null) { + return List.of(); + } + return baseMapper.selectList( + new LambdaQueryWrapper() + .eq(PgStudent::getMemberId, memberId) + .orderByDesc(PgStudent::getCreateTime) + ); + } + + @Override + public int bindStudentsToMember(Long memberId, List studentIds) { + if (memberId == null || studentIds == null || studentIds.isEmpty()) { + return 0; + } + int count = 0; + for (Long studentId : studentIds) { + PgStudent student = new PgStudent(); + student.setStudentId(studentId); + student.setMemberId(memberId); + count += baseMapper.updateById(student); + } + return count; + } + + @Override + public int unbindStudent(Long studentId) { + if (studentId == null) { + return 0; + } + // 使用原生SQL更新memberId为null + return baseMapper.update(null, + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(PgStudent::getStudentId, studentId) + .set(PgStudent::getMemberId, null) + ); + } } diff --git a/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue b/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue index 14b791e..7cae23b 100644 --- a/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue +++ b/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue @@ -140,8 +140,8 @@ - @@ -162,6 +165,7 @@ import { Plus } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { reactive, ref } from 'vue' import request from '@/utils/request' +import StudentSelectDialog from './StudentSelectDialog.vue' const emit = defineEmits(['success']) @@ -169,6 +173,7 @@ const visible = ref(false) const isEdit = ref(false) const formRef = ref(null) const submitLoading = ref(false) +const studentSelectRef = ref(null) // 表单数据 const form = reactive({ @@ -229,10 +234,9 @@ const open = async (row) => { const res = await request.get(`/business/member/${row.memberId}`) if (res.code === 200 && res.data) { Object.assign(form, res.data) - // 处理区域级联 + // 后端已返回 regionIds 数组用于级联选择器回显 + // 加载学校信息的下拉选项 if (form.regionId) { - // 构建regionIds数组用于级联选择器回显 - form.regionIds = [form.regionId] await loadSchoolList(form.regionId) } if (form.schoolId) { @@ -241,6 +245,8 @@ const open = async (row) => { if (form.schoolGradeId) { await loadClassList(form.schoolGradeId) } + // 加载已绑定的学生列表 + await loadBoundStudents() } } catch (e) { console.error('加载会员数据失败', e) @@ -248,6 +254,22 @@ const open = async (row) => { } } +/** + * 加载已绑定的学生列表 + */ +const loadBoundStudents = async () => { + if (!form.memberId) { + form.students = [] + return + } + try { + const res = await request.get(`/business/member/${form.memberId}/students`) + form.students = res.data || [] + } catch (e) { + form.students = [] + } +} + /** * 重置表单 */ @@ -287,8 +309,8 @@ const loadRegionTree = async () => { */ const loadSchoolList = async (regionId) => { try { - const res = await request.get('/business/school/list', { params: { regionId } }) - schoolList.value = res.rows || [] + const res = await request.get('/business/school/listAll', { params: { regionId } }) + schoolList.value = res.data || [] } catch (e) { schoolList.value = [] } @@ -299,7 +321,7 @@ const loadSchoolList = async (regionId) => { */ const loadGradeList = async (schoolId) => { try { - const res = await request.get('/business/school/grades', { params: { schoolId } }) + const res = await request.get(`/business/school/${schoolId}/grades`) gradeList.value = res.data || [] } catch (e) { gradeList.value = [] @@ -311,7 +333,7 @@ const loadGradeList = async (schoolId) => { */ const loadClassList = async (schoolGradeId) => { try { - const res = await request.get('/business/school/classes', { params: { schoolGradeId } }) + const res = await request.get(`/business/school/grade/${schoolGradeId}/classes`) classList.value = res.data || [] } catch (e) { classList.value = [] @@ -379,14 +401,30 @@ const handleGradeChange = () => { * 添加学生 */ const handleAddStudent = () => { - ElMessage.info('学生选择功能待开发') + if (!isEdit.value || !form.memberId) { + ElMessage.warning('请先保存会员信息后再绑定学生') + return + } + studentSelectRef.value?.open({ + memberId: form.memberId, + identityType: form.identityType, + schoolId: form.schoolId + }) } /** - * 移除学生 + * 解绑学生 */ -const handleRemoveStudent = (index) => { - form.students.splice(index, 1) +const handleRemoveStudent = async (row) => { + try { + const res = await request.post(`/business/member/unbindStudent/${row.studentId}`) + if (res.code === 200) { + ElMessage.success('解绑成功') + await loadBoundStudents() + } + } catch (e) { + console.error('解绑失败', e) + } } /** diff --git a/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue b/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue new file mode 100644 index 0000000..7f75fb3 --- /dev/null +++ b/frontend/ruoyi-ui/src/views/business/member/components/StudentSelectDialog.vue @@ -0,0 +1,238 @@ + + + + + +