package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.constants.MiniUserType; import com.sinata.appointment.dto.StaffGroupAppendMembersRequest; import com.sinata.appointment.dto.StaffGroupCreateRequest; import com.sinata.appointment.dto.StaffGroupDetailVo; import com.sinata.appointment.dto.StaffGroupListVo; import com.sinata.appointment.dto.StaffGroupMemberVo; import com.sinata.appointment.dto.StaffGroupUpdateRequest; import com.sinata.appointment.entity.MpStaffGroup; import com.sinata.appointment.entity.MpStaffGroupMember; import com.sinata.appointment.entity.MpUser; import com.sinata.appointment.exception.BizException; import com.sinata.appointment.mapper.MpStaffGroupMapper; import com.sinata.appointment.mapper.MpStaffGroupMemberMapper; import com.sinata.appointment.mapper.MpUserMapper; import com.sinata.appointment.mapper.SysDeptMapper; import com.sinata.appointment.util.MobileMaskUtil; import com.sinata.appointment.util.UuidAssert; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service public class StaffGroupManageApplicationService { private final MpStaffGroupMapper mpStaffGroupMapper; private final MpStaffGroupMemberMapper mpStaffGroupMemberMapper; private final MpUserMapper mpUserMapper; private final SysDeptMapper sysDeptMapper; public StaffGroupManageApplicationService( MpStaffGroupMapper mpStaffGroupMapper, MpStaffGroupMemberMapper mpStaffGroupMemberMapper, MpUserMapper mpUserMapper, SysDeptMapper sysDeptMapper) { this.mpStaffGroupMapper = mpStaffGroupMapper; this.mpStaffGroupMemberMapper = mpStaffGroupMemberMapper; this.mpUserMapper = mpUserMapper; this.sysDeptMapper = sysDeptMapper; } public IPage page( long page, long size, String groupName, String leaderMobile) { LambdaQueryWrapper q = new LambdaQueryWrapper() .eq(MpStaffGroup::getDelFlag, 0) .like(StringUtils.hasText(groupName), MpStaffGroup::getGroupName, groupName) .orderByDesc(MpStaffGroup::getCreateTime); if (StringUtils.hasText(leaderMobile)) { String kw = leaderMobile.trim(); List leaders = mpUserMapper.selectList( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0) .eq(MpUser::getUserType, MiniUserType.EMPLOYEE) .like(MpUser::getMobile, kw)); if (leaders.isEmpty()) { Page empty = new Page<>(page, size, 0); empty.setRecords(List.of()); return empty; } List leaderIds = leaders.stream().map(MpUser::getId).toList(); q.in(MpStaffGroup::getLeaderMpUserId, leaderIds); } Page p = mpStaffGroupMapper.selectPage(new Page<>(page, size), q); // preload leaders and counts Set leaderIds = new HashSet<>(); Set groupIds = new HashSet<>(); for (MpStaffGroup g : p.getRecords()) { groupIds.add(g.getId()); if (g.getLeaderMpUserId() != null) { leaderIds.add(g.getLeaderMpUserId()); } } Map leaderMap = mpUsersByIds(leaderIds); Map countMap = memberCountByGroupIds(groupIds); return p.convert( g -> { StaffGroupListVo vo = new StaffGroupListVo(); vo.setId(g.getId()); vo.setUuid(g.getUuid()); vo.setGroupName(g.getGroupName()); MpUser leader = g.getLeaderMpUserId() != null ? leaderMap.get(g.getLeaderMpUserId()) : null; vo.setLeaderName(leader != null ? leader.getNickName() : null); vo.setLeaderMobileMasked(leader != null ? MobileMaskUtil.mask(leader.getMobile()) : null); vo.setMemberCount(countMap.getOrDefault(g.getId(), 0)); vo.setCreateTime(g.getCreateTime()); return vo; }); } public StaffGroupDetailVo detail(Long id, String uuid) { MpStaffGroup g = loadActiveWithUuid(id, uuid); StaffGroupDetailVo vo = new StaffGroupDetailVo(); vo.setId(g.getId()); vo.setUuid(g.getUuid()); vo.setGroupName(g.getGroupName()); vo.setLeaderMpUserId(g.getLeaderMpUserId()); vo.setCreateTime(g.getCreateTime()); // members List rels = mpStaffGroupMemberMapper.selectList( new LambdaQueryWrapper() .eq(MpStaffGroupMember::getDelFlag, 0) .eq(MpStaffGroupMember::getGroupId, g.getId()) .orderByAsc(MpStaffGroupMember::getId)); Set memberIds = new HashSet<>(); for (MpStaffGroupMember r : rels) { memberIds.add(r.getMpUserId()); } Map memberMap = mpUsersByIds(memberIds); Map deptNames = deptNamesByMemberMap(memberMap); List members = new ArrayList<>(); for (MpStaffGroupMember r : rels) { MpUser u = memberMap.get(r.getMpUserId()); if (u == null) { continue; } StaffGroupMemberVo m = new StaffGroupMemberVo(); m.setId(u.getId()); m.setUuid(u.getUuid()); m.setRealName(u.getNickName()); m.setStaffNo(u.getStaffNo()); m.setDeptId(u.getDeptId()); m.setDeptName(u.getDeptId() != null ? deptNames.get(u.getDeptId()) : null); members.add(m); } vo.setMembers(members); vo.setMemberCount(members.size()); // leader display if (g.getLeaderMpUserId() != null) { MpUser leader = memberMap.get(g.getLeaderMpUserId()); if (leader == null) { leader = mpUserMapper.selectById(g.getLeaderMpUserId()); } if (leader != null) { vo.setLeaderName(leader.getNickName()); vo.setLeaderMobile(leader.getMobile()); vo.setLeaderMobileMasked(MobileMaskUtil.mask(leader.getMobile())); } } return vo; } @Transactional(rollbackFor = Exception.class) public Map create(StaffGroupCreateRequest req) { String name = req.getGroupName().trim(); List memberIds = dedup(req.getMemberIds()); Long leaderId = req.getLeaderMpUserId(); if (memberIds.isEmpty()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "关联人员不能为空"); } if (leaderId == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人不能为空"); } assertLeaderHasUsableMobile(leaderId); validateMembers(memberIds); if (!memberIds.contains(leaderId)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人必须在关联人员中"); } MpStaffGroup g = new MpStaffGroup(); g.setGroupName(name); g.setLeaderMpUserId(leaderId); mpStaffGroupMapper.insert(g); saveMembers(g.getId(), memberIds); MpStaffGroup fresh = mpStaffGroupMapper.selectById(g.getId()); return Map.of("id", fresh.getId(), "uuid", fresh.getUuid()); } @Transactional(rollbackFor = Exception.class) public void update(Long id, StaffGroupUpdateRequest req) { MpStaffGroup g = loadActiveWithUuid(id, req.getUuid()); String name = req.getGroupName().trim(); List memberIds = dedup(req.getMemberIds()); Long leaderId = req.getLeaderMpUserId(); if (memberIds.isEmpty()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "关联人员不能为空"); } if (leaderId == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人不能为空"); } assertLeaderHasUsableMobile(leaderId); validateMembers(memberIds); if (!memberIds.contains(leaderId)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人必须在关联人员中"); } g.setGroupName(name); g.setLeaderMpUserId(leaderId); mpStaffGroupMapper.updateById(g); deleteMembersPhysically(g.getId()); saveMembers(g.getId(), memberIds); } @Transactional(rollbackFor = Exception.class) public void delete(Long id, String uuid) { MpStaffGroup g = loadActiveWithUuid(id, uuid); int affected = mpStaffGroupMapper.deleteById(g.getId()); if (affected <= 0) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "删除失败,请刷新后重试"); } deleteMembersPhysically(g.getId()); } @Transactional(rollbackFor = Exception.class) public void appendMembers(Long id, StaffGroupAppendMembersRequest req) { MpStaffGroup g = loadActiveWithUuid(id, req.getUuid()); List ids = dedup(req.getMemberIds()); if (ids.isEmpty()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请至少选择一名成员"); } validateMembers(ids); Set existing = existingMemberMpUserIds(g.getId()); int added = 0; for (Long mid : ids) { if (existing.contains(mid)) { continue; } MpStaffGroupMember rel = new MpStaffGroupMember(); rel.setGroupId(g.getId()); rel.setMpUserId(mid); mpStaffGroupMemberMapper.insert(rel); existing.add(mid); added++; } if (added == 0) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "所选成员均已在分组中"); } } @Transactional(rollbackFor = Exception.class) public void removeMember(Long groupId, String uuid, Long mpUserId) { MpStaffGroup g = loadActiveWithUuid(groupId, uuid); if (mpUserId == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "成员无效"); } if (g.getLeaderMpUserId() != null && g.getLeaderMpUserId().equals(mpUserId)) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "不能移除负责人"); } MpStaffGroupMember one = mpStaffGroupMemberMapper.selectOne( new LambdaQueryWrapper() .eq(MpStaffGroupMember::getGroupId, g.getId()) .eq(MpStaffGroupMember::getMpUserId, mpUserId) .eq(MpStaffGroupMember::getDelFlag, 0)); if (one == null) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "该成员不在分组中"); } mpStaffGroupMemberMapper.deletePhysicalByGroupAndUser(g.getId(), mpUserId); } private Set existingMemberMpUserIds(Long groupId) { List rels = mpStaffGroupMemberMapper.selectList( new LambdaQueryWrapper() .eq(MpStaffGroupMember::getGroupId, groupId) .eq(MpStaffGroupMember::getDelFlag, 0)); Set set = new HashSet<>(); for (MpStaffGroupMember r : rels) { if (r.getMpUserId() != null) { set.add(r.getMpUserId()); } } return set; } private void deleteMembersPhysically(Long groupId) { mpStaffGroupMemberMapper.deletePhysicalByGroupId(groupId); } private void assertLeaderHasUsableMobile(Long leaderMpUserId) { MpUser u = mpUserMapper.selectById(leaderMpUserId); if (u == null || (u.getDelFlag() != null && u.getDelFlag() == 1)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人不存在"); } if (!MiniUserType.EMPLOYEE.equals(u.getUserType())) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人须为员工"); } if (u.getStatus() != null && u.getStatus() == 0) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "负责人已禁用"); } if (!StringUtils.hasText(u.getMobile())) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "负责人联系电话不能为空"); } } private MpStaffGroup loadActiveWithUuid(Long id, String uuid) { MpStaffGroup g = mpStaffGroupMapper.selectById(id); if (g == null || (g.getDelFlag() != null && g.getDelFlag() == 1)) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "资源不存在或无权访问"); } UuidAssert.match(g, uuid); return g; } private Map mpUsersByIds(Set ids) { Map map = new HashMap<>(); if (ids == null || ids.isEmpty()) { return map; } List users = mpUserMapper.selectList( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0) .in(MpUser::getId, ids)); for (MpUser u : users) { map.put(u.getId(), u); } return map; } private Map memberCountByGroupIds(Set groupIds) { Map map = new HashMap<>(); if (groupIds == null || groupIds.isEmpty()) { return map; } List rels = mpStaffGroupMemberMapper.selectList( new LambdaQueryWrapper() .eq(MpStaffGroupMember::getDelFlag, 0) .in(MpStaffGroupMember::getGroupId, groupIds)); for (MpStaffGroupMember r : rels) { map.put(r.getGroupId(), map.getOrDefault(r.getGroupId(), 0) + 1); } return map; } private Map deptNamesByMemberMap(Map memberMap) { Set deptIds = new HashSet<>(); for (MpUser u : memberMap.values()) { if (u.getDeptId() != null) { deptIds.add(u.getDeptId()); } } if (deptIds.isEmpty()) { return new HashMap<>(); } Map out = new HashMap<>(); List depts = sysDeptMapper.selectBatchIds(deptIds); for (com.sinata.appointment.entity.SysDept d : depts) { out.put(d.getId(), d.getDeptName()); } return out; } private static List dedup(List ids) { if (ids == null || ids.isEmpty()) { return List.of(); } Set set = new java.util.LinkedHashSet<>(ids); return new java.util.ArrayList<>(set); } private void validateMembers(List memberIds) { if (memberIds == null) { return; } for (Long id : memberIds) { MpUser u = mpUserMapper.selectById(id); if (u == null || (u.getDelFlag() != null && u.getDelFlag() == 1)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "关联人员不存在: " + id); } if (!MiniUserType.EMPLOYEE.equals(u.getUserType())) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "仅允许绑定员工: " + id); } if (u.getStatus() != null && u.getStatus() == 0) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "员工已禁用: " + id); } } } private void saveMembers(Long groupId, List memberIds) { if (memberIds == null || memberIds.isEmpty()) { return; } for (Long mid : memberIds) { MpStaffGroupMember rel = new MpStaffGroupMember(); rel.setGroupId(groupId); rel.setMpUserId(mid); mpStaffGroupMemberMapper.insert(rel); } } }