package com.tourguide.modules.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tourguide.common.api.PageResponse; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.user.dto.GuideRegistrationTeamOptionResponse; import com.tourguide.modules.user.dto.GuideTeamResponse; import com.tourguide.modules.user.dto.GuideTeamCollectionGuideResponse; import com.tourguide.modules.user.dto.GuideTeamScenicRequest; import com.tourguide.modules.user.dto.GuideTeamSaveRequest; import com.tourguide.modules.user.entity.GuideTeam; import com.tourguide.modules.user.entity.GuideTeamScenicConfig; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideTeamMapper; import com.tourguide.modules.user.mapper.GuideTeamScenicConfigMapper; import com.tourguide.modules.user.mapper.GuideUserMapper; import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper; import com.tourguide.modules.operation.entity.OperationCity; import com.tourguide.modules.operation.entity.OperationScenicSpot; import com.tourguide.modules.operation.entity.ScenicMeetingPoint; import com.tourguide.modules.operation.mapper.OperationCityMapper; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import com.tourguide.modules.operation.mapper.ScenicMeetingPointMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Service public class GuideTeamService { private final GuideTeamMapper teamMapper; private final GuideTeamScenicConfigMapper scenicConfigMapper; private final GuideUserMapper guideUserMapper; private final GuideWithdrawalMapper withdrawalMapper; private final OperationScenicSpotMapper operationScenicSpotMapper; private final OperationCityMapper operationCityMapper; private final ScenicMeetingPointMapper meetingPointMapper; public GuideTeamService(GuideTeamMapper teamMapper, GuideTeamScenicConfigMapper scenicConfigMapper, GuideUserMapper guideUserMapper, GuideWithdrawalMapper withdrawalMapper) { this(teamMapper, scenicConfigMapper, guideUserMapper, withdrawalMapper, null, null, null); } @Autowired public GuideTeamService(GuideTeamMapper teamMapper, GuideTeamScenicConfigMapper scenicConfigMapper, GuideUserMapper guideUserMapper, GuideWithdrawalMapper withdrawalMapper, OperationScenicSpotMapper operationScenicSpotMapper, OperationCityMapper operationCityMapper, ScenicMeetingPointMapper meetingPointMapper) { this.teamMapper = teamMapper; this.scenicConfigMapper = scenicConfigMapper; this.guideUserMapper = guideUserMapper; this.withdrawalMapper = withdrawalMapper; this.operationScenicSpotMapper = operationScenicSpotMapper; this.operationCityMapper = operationCityMapper; this.meetingPointMapper = meetingPointMapper; } public GuideTeamResponse detail(Long id) { GuideTeam team = requireTeam(id); List scenicConfigs = scenicConfigMapper.selectList( new LambdaQueryWrapper() .eq(GuideTeamScenicConfig::getTeamId, id) .orderByAsc(GuideTeamScenicConfig::getId)); return GuideTeamResponse.from(team, scenicConfigs); } public PageResponse page(long page, long size, String keyword, Integer status, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String searchKeyword = keyword.trim(); query.and(wrapper -> wrapper.like(GuideTeam::getTeamName, searchKeyword) .or().like(GuideTeam::getContactPhone, searchKeyword)); } query.eq(status != null, GuideTeam::getStatus, status) .ge(startDate != null, GuideTeam::getCreatedAt, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, GuideTeam::getCreatedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(GuideTeam::getId); Page result = teamMapper.selectPage(new Page(page, size), query); return new PageResponse( result.getRecords().stream().map(GuideTeamResponse::from).collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } public List registrationOptions() { List options = new ArrayList(); options.add(GuideRegistrationTeamOptionResponse.personal()); List teams = teamMapper.selectList(new LambdaQueryWrapper() .eq(GuideTeam::getStatus, 1) .orderByAsc(GuideTeam::getTeamName)); for (GuideTeam team : teams) { options.add(GuideRegistrationTeamOptionResponse.team(team)); } return options; } public List collectionGuideOptions(Long teamId) { requireTeam(teamId); return guideUserMapper.selectList(new LambdaQueryWrapper() .eq(GuideUser::getTeamId, teamId) .orderByAsc(GuideUser::getRealName) .orderByAsc(GuideUser::getId)) .stream() .map(guide -> new GuideTeamCollectionGuideResponse( guide, withdrawalMapper.countActiveByGuideId(guide.getId()) > 0)) .collect(Collectors.toList()); } @Transactional public GuideTeamResponse create(GuideTeamSaveRequest request) { ensureNameAvailable(request.getTeamName(), null); GuideTeam team = new GuideTeam(); apply(team, request); team.setCollectionGuideId(null); team.setBalance(java.math.BigDecimal.ZERO); team.setStatus(1); teamMapper.insert(team); if (request.getCollectionGuideId() != null) { validateCollectionGuide(team.getId(), request.getCollectionGuideId()); team.setCollectionGuideId(request.getCollectionGuideId()); teamMapper.updateById(team); } replaceScenicConfigs(team.getId(), request.getScenicConfigs()); return detail(team.getId()); } @Transactional public GuideTeamResponse update(Long id, GuideTeamSaveRequest request) { GuideTeam team = requireTeam(id); ensureNameAvailable(request.getTeamName(), id); if (!java.util.Objects.equals(team.getCollectionGuideId(), request.getCollectionGuideId())) { validateCollectionGuide(id, request.getCollectionGuideId()); } apply(team, request); teamMapper.updateById(team); replaceScenicConfigs(id, request.getScenicConfigs()); return detail(id); } @Transactional public void updateStatus(Long id, Integer status) { GuideTeam team = requireTeam(id); team.setStatus(status); teamMapper.updateById(team); } private GuideTeam requireTeam(Long id) { GuideTeam team = teamMapper.selectById(id); if (team == null) { throw new BusinessException(404, "讲解队不存在"); } return team; } private void ensureNameAvailable(String teamName, Long excludedId) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(GuideTeam::getTeamName, teamName.trim()) .ne(excludedId != null, GuideTeam::getId, excludedId); if (teamMapper.selectCount(query) > 0) { throw new BusinessException(409, "讲解队名称已存在"); } } private void apply(GuideTeam team, GuideTeamSaveRequest request) { List scenicConfigs = request.getScenicConfigs() == null ? Collections.emptyList() : request.getScenicConfigs(); team.setTeamName(request.getTeamName().trim()); team.setContactPhone(request.getContactPhone().trim()); team.setOperatingArea(scenicConfigs.stream() .map(GuideTeamScenicRequest::getScenicName) .filter(StringUtils::hasText) .map(String::trim) .distinct() .collect(Collectors.joining(","))); team.setCompanyName(trimToNull(request.getCompanyName())); team.setUnifiedSocialCreditCode(trimToNull(request.getUnifiedSocialCreditCode())); team.setCommissionRatio(request.getCommissionRatio()); team.setWechatOpenId(trimToNull(request.getWechatOpenId())); team.setMerchantId(trimToNull(request.getMerchantId())); team.setCollectionGuideId(request.getCollectionGuideId()); } private void validateCollectionGuide(Long teamId, Long guideId) { if (guideId == null) return; GuideUser guide = guideUserMapper.selectForUpdate(guideId); if (guide == null || guide.getStatus() == null || guide.getStatus() != 1) { throw new BusinessException(409, "收款导游不存在或已停用"); } if (!teamId.equals(guide.getTeamId())) { throw new BusinessException(409, "收款导游必须属于当前讲解队"); } if (guide.getBalance() != null && guide.getBalance().signum() != 0) { throw new BusinessException(409, "导游账户余额必须为0才能配置为收款导游"); } if (withdrawalMapper.countActiveByGuideId(guideId) > 0) { throw new BusinessException(409, "导游存在进行中的提现单,不能配置为收款导游"); } } private void replaceScenicConfigs(Long teamId, List requests) { scenicConfigMapper.markDeletedByTeamId(teamId); if (requests == null) { return; } Set scenicNames = new HashSet(); for (GuideTeamScenicRequest request : requests) { validateManagedScenic(request); String scenicName = request.getScenicName().trim(); if (!scenicNames.add(scenicName)) { throw new BusinessException(409, "同一讲解队不能重复配置相同景区"); } GuideTeamScenicConfig config = new GuideTeamScenicConfig(); config.setTeamId(teamId); config.setScenicName(scenicName); config.setCityName(request.getCityName().trim()); config.setMeetingPoints(request.getMeetingPoints().trim()); scenicConfigMapper.upsert(config); } } private void validateManagedScenic(GuideTeamScenicRequest request) { if (operationScenicSpotMapper == null || operationCityMapper == null || meetingPointMapper == null) { throw new BusinessException(500, "景区配置校验服务未初始化"); } OperationCity city = operationCityMapper.selectOne(new LambdaQueryWrapper() .eq(OperationCity::getCityName, request.getCityName().trim()) .eq(OperationCity::getStatus, 1) .last("LIMIT 1")); if (city == null) { throw new BusinessException(400, "所选景区城市不存在或已停用"); } OperationScenicSpot scenicSpot = operationScenicSpotMapper.selectOne( new LambdaQueryWrapper() .eq(OperationScenicSpot::getScenicName, request.getScenicName().trim()) .eq(OperationScenicSpot::getCityId, city.getId()) .eq(OperationScenicSpot::getStatus, 1) .last("LIMIT 1")); if (scenicSpot == null) { throw new BusinessException(400, "所选景区不存在或已停用"); } Set allowedMeetingPoints = meetingPointMapper.selectList( new LambdaQueryWrapper() .eq(ScenicMeetingPoint::getScenicSpotId, scenicSpot.getId()) .eq(ScenicMeetingPoint::getStatus, 1)) .stream().map(ScenicMeetingPoint::getPointName).collect(Collectors.toSet()); List selectedMeetingPoints = java.util.Arrays.stream(request.getMeetingPoints().split("[,,]")) .map(String::trim).filter(StringUtils::hasText).collect(Collectors.toList()); if (selectedMeetingPoints.isEmpty() || selectedMeetingPoints.size() != new HashSet(selectedMeetingPoints).size() || !allowedMeetingPoints.containsAll(selectedMeetingPoints)) { throw new BusinessException(400, "汇合点必须从所选景区的启用汇合点中选择"); } request.setScenicName(scenicSpot.getScenicName()); request.setCityName(city.getCityName()); request.setMeetingPoints(String.join(",", selectedMeetingPoints)); } private String trimToNull(String value) { return StringUtils.hasText(value) ? value.trim() : null; } }