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.GuideAutoDisableConfigRequest; import com.tourguide.modules.user.dto.GuideAutoDisableConfigResponse; import com.tourguide.modules.user.dto.GuideProfileUpdateRequest; import com.tourguide.modules.user.dto.GuideScenicConfigResponse; import com.tourguide.modules.user.dto.GuideUserCreateRequest; import com.tourguide.modules.user.dto.GuideUserDetailResponse; import com.tourguide.modules.user.dto.GuideUserDetailUpdateRequest; import com.tourguide.modules.user.dto.GuideUserResponse; import com.tourguide.modules.user.entity.GuideAutoDisableConfig; import com.tourguide.modules.user.entity.GuideApplication; import com.tourguide.modules.user.entity.GuideScenicConfig; import com.tourguide.modules.user.entity.GuideTeam; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideAutoDisableConfigMapper; import com.tourguide.modules.user.mapper.GuideScenicConfigMapper; import com.tourguide.modules.user.mapper.GuideTeamMapper; import com.tourguide.modules.user.mapper.GuideUserMapper; import com.tourguide.modules.operation.entity.OperationCity; import com.tourguide.modules.operation.entity.OperationScenicSpot; import com.tourguide.modules.operation.mapper.OperationCityMapper; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDate; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** 仅处理导游端账号,避免导游身份与游客或后台管理员混用。 */ @Service public class GuideUserService { public static final int STATUS_DISABLED = 0; public static final int STATUS_ENABLED = 1; public static final int STATUS_PENDING = 2; public static final int STATUS_CANCELLED = 3; private static final String DEFAULT_GUIDE_PASSWORD = "123456"; private final GuideUserMapper userMapper; private final GuideTeamMapper teamMapper; private final GuideScenicConfigMapper scenicConfigMapper; private final GuideAutoDisableConfigMapper autoDisableConfigMapper; private final OperationScenicSpotMapper scenicSpotMapper; private final OperationCityMapper cityMapper; private final PasswordEncoder passwordEncoder; public GuideUserService(GuideUserMapper userMapper, GuideTeamMapper teamMapper, GuideScenicConfigMapper scenicConfigMapper, GuideAutoDisableConfigMapper autoDisableConfigMapper, OperationScenicSpotMapper scenicSpotMapper, OperationCityMapper cityMapper, PasswordEncoder passwordEncoder) { this.userMapper = userMapper; this.teamMapper = teamMapper; this.scenicConfigMapper = scenicConfigMapper; this.autoDisableConfigMapper = autoDisableConfigMapper; this.scenicSpotMapper = scenicSpotMapper; this.cityMapper = cityMapper; this.passwordEncoder = passwordEncoder; } public GuideUser findEnabledById(Long id) { GuideUser user = userMapper.selectById(id); if (user == null || user.getStatus() == null || user.getStatus() != STATUS_ENABLED) { throw new BusinessException(401, "导游用户不存在或已停用"); } return user; } public GuideUser findWorkbenchAccessibleById(Long id) { GuideUser user = findById(id); if (user.getStatus() == null || user.getStatus() == STATUS_CANCELLED) { throw new BusinessException(401, "导游用户不存在或已注销"); } return user; } public GuideUser findByUsername(String username) { return userMapper.selectOne(new LambdaQueryWrapper().eq(GuideUser::getUsername, username).last("LIMIT 1")); } public GuideUser findByPhone(String phone) { return userMapper.selectOne(new LambdaQueryWrapper() .and(query -> query.eq(GuideUser::getUsername, phone).or().eq(GuideUser::getMobile, phone)) .last("LIMIT 1")); } public PageResponse page(long page, long size, String keyword, Integer status, LocalDate startDate, LocalDate endDate, String ratingLevel, String teamType, Boolean whitelisted) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String searchKeyword = keyword.trim(); query.and(wrapper -> wrapper.like(GuideUser::getUsername, searchKeyword) .or().like(GuideUser::getRealName, searchKeyword) .or().like(GuideUser::getMobile, searchKeyword) .or().like(GuideUser::getCertificateNo, searchKeyword)); } query.eq(status != null, GuideUser::getStatus, status) .eq(whitelisted != null, GuideUser::getAutoDisableWhitelisted, Boolean.TRUE.equals(whitelisted) ? 1 : 0) .ge(startDate != null, GuideUser::getCreatedAt, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, GuideUser::getCreatedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay()); if ("personal".equals(teamType)) query.isNull(GuideUser::getTeamId); if ("team".equals(teamType)) query.isNotNull(GuideUser::getTeamId); if ("high".equals(ratingLevel)) query.ge(GuideUser::getRating, 4); if ("medium".equals(ratingLevel)) query.ge(GuideUser::getRating, 3).lt(GuideUser::getRating, 4); if ("low".equals(ratingLevel)) query.lt(GuideUser::getRating, 3); query.orderByDesc(GuideUser::getId); Page result = userMapper.selectPage(new Page(page, size), query); Map teamNames = loadTeamNames(result.getRecords()); return new PageResponse(result.getRecords().stream() .map(user -> GuideUserResponse.from(user, teamNames.get(user.getTeamId()))) .collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } public GuideUserDetailResponse detail(Long id) { GuideUser user = findById(id); GuideTeam team = user.getTeamId() == null ? null : teamMapper.selectById(user.getTeamId()); List scenicConfigs = scenicConfigMapper.selectList( new LambdaQueryWrapper().eq(GuideScenicConfig::getGuideId, id).orderByAsc(GuideScenicConfig::getId)); return new GuideUserDetailResponse(user, team == null ? null : team.getTeamName(), scenicConfigs); } @Transactional public List addScenicConfig(Long guideId, Long scenicSpotId) { findById(guideId); OperationScenicSpot scenicSpot = scenicSpotMapper.selectById(scenicSpotId); if (scenicSpot == null || scenicSpot.getStatus() == null || scenicSpot.getStatus() != 1) { throw new BusinessException(404, "景区不存在或未启用"); } OperationCity city = scenicSpot.getCityId() == null ? null : cityMapper.selectById(scenicSpot.getCityId()); GuideScenicConfig config = new GuideScenicConfig(); config.setGuideId(guideId); config.setScenicName(scenicSpot.getScenicName()); config.setCityName(city == null ? null : city.getCityName()); scenicConfigMapper.upsert(config); return scenicConfigs(guideId); } @Transactional public List deleteScenicConfig(Long guideId, Long configId) { findById(guideId); GuideScenicConfig config = scenicConfigMapper.selectById(configId); if (config == null || !guideId.equals(config.getGuideId())) { throw new BusinessException(404, "导游景区配置不存在"); } scenicConfigMapper.deleteById(configId); return scenicConfigs(guideId); } private List scenicConfigs(Long guideId) { return scenicConfigMapper.selectList(new LambdaQueryWrapper() .eq(GuideScenicConfig::getGuideId, guideId) .orderByAsc(GuideScenicConfig::getId)) .stream().map(GuideScenicConfigResponse::new).collect(Collectors.toList()); } public GuideUser findById(Long id) { GuideUser user = userMapper.selectById(id); if (user == null) { throw new BusinessException(404, "导游用户不存在"); } return user; } private Map loadTeamNames(List users) { Set teamIds = users.stream().map(GuideUser::getTeamId).filter(id -> id != null).collect(Collectors.toSet()); if (teamIds.isEmpty()) return Collections.emptyMap(); Map names = new HashMap(); for (GuideTeam team : teamMapper.selectBatchIds(teamIds)) names.put(team.getId(), team.getTeamName()); return names; } @Transactional public GuideUserResponse create(GuideUserCreateRequest request) { if (userMapper.selectCount(new LambdaQueryWrapper().eq(GuideUser::getUsername, request.getUsername())) > 0) { throw new BusinessException(409, "导游用户名已存在"); } GuideUser user = new GuideUser(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setRealName(request.getRealName()); user.setMobile(request.getMobile()); user.setCertificateNo(request.getCertificateNo()); user.setStatus(1); user.setDeleted(0); userMapper.insert(user); return GuideUserResponse.from(user); } /** 审核通过后创建正式导游账号,申请手机号作为登录用户名。 */ @Transactional public GuideUserResponse createFromApprovedApplication(GuideApplication application) { if (!StringUtils.hasText(application.getMobile())) { throw new BusinessException(400, "申请手机号不能为空"); } String username = application.getMobile().trim(); GuideUser existingUser = findByPhone(username); if (existingUser != null && (existingUser.getStatus() == null || existingUser.getStatus() != 2)) { throw new BusinessException(409, "该手机号已存在导游账号"); } if (StringUtils.hasText(application.getCertificateNo()) && userMapper.selectCount(new LambdaQueryWrapper() .eq(GuideUser::getCertificateNo, application.getCertificateNo()) .ne(existingUser != null, GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) { throw new BusinessException(409, "该导游证号已存在导游账号"); } if (existingUser != null) { copyApplicationProfile(existingUser, application); existingUser.setStatus(1); userMapper.updateById(existingUser); return GuideUserResponse.from(existingUser); } GuideUser user = new GuideUser(); user.setUsername(username); user.setPassword(passwordEncoder.encode(DEFAULT_GUIDE_PASSWORD)); copyApplicationProfile(user, application); user.setStatus(1); user.setDeleted(0); userMapper.insert(user); return GuideUserResponse.from(user); } @Transactional public GuideUserResponse createPendingFromApplication(GuideApplication application) { GuideUser user = findByPhone(application.getMobile()); if (user == null) { user = new GuideUser(); user.setUsername(application.getMobile().trim()); user.setPassword(passwordEncoder.encode(DEFAULT_GUIDE_PASSWORD)); user.setStatus(2); user.setDeleted(0); } else if (user.getStatus() == null || user.getStatus() != 2) { throw new BusinessException(409, "该手机号已存在导游账号"); } copyApplicationProfile(user, application); if (user.getId() == null) userMapper.insert(user); else userMapper.updateById(user); return GuideUserResponse.from(user); } private void copyApplicationProfile(GuideUser user, GuideApplication application) { user.setRealName(application.getRealName()); user.setMobile(application.getMobile()); user.setGender(application.getGender()); user.setCertificateNo(application.getCertificateNo()); user.setIdCardNo(application.getIdCardNo()); user.setIdCardFrontUrl(application.getIdCardFrontUrl()); user.setIdCardBackUrl(application.getIdCardBackUrl()); user.setGuideCertificateUrl(application.getCertificateUrl()); user.setQualificationCertificateUrl(application.getQualificationCertificateUrl()); user.setTeamId(application.getTeamId()); } @Transactional public GuideUserResponse updateProfile(Long id, GuideProfileUpdateRequest request) { GuideUser user = findEnabledById(id); user.setRealName(request.getRealName()); user.setAvatar(request.getAvatar()); user.setCertificateNo(request.getCertificateNo()); userMapper.updateById(user); return GuideUserResponse.from(user); } @Transactional public GuideUserResponse updateAvatar(Long id, String avatar) { GuideUser user = findEnabledById(id); user.setAvatar(avatar); userMapper.updateById(user); return GuideUserResponse.from(user); } public void ensurePhoneAvailable(Long currentGuideId, String phone) { GuideUser existing = findByPhone(phone); if (existing != null && !existing.getId().equals(currentGuideId)) { throw new BusinessException(409, "该手机号已绑定其他导游账号"); } } @Transactional public GuideUserResponse changeMobile(Long id, String newPhone) { GuideUser user = findEnabledById(id); ensurePhoneAvailable(id, newPhone); String oldPhone = user.getMobile(); user.setMobile(newPhone); if (oldPhone != null && oldPhone.equals(user.getUsername())) { user.setUsername(newPhone); } userMapper.updateById(user); return GuideUserResponse.from(user); } @Transactional public GuideUserDetailResponse updateDetail(Long id, GuideUserDetailUpdateRequest request) { GuideUser user = findById(id); user.setRealName(request.getRealName()); user.setMobile(request.getMobile()); user.setAvatar(request.getAvatar()); user.setCertificateNo(request.getCertificateNo()); user.setGender(request.getGender()); user.setBirthDate(request.getBirthDate()); user.setWorkYears(request.getWorkYears()); user.setIdCardNo(request.getIdCardNo()); user.setIdCardFrontUrl(request.getIdCardFrontUrl()); user.setIdCardBackUrl(request.getIdCardBackUrl()); user.setGuideCertificateUrl(request.getGuideCertificateUrl()); user.setQualificationCertificateUrl(request.getQualificationCertificateUrl()); user.setTeamId(request.getTeamId()); user.setWechatOpenId(request.getWechatOpenId()); userMapper.updateById(user); return detail(id); } @Transactional public void updateWhitelist(Long id, boolean enabled) { GuideUser user = findById(id); user.setAutoDisableWhitelisted(enabled ? 1 : 0); userMapper.updateById(user); } public GuideAutoDisableConfigResponse getAutoDisableConfig() { GuideAutoDisableConfig config = autoDisableConfigMapper.selectById(1); if (config == null) { config = new GuideAutoDisableConfig(); config.setId(1); config.setOrderThreshold(1); config.setRatingThreshold(new java.math.BigDecimal(3.00)); autoDisableConfigMapper.insert(config); } return new GuideAutoDisableConfigResponse(config); } @Transactional public GuideAutoDisableConfigResponse updateAutoDisableConfig(GuideAutoDisableConfigRequest request) { GuideAutoDisableConfig config = autoDisableConfigMapper.selectById(1); if (config == null) { config = new GuideAutoDisableConfig(); config.setId(1); config.setOrderThreshold(request.getOrderThreshold()); config.setRatingThreshold(request.getRatingThreshold()); autoDisableConfigMapper.insert(config); } else { config.setOrderThreshold(request.getOrderThreshold()); config.setRatingThreshold(request.getRatingThreshold()); autoDisableConfigMapper.updateById(config); } return new GuideAutoDisableConfigResponse(autoDisableConfigMapper.selectById(1)); } @Transactional public void updateStatus(Long id, Integer status) { GuideUser user = userMapper.selectById(id); if (user == null) { throw new BusinessException(404, "导游用户不存在"); } if (user.getStatus() != null && user.getStatus() == STATUS_CANCELLED) { throw new BusinessException(409, "已注销导游账号不可重新启用"); } user.setStatus(status); userMapper.updateById(user); } }