From 5ef8420acee1315ccaf6911dfce476481b7c93f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E7=A0=81-=E6=96=B9=E6=99=93=E8=BE=89?= Date: Mon, 2 Feb 2026 19:04:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AD=A6=E6=A0=A1=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=A0=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 默认折叠:移除 default-expand-all 2. 修复折叠展开联动问题:row-key 改为 type_id 组合,保证唯一性 3. 删除提示优化: - 有子级时显示具体数量(如"3个年级、5个班级") - 有子级时弹出 alert 提示"请先删除子级",而不是直接删除 --- .../member/controller/PgMemberController.java | 49 ++++ .../member/service/IPgMemberService.java | 58 +++++ .../service/impl/PgMemberServiceImpl.java | 160 +++++++++++++ .../member/components/MemberDialog.vue | 215 +++++++++++------- .../member/components/ResetPwdDialog.vue | 101 ++++++++ .../src/views/business/member/index.vue | 126 ++++++---- .../src/views/business/school/index.vue | 49 +++- 7 files changed, 623 insertions(+), 135 deletions(-) create mode 100644 frontend/ruoyi-ui/src/views/business/member/components/ResetPwdDialog.vue 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 38a50dc..6ccd881 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 @@ -13,6 +13,9 @@ import org.dromara.pangu.member.service.IPgMemberService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.Map; + /** * 会员管理 * @@ -26,18 +29,27 @@ public class PgMemberController extends BaseController { private final IPgMemberService memberService; + /** + * 查询会员列表 + */ @SaCheckPermission("business:member:list") @GetMapping("/list") public TableDataInfo list(PgMember member, PageQuery pageQuery) { return memberService.selectPageList(member, pageQuery); } + /** + * 获取会员详情 + */ @SaCheckPermission("business:member:query") @GetMapping("/{memberId}") public R getInfo(@PathVariable Long memberId) { return R.ok(memberService.selectById(memberId)); } + /** + * 新增会员 + */ @SaCheckPermission("business:member:add") @Log(title = "会员管理", businessType = BusinessType.INSERT) @PostMapping @@ -45,6 +57,9 @@ public class PgMemberController extends BaseController { return toAjax(memberService.insert(member)); } + /** + * 修改会员 + */ @SaCheckPermission("business:member:edit") @Log(title = "会员管理", businessType = BusinessType.UPDATE) @PutMapping @@ -52,10 +67,44 @@ public class PgMemberController extends BaseController { return toAjax(memberService.update(member)); } + /** + * 删除会员 + */ @SaCheckPermission("business:member:remove") @Log(title = "会员管理", businessType = BusinessType.DELETE) @DeleteMapping("/{memberIds}") public R remove(@PathVariable Long[] memberIds) { return toAjax(memberService.deleteByIds(memberIds)); } + + /** + * 重置会员密码 + */ + @SaCheckPermission("business:member:resetPwd") + @Log(title = "会员管理-重置密码", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd/{memberId}") + public R> resetPwd(@PathVariable Long memberId) { + String newPassword = memberService.resetPassword(memberId); + Map result = new HashMap<>(2); + result.put("password", newPassword); + return R.ok(result); + } + + /** + * 修改会员状态 + */ + @SaCheckPermission("business:member:edit") + @Log(title = "会员管理-状态变更", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public R changeStatus(@RequestBody PgMember member) { + return toAjax(memberService.changeStatus(member.getMemberId(), member.getStatus())); + } + + /** + * 检查手机号是否唯一 + */ + @GetMapping("/checkPhoneUnique") + public R checkPhoneUnique(@RequestParam String phone, @RequestParam(required = false) Long memberId) { + return R.ok(memberService.checkPhoneUnique(phone, memberId)); + } } diff --git a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberService.java b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberService.java index 3065124..5a5f5b0 100644 --- a/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberService.java +++ b/backend/ruoyi-modules/pangu-business/src/main/java/org/dromara/pangu/member/service/IPgMemberService.java @@ -12,10 +12,68 @@ import java.util.List; * @author pangu */ public interface IPgMemberService { + + /** + * 分页查询会员列表 + */ TableDataInfo selectPageList(PgMember member, PageQuery pageQuery); + + /** + * 查询会员列表 + */ List selectList(PgMember member); + + /** + * 根据ID获取会员详情 + */ PgMember selectById(Long memberId); + + /** + * 新增会员 + */ int insert(PgMember member); + + /** + * 修改会员 + */ int update(PgMember member); + + /** + * 批量删除会员 + */ int deleteByIds(Long[] memberIds); + + /** + * 删除单个会员 + */ + int deleteById(Long memberId); + + /** + * 重置会员密码 + * @return 新密码 + */ + String resetPassword(Long memberId); + + /** + * 修改会员状态 + */ + int changeStatus(Long memberId, String status); + + /** + * 检查手机号是否唯一 + * @param phone 手机号 + * @param memberId 会员ID(编辑时排除自己) + * @return true=唯一,false=不唯一 + */ + boolean checkPhoneUnique(String phone, Long memberId); + + /** + * 检查会员是否可删除(是否绑定学生) + */ + boolean checkCanDelete(Long memberId); + + /** + * 根据手机号查询会员 + */ + PgMember selectByPhone(String phone); } 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 a832747..ce4a766 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 @@ -1,17 +1,23 @@ package org.dromara.pangu.member.service.impl; +import cn.hutool.core.util.RandomUtil; 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 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.mapper.PgMemberMapper; import org.dromara.pangu.member.service.IPgMemberService; +import org.dromara.pangu.student.mapper.PgStudentMapper; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; +import java.util.Date; import java.util.List; /** @@ -24,6 +30,10 @@ import java.util.List; public class PgMemberServiceImpl implements IPgMemberService { private final PgMemberMapper baseMapper; + private final PgStudentMapper studentMapper; + + private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + private static final String DEFAULT_PASSWORD = "123456"; @Override public TableDataInfo selectPageList(PgMember member, PageQuery pageQuery) { @@ -43,20 +53,170 @@ public class PgMemberServiceImpl implements IPgMemberService { } @Override + @Transactional(rollbackFor = Exception.class) public int insert(PgMember member) { + // 校验手机号唯一性 + if (!checkPhoneUnique(member.getPhone(), null)) { + throw new ServiceException("手机号已存在"); + } + + // 生成会员编号: JS + 时间戳 + member.setMemberCode("JS" + System.currentTimeMillis()); + + // 昵称未填写时自动生成: 用户 + 手机号后4位 + if (StrUtil.isBlank(member.getNickname()) && StrUtil.isNotBlank(member.getPhone())) { + member.setNickname("用户" + member.getPhone().substring(member.getPhone().length() - 4)); + } + + // 密码加密(默认密码) + if (StrUtil.isBlank(member.getPassword())) { + member.setPassword(PASSWORD_ENCODER.encode(DEFAULT_PASSWORD)); + } else { + member.setPassword(PASSWORD_ENCODER.encode(member.getPassword())); + } + + // 设置注册时间和来源 + member.setRegisterTime(new Date()); + if (StrUtil.isBlank(member.getRegisterSource())) { + member.setRegisterSource("3"); // 后台新增 + } + + // 默认状态为正常 + if (StrUtil.isBlank(member.getStatus())) { + member.setStatus("0"); + } + + // 教师身份校验必填字段 + validateTeacherInfo(member); + return baseMapper.insert(member); } @Override + @Transactional(rollbackFor = Exception.class) public int update(PgMember member) { + // 校验手机号唯一性 + if (StrUtil.isNotBlank(member.getPhone()) && !checkPhoneUnique(member.getPhone(), member.getMemberId())) { + 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); + return baseMapper.updateById(member); } @Override + @Transactional(rollbackFor = Exception.class) public int deleteByIds(Long[] memberIds) { + // 检查每个会员是否可删除 + for (Long memberId : memberIds) { + if (!checkCanDelete(memberId)) { + PgMember member = baseMapper.selectById(memberId); + String nickname = member != null ? member.getNickname() : String.valueOf(memberId); + throw new ServiceException("会员【" + nickname + "】已绑定学生,请先解绑学生后再删除"); + } + } return baseMapper.deleteByIds(Arrays.asList(memberIds)); } + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteById(Long memberId) { + if (!checkCanDelete(memberId)) { + throw new ServiceException("该会员已绑定学生,请先解绑学生后再删除"); + } + return baseMapper.deleteById(memberId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String resetPassword(Long memberId) { + PgMember member = baseMapper.selectById(memberId); + if (member == null) { + throw new ServiceException("会员不存在"); + } + + // 生成8位随机密码 + String newPassword = RandomUtil.randomString(8); + + PgMember updateMember = new PgMember(); + updateMember.setMemberId(memberId); + updateMember.setPassword(PASSWORD_ENCODER.encode(newPassword)); + baseMapper.updateById(updateMember); + + return newPassword; + } + + @Override + public int changeStatus(Long memberId, String status) { + PgMember member = new PgMember(); + member.setMemberId(memberId); + member.setStatus(status); + return baseMapper.updateById(member); + } + + @Override + public boolean checkPhoneUnique(String phone, Long memberId) { + if (StrUtil.isBlank(phone)) { + return true; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PgMember::getPhone, phone); + if (memberId != null) { + wrapper.ne(PgMember::getMemberId, memberId); + } + return baseMapper.selectCount(wrapper) == 0; + } + + @Override + public boolean checkCanDelete(Long memberId) { + // 检查是否有绑定的学生 + Long count = studentMapper.selectCount( + new LambdaQueryWrapper() + .eq(org.dromara.pangu.student.domain.PgStudent::getMemberId, memberId) + ); + return count == 0; + } + + @Override + public PgMember selectByPhone(String phone) { + return baseMapper.selectOne( + new LambdaQueryWrapper().eq(PgMember::getPhone, phone) + ); + } + + /** + * 校验教师身份必填字段 + */ + 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 buildQueryWrapper(PgMember member) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.like(StrUtil.isNotBlank(member.getNickname()), PgMember::getNickname, member.getNickname()); 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 47c88b9..14b791e 100644 --- a/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue +++ b/frontend/ruoyi-ui/src/views/business/member/components/MemberDialog.vue @@ -5,11 +5,10 @@ - - + + - - - + + - + + + + + @@ -94,13 +111,13 @@ :total="total" layout="total, sizes, prev, pager, next, jumper" style="margin-top: 16px; justify-content: flex-end" - @size-change="handleQuery" - @current-change="handleQuery" + @size-change="getList" + @current-change="getList" /> - + @@ -130,9 +147,7 @@ const queryParams = ref({ phone: '', nickname: '', identityType: '', - status: '', - beginTime: '', - endTime: '' + status: '' }) const memberDialogRef = ref() @@ -144,22 +159,28 @@ const maskPhone = (phone) => { return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') } +// 格式化注册来源 +const formatRegisterSource = (source) => { + const map = { '1': '小程序', '2': 'H5', '3': '后台新增', '4': '批量导入' } + return map[source] || source +} + // 获取会员列表 const getList = async () => { loading.value = true - // 处理日期范围 - if (dateRange.value && dateRange.value.length === 2) { - queryParams.value.beginTime = dateRange.value[0] - queryParams.value.endTime = dateRange.value[1] - } else { - queryParams.value.beginTime = '' - queryParams.value.endTime = '' - } try { - const res = await request.get('/business/member/list', { params: queryParams.value }) + const params = { ...queryParams.value } + // 处理日期范围 + if (dateRange.value && dateRange.value.length === 2) { + params.params = { + beginRegisterTime: dateRange.value[0], + endRegisterTime: dateRange.value[1] + } + } + const res = await request.get('/business/member/list', { params }) if (res.code === 200) { - tableData.value = res.rows - total.value = res.total + tableData.value = res.rows || [] + total.value = res.total || 0 } } finally { loading.value = false @@ -180,9 +201,7 @@ const resetQuery = () => { phone: '', nickname: '', identityType: '', - status: '', - beginTime: '', - endTime: '' + status: '' } dateRange.value = [] getList() @@ -200,22 +219,49 @@ const handleEdit = (row) => { // 重置密码 const handleResetPwd = (row) => { - resetPwdDialogRef.value?.open(row) -} - -// 删除 -const handleDelete = (row) => { - ElMessageBox.confirm(`确定要删除会员"${row.nickname}"吗?`, '提示', { + ElMessageBox.confirm(`确定要重置会员"${row.nickname || row.phone}"的密码吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { - const res = await request.delete(`/business/member/${row.id}`) + const res = await request.put(`/business/member/resetPwd/${row.memberId}`) + if (res.code === 200) { + resetPwdDialogRef.value?.open(res.data.password) + } + }).catch(() => {}) +} + +// 删除 +const handleDelete = async (row) => { + try { + const res = await request.delete(`/business/member/${row.memberId}`) if (res.code === 200) { ElMessage.success('删除成功') getList() } - }).catch(() => {}) + } catch (e) { + // 错误已在request中处理 + } +} + +// 状态变更 +const handleStatusChange = async (row) => { + const text = row.status === '0' ? '启用' : '停用' + try { + const res = await request.put('/business/member/changeStatus', { + memberId: row.memberId, + status: row.status + }) + if (res.code === 200) { + ElMessage.success(`${text}成功`) + } else { + // 恢复原状态 + row.status = row.status === '0' ? '1' : '0' + } + } catch (e) { + // 恢复原状态 + row.status = row.status === '0' ? '1' : '0' + } } onMounted(() => { diff --git a/frontend/ruoyi-ui/src/views/business/school/index.vue b/frontend/ruoyi-ui/src/views/business/school/index.vue index cd6f492..638df52 100644 --- a/frontend/ruoyi-ui/src/views/business/school/index.vue +++ b/frontend/ruoyi-ui/src/views/business/school/index.vue @@ -56,9 +56,8 @@ { classDialogRef.value?.open(schoolData) } +// 统计子级数量 +const countChildren = (node) => { + let gradeCount = 0 + let classCount = 0 + if (node.children && node.children.length > 0) { + for (const child of node.children) { + if (child.type === 'grade') { + gradeCount++ + // 统计年级下的班级 + if (child.children && child.children.length > 0) { + classCount += child.children.filter(c => c.type === 'class').length + } + } else if (child.type === 'class') { + classCount++ + } + } + } + return { gradeCount, classCount } +} + // 删除操作 - 删除时提示是否有子级 const handleDelete = (row, type) => { let message = '' let url = '' if (type === 'school') { - // 检查是否有子级 - if (row.children && row.children.length > 0) { - message = `学校"${row.name}"下有年级/班级,确定要删除吗?删除后其下的年级和班级也将被删除。` - } else { - message = `确定要删除学校"${row.name}"吗?` + const { gradeCount, classCount } = countChildren(row) + if (gradeCount > 0 || classCount > 0) { + // 有子级时,提示需要先删除子级 + let childInfo = [] + if (gradeCount > 0) childInfo.push(`${gradeCount}个年级`) + if (classCount > 0) childInfo.push(`${classCount}个班级`) + message = `学校"${row.name}"下存在${childInfo.join('、')},请先删除年级和班级后再删除学校。` + ElMessageBox.alert(message, '无法删除', { type: 'warning' }) + return } + message = `确定要删除学校"${row.name}"吗?` url = `/business/school/${row.id}` } else if (type === 'grade') { - if (row.children && row.children.length > 0) { - message = `年级"${row.name}"下有班级,确定要删除吗?其下的班级也将被删除。` - } else { - message = `确定要删除年级"${row.name}"吗?` + const classCount = (row.children || []).filter(c => c.type === 'class').length + if (classCount > 0) { + // 有班级时,提示需要先删除班级 + message = `年级"${row.name}"下存在${classCount}个班级,请先删除班级后再删除年级。` + ElMessageBox.alert(message, '无法删除', { type: 'warning' }) + return } + message = `确定要删除年级"${row.name}"吗?` url = `/business/school/grade/${row.id}` } else { message = `确定要删除班级"${row.name}"吗?`