From 39f6fca28e3f78abe48f08e474ec011abb990b36 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期三, 09 九月 2026 10:39:21 +0800
Subject: [PATCH] update

---
 backend/src/main/java/com/tourguide/modules/user/service/GuideUserService.java |  203 ++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 192 insertions(+), 11 deletions(-)

diff --git a/backend/src/main/java/com/tourguide/modules/user/service/GuideUserService.java b/backend/src/main/java/com/tourguide/modules/user/service/GuideUserService.java
index 3400b74..16695af 100644
--- a/backend/src/main/java/com/tourguide/modules/user/service/GuideUserService.java
+++ b/backend/src/main/java/com/tourguide/modules/user/service/GuideUserService.java
@@ -16,25 +16,33 @@
 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.GuideTeamScenicConfig;
 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.GuideTeamScenicConfigMapper;
 import com.tourguide.modules.user.mapper.GuideUserMapper;
+import com.tourguide.modules.user.mapper.GuideApplicationMapper;
 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.beans.factory.annotation.Autowired;
 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.DateTimeException;
 import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /** 仅处理导游端账号,避免导游身份与游客或后台管理员混用。 */
@@ -45,8 +53,11 @@
     public static final int STATUS_PENDING = 2;
     public static final int STATUS_CANCELLED = 3;
     private static final String DEFAULT_GUIDE_PASSWORD = "123456";
+    private static final Pattern WORK_YEAR_START_PATTERN = Pattern.compile("^(\\d+)");
     private final GuideUserMapper userMapper;
     private final GuideTeamMapper teamMapper;
+    private final GuideApplicationMapper applicationMapper;
+    private final GuideTeamScenicConfigMapper teamScenicConfigMapper;
     private final GuideScenicConfigMapper scenicConfigMapper;
     private final GuideAutoDisableConfigMapper autoDisableConfigMapper;
     private final OperationScenicSpotMapper scenicSpotMapper;
@@ -59,8 +70,34 @@
                             OperationScenicSpotMapper scenicSpotMapper,
                             OperationCityMapper cityMapper,
                             PasswordEncoder passwordEncoder) {
+        this(userMapper, teamMapper, null, null, scenicConfigMapper, autoDisableConfigMapper,
+                scenicSpotMapper, cityMapper, passwordEncoder);
+    }
+
+    public GuideUserService(GuideUserMapper userMapper, GuideTeamMapper teamMapper,
+                            GuideApplicationMapper applicationMapper,
+                            GuideScenicConfigMapper scenicConfigMapper,
+                            GuideAutoDisableConfigMapper autoDisableConfigMapper,
+                            OperationScenicSpotMapper scenicSpotMapper,
+                            OperationCityMapper cityMapper,
+                            PasswordEncoder passwordEncoder) {
+        this(userMapper, teamMapper, applicationMapper, null, scenicConfigMapper, autoDisableConfigMapper,
+                scenicSpotMapper, cityMapper, passwordEncoder);
+    }
+
+    @Autowired
+    public GuideUserService(GuideUserMapper userMapper, GuideTeamMapper teamMapper,
+                            GuideApplicationMapper applicationMapper,
+                            GuideTeamScenicConfigMapper teamScenicConfigMapper,
+                            GuideScenicConfigMapper scenicConfigMapper,
+                            GuideAutoDisableConfigMapper autoDisableConfigMapper,
+                            OperationScenicSpotMapper scenicSpotMapper,
+                            OperationCityMapper cityMapper,
+                            PasswordEncoder passwordEncoder) {
         this.userMapper = userMapper;
         this.teamMapper = teamMapper;
+        this.applicationMapper = applicationMapper;
+        this.teamScenicConfigMapper = teamScenicConfigMapper;
         this.scenicConfigMapper = scenicConfigMapper;
         this.autoDisableConfigMapper = autoDisableConfigMapper;
         this.scenicSpotMapper = scenicSpotMapper;
@@ -106,6 +143,7 @@
                     .or().like(GuideUser::getCertificateNo, searchKeyword));
         }
         query.eq(status != null, GuideUser::getStatus, status)
+                .ne(GuideUser::getStatus, STATUS_PENDING)
                 .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());
@@ -128,23 +166,61 @@
         GuideTeam team = user.getTeamId() == null ? null : teamMapper.selectById(user.getTeamId());
         List<GuideScenicConfig> scenicConfigs = scenicConfigMapper.selectList(
                 new LambdaQueryWrapper<GuideScenicConfig>().eq(GuideScenicConfig::getGuideId, id).orderByAsc(GuideScenicConfig::getId));
-        return new GuideUserDetailResponse(user, team == null ? null : team.getTeamName(), scenicConfigs);
+        LocalDate birthDate = user.getBirthDate();
+        Integer workYears = user.getWorkYears();
+        if (birthDate == null || workYears == null) {
+            GuideApplication application = findLatestApplication(user);
+            if (birthDate == null && application != null) birthDate = parseBirthDate(application.getIdCardNo());
+            if (workYears == null && application != null) workYears = parseWorkYears(application.getWorkYearRange());
+        }
+        return new GuideUserDetailResponse(user, team == null ? null : team.getTeamName(), scenicConfigs,
+                birthDate, workYears);
+    }
+
+    private GuideApplication findLatestApplication(GuideUser user) {
+        if (applicationMapper == null) return null;
+        if (StringUtils.hasText(user.getIdCardNo())) {
+            GuideApplication application = applicationMapper.selectOne(new LambdaQueryWrapper<GuideApplication>()
+                    .eq(GuideApplication::getIdCardNo, user.getIdCardNo().trim())
+                    .in(GuideApplication::getAuditStatus, 0, 1)
+                    .orderByDesc(GuideApplication::getId)
+                    .last("LIMIT 1"));
+            if (application != null) return application;
+        }
+        if (!StringUtils.hasText(user.getMobile())) return null;
+        return applicationMapper.selectOne(new LambdaQueryWrapper<GuideApplication>()
+                .eq(GuideApplication::getMobile, user.getMobile().trim())
+                .in(GuideApplication::getAuditStatus, 0, 1)
+                .orderByDesc(GuideApplication::getId)
+                .last("LIMIT 1"));
     }
 
     @Transactional
     public List<GuideScenicConfigResponse> addScenicConfig(Long guideId, Long scenicSpotId) {
-        findById(guideId);
+        GuideUser guide = 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());
+        ensureScenicBelongsToGuideTeam(guide, scenicSpot);
         GuideScenicConfig config = new GuideScenicConfig();
         config.setGuideId(guideId);
         config.setScenicName(scenicSpot.getScenicName());
         config.setCityName(city == null ? null : city.getCityName());
         scenicConfigMapper.upsert(config);
         return scenicConfigs(guideId);
+    }
+
+    private void ensureScenicBelongsToGuideTeam(GuideUser guide, OperationScenicSpot scenicSpot) {
+        if (guide.getTeamId() == null) return;
+        long configured = teamScenicConfigMapper.selectCount(
+                new LambdaQueryWrapper<GuideTeamScenicConfig>()
+                        .eq(GuideTeamScenicConfig::getTeamId, guide.getTeamId())
+                        .eq(GuideTeamScenicConfig::getScenicName, scenicSpot.getScenicName()));
+        if (configured == 0) {
+            throw new BusinessException(400, "只能配置所属讲解队下的景区");
+        }
     }
 
     @Transactional
@@ -207,12 +283,7 @@
         if (existingUser != null && (existingUser.getStatus() == null || existingUser.getStatus() != 2)) {
             throw new BusinessException(409, "该手机号已存在导游账号");
         }
-        if (StringUtils.hasText(application.getCertificateNo())
-                && userMapper.selectCount(new LambdaQueryWrapper<GuideUser>()
-                .eq(GuideUser::getCertificateNo, application.getCertificateNo())
-                .ne(existingUser != null, GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) {
-            throw new BusinessException(409, "该导游证号已存在导游账号");
-        }
+        ensureUniqueGuideIdentity(application, existingUser);
         if (existingUser != null) {
             copyApplicationProfile(existingUser, application);
             existingUser.setStatus(1);
@@ -238,18 +309,45 @@
             user.setPassword(passwordEncoder.encode(DEFAULT_GUIDE_PASSWORD));
             user.setStatus(2);
             user.setDeleted(0);
-        } else if (user.getStatus() == null || user.getStatus() != 2) {
+        } else if (user.getStatus() == null
+                || (user.getStatus() != STATUS_PENDING && user.getStatus() != STATUS_CANCELLED)) {
             throw new BusinessException(409, "该手机号已存在导游账号");
+        }
+        ensureUniqueGuideIdentity(application, user);
+        if (user.getStatus() == STATUS_CANCELLED) {
+            user.setStatus(STATUS_PENDING);
+            user.setAutoDisableWhitelisted(0);
+            user.setAutoDisabled(0);
+            user.setDeleted(0);
         }
         copyApplicationProfile(user, application);
         if (user.getId() == null) userMapper.insert(user); else userMapper.updateById(user);
         return GuideUserResponse.from(user);
     }
 
+    private void ensureUniqueGuideIdentity(GuideApplication application, GuideUser existingUser) {
+        if (StringUtils.hasText(application.getCertificateNo())
+                && userMapper.selectCount(new LambdaQueryWrapper<GuideUser>()
+                .eq(GuideUser::getCertificateNo, application.getCertificateNo())
+                .ne(existingUser != null && existingUser.getId() != null,
+                        GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) {
+            throw new BusinessException(409, "该导游证号已存在导游账号");
+        }
+        if (StringUtils.hasText(application.getIdCardNo())
+                && userMapper.selectCount(new LambdaQueryWrapper<GuideUser>()
+                .eq(GuideUser::getIdCardNo, application.getIdCardNo())
+                .ne(existingUser != null && existingUser.getId() != null,
+                        GuideUser::getId, existingUser == null ? null : existingUser.getId())) > 0) {
+            throw new BusinessException(409, "该身份证号已存在导游账号");
+        }
+    }
+
     private void copyApplicationProfile(GuideUser user, GuideApplication application) {
         user.setRealName(application.getRealName());
         user.setMobile(application.getMobile());
         user.setGender(application.getGender());
+        user.setBirthDate(parseBirthDate(application.getIdCardNo()));
+        user.setWorkYears(parseWorkYears(application.getWorkYearRange()));
         user.setCertificateNo(application.getCertificateNo());
         user.setIdCardNo(application.getIdCardNo());
         user.setIdCardFrontUrl(application.getIdCardFrontUrl());
@@ -257,6 +355,31 @@
         user.setGuideCertificateUrl(application.getCertificateUrl());
         user.setQualificationCertificateUrl(application.getQualificationCertificateUrl());
         user.setTeamId(application.getTeamId());
+    }
+
+    private LocalDate parseBirthDate(String idCardNo) {
+        if (!StringUtils.hasText(idCardNo)) return null;
+        String normalized = idCardNo.trim().toUpperCase();
+        if (normalized.length() < 14) return null;
+        try {
+            return LocalDate.parse(normalized.substring(6, 14), DateTimeFormatter.BASIC_ISO_DATE);
+        } catch (DateTimeException exception) {
+            return null;
+        }
+    }
+
+    private Integer parseWorkYears(String workYearRange) {
+        if (!StringUtils.hasText(workYearRange)) return null;
+        String normalized = workYearRange.trim();
+        Matcher matcher = WORK_YEAR_START_PATTERN.matcher(normalized);
+        if (!matcher.find()) return null;
+        try {
+            int startYears = Integer.parseInt(matcher.group(1));
+            if (startYears > 100) return null;
+            return startYears == 1 && normalized.indexOf('-') < 0 ? 0 : startYears;
+        } catch (NumberFormatException exception) {
+            return null;
+        }
     }
 
     @Transactional
@@ -275,6 +398,39 @@
         user.setAvatar(avatar);
         userMapper.updateById(user);
         return GuideUserResponse.from(user);
+    }
+
+    @Transactional
+    public GuideUserResponse updateAvatarByAdmin(Long id, String avatar) {
+        GuideUser user = findById(id);
+        user.setAvatar(avatar);
+        userMapper.updateById(user);
+        return GuideUserResponse.from(user);
+    }
+
+    public boolean supportsDocumentType(String documentType) {
+        return "idCardFrontUrl".equals(documentType)
+                || "idCardBackUrl".equals(documentType)
+                || "guideCertificateUrl".equals(documentType)
+                || "qualificationCertificateUrl".equals(documentType);
+    }
+
+    @Transactional
+    public String updateDocumentByAdmin(Long id, String documentType, String documentUrl) {
+        GuideUser user = findById(id);
+        if ("idCardFrontUrl".equals(documentType)) {
+            user.setIdCardFrontUrl(documentUrl);
+        } else if ("idCardBackUrl".equals(documentType)) {
+            user.setIdCardBackUrl(documentUrl);
+        } else if ("guideCertificateUrl".equals(documentType)) {
+            user.setGuideCertificateUrl(documentUrl);
+        } else if ("qualificationCertificateUrl".equals(documentType)) {
+            user.setQualificationCertificateUrl(documentUrl);
+        } else {
+            throw new BusinessException(400, "不支持的证件类型");
+        }
+        userMapper.updateById(user);
+        return documentUrl;
     }
 
     public void ensurePhoneAvailable(Long currentGuideId, String phone) {
@@ -299,7 +455,13 @@
 
     @Transactional
     public GuideUserDetailResponse updateDetail(Long id, GuideUserDetailUpdateRequest request) {
-        GuideUser user = findById(id);
+        GuideUser user = userMapper.selectForUpdate(id);
+        if (user == null) throw new BusinessException(404, "导游用户不存在");
+        if (!java.util.Objects.equals(user.getTeamId(), request.getTeamId())
+                && teamMapper.selectCount(new LambdaQueryWrapper<GuideTeam>()
+                .eq(GuideTeam::getCollectionGuideId, id)) > 0) {
+            throw new BusinessException(409, "该导游已配置为讲解队收款导游,请先更换收款导游");
+        }
         user.setRealName(request.getRealName());
         user.setMobile(request.getMobile());
         user.setAvatar(request.getAvatar());
@@ -323,6 +485,14 @@
     public void updateWhitelist(Long id, boolean enabled) {
         GuideUser user = findById(id);
         user.setAutoDisableWhitelisted(enabled ? 1 : 0);
+        // 将因自动禁用规则停用的导游加入白名单后,立即恢复账号状态。
+        // 仅处理 autoDisabled 标记,避免误启用管理员手动停用的导游。
+        if (enabled
+                && Integer.valueOf(STATUS_DISABLED).equals(user.getStatus())
+                && Integer.valueOf(1).equals(user.getAutoDisabled())) {
+            user.setStatus(STATUS_ENABLED);
+            user.setAutoDisabled(0);
+        }
         userMapper.updateById(user);
     }
 
@@ -357,12 +527,23 @@
 
     @Transactional
     public void updateStatus(Long id, Integer status) {
-        GuideUser user = userMapper.selectById(id);
+        GuideUser user = userMapper.selectForUpdate(id);
         if (user == null) { throw new BusinessException(404, "导游用户不存在"); }
+        if (!Integer.valueOf(STATUS_ENABLED).equals(status)
+                && teamMapper.selectCount(new LambdaQueryWrapper<GuideTeam>()
+                .eq(GuideTeam::getCollectionGuideId, id)) > 0) {
+            throw new BusinessException(409, "该导游已配置为讲解队收款导游,请先更换收款导游");
+        }
         if (user.getStatus() != null && user.getStatus() == STATUS_CANCELLED) {
             throw new BusinessException(409, "已注销导游账号不可重新启用");
         }
+        if (Integer.valueOf(STATUS_ENABLED).equals(status)
+                && Integer.valueOf(1).equals(user.getAutoDisabled())
+                && !Integer.valueOf(1).equals(user.getAutoDisableWhitelisted())) {
+            throw new BusinessException(409, "该导游由自动禁用规则停用,请先在自动禁用设置中添加白名单后再启用");
+        }
         user.setStatus(status);
+        user.setAutoDisabled(0);
         userMapper.updateById(user);
     }
 }

--
Gitblit v1.7.1