liujie
2026-08-21 ad5db85e540947e82c7a4d5612879b3b3ecdf459
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package com.tourguide.modules.user.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tourguide.common.exception.BusinessException;
import com.tourguide.common.validation.ChineseIdCardValidator;
import com.tourguide.modules.operation.entity.OperationCity;
import com.tourguide.modules.operation.mapper.OperationCityMapper;
import com.tourguide.modules.user.dto.GuideRegistrationResponse;
import com.tourguide.modules.user.dto.GuideRegistrationSubmitRequest;
import com.tourguide.modules.user.dto.GuideRegistrationTokenResponse;
import com.tourguide.modules.user.dto.SmsCodeResponse;
import com.tourguide.modules.user.entity.GuideApplication;
import com.tourguide.modules.user.entity.GuideTeam;
import com.tourguide.modules.user.mapper.GuideApplicationMapper;
import com.tourguide.modules.user.mapper.GuideTeamMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
 
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
 
@Service
public class GuideRegistrationService {
    private static final int PENDING = 0;
    private static final int APPROVED = 1;
    private static final Duration TOKEN_TTL = Duration.ofMinutes(30);
    private static final String TOKEN_KEY_PREFIX = "tourguide:guide-registration:token:";
    private static final DateTimeFormatter APPLICATION_NO_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
 
    private final GuideVerificationCodeService verificationCodeService;
    private final StringRedisTemplate redisTemplate;
    private final GuideApplicationMapper applicationMapper;
    private final GuideUserService guideUserService;
    private final OperationCityMapper cityMapper;
    private final GuideTeamMapper teamMapper;
 
    public GuideRegistrationService(GuideVerificationCodeService verificationCodeService,
                                    StringRedisTemplate redisTemplate,
                                    GuideApplicationMapper applicationMapper,
                                    GuideUserService guideUserService,
                                    OperationCityMapper cityMapper,
                                    GuideTeamMapper teamMapper) {
        this.verificationCodeService = verificationCodeService;
        this.redisTemplate = redisTemplate;
        this.applicationMapper = applicationMapper;
        this.guideUserService = guideUserService;
        this.cityMapper = cityMapper;
        this.teamMapper = teamMapper;
    }
 
    public GuideRegistrationTokenResponse verifyPhone(String phone, String code) {
        ensurePhoneCanRegister(phone);
        verificationCodeService.verifyAndConsume(phone, code);
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(tokenKey(token), phone, TOKEN_TTL);
        return new GuideRegistrationTokenResponse(token, TOKEN_TTL.getSeconds());
    }
 
    public SmsCodeResponse sendCode(String phone) {
        ensurePhoneCanRegister(phone);
        return verificationCodeService.send(phone);
    }
 
    public String requireVerifiedPhone(String registrationToken) {
        if (!StringUtils.hasText(registrationToken)) {
            throw new BusinessException(401, "注册验证已失效,请重新验证手机号");
        }
        String phone = redisTemplate.opsForValue().get(tokenKey(registrationToken));
        if (!StringUtils.hasText(phone)) {
            throw new BusinessException(401, "注册验证已失效,请重新验证手机号");
        }
        return phone;
    }
 
    @Transactional
    public GuideRegistrationResponse submit(String registrationToken, GuideRegistrationSubmitRequest request) {
        String phone = requireVerifiedPhone(registrationToken);
        ensurePhoneCanRegister(phone);
        String idCardNo = request.getIdCardNo().trim().toUpperCase();
        if (!ChineseIdCardValidator.isValid(idCardNo)) {
            throw new BusinessException(400, "身份证号格式不正确");
        }
        if (applicationMapper.selectCount(new LambdaQueryWrapper<GuideApplication>()
                .eq(GuideApplication::getIdCardNo, idCardNo)
                .in(GuideApplication::getAuditStatus, PENDING, APPROVED)) > 0) {
            throw new BusinessException(409, "该身份证号已提交过导游申请");
        }
        OperationCity city = findEnabledBankCity(request.getBankCityId());
        GuideTeam team = findEnabledGuideTeam(request.getGuideType(), request.getTeamId());
 
        GuideApplication application = new GuideApplication();
        application.setApplicationNo(buildApplicationNo());
        application.setRealName(request.getRealName().trim());
        application.setMobile(phone);
        application.setGender(request.getGender());
        application.setAgeRange(normalizeOptional(request.getAgeRange()));
        application.setWorkYearRange(normalizeOptional(request.getWorkYearRange()));
        application.setGuideType(request.getGuideType());
        application.setTeamId(team == null ? null : team.getId());
        application.setIdCardNo(idCardNo);
        application.setIdCardFrontUrl(request.getIdCardFrontUrl());
        application.setIdCardBackUrl(request.getIdCardBackUrl());
        application.setCertificateUrl(request.getGuideCertificateUrl());
        application.setQualificationCertificateUrl(normalizeOptional(request.getQualificationCertificateUrl()));
        application.setCertificateValidUntil(request.getCertificateValidUntil());
        application.setBankCardNo(normalizeOptional(request.getBankCardNo()));
        if (city != null) {
            application.setBankCityId(city.getId());
            application.setBankCityName(city.getCityName());
        }
        application.setAuditStatus(PENDING);
        application.setDeleted(0);
        applicationMapper.insert(application);
        guideUserService.createPendingFromApplication(application);
        redisTemplate.delete(tokenKey(registrationToken));
        return new GuideRegistrationResponse(application);
    }
 
    private void ensurePhoneCanRegister(String phone) {
        com.tourguide.modules.user.entity.GuideUser guide = guideUserService.findByPhone(phone);
        if (guide != null && (guide.getStatus() == null || guide.getStatus() != 2)) {
            throw new BusinessException(409, "该手机号已注册为导游");
        }
        if (applicationMapper.selectCount(new LambdaQueryWrapper<GuideApplication>()
                .eq(GuideApplication::getMobile, phone)
                .eq(GuideApplication::getAuditStatus, PENDING)) > 0) {
            throw new BusinessException(409, "该手机号已有待审核申请");
        }
    }
 
    private String buildApplicationNo() {
        return "GA" + LocalDateTime.now().format(APPLICATION_NO_TIME)
                + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
    }
 
    private String normalizeOptional(String value) {
        return StringUtils.hasText(value) ? value.trim() : null;
    }
 
    private OperationCity findEnabledBankCity(Long bankCityId) {
        if (bankCityId == null) {
            return null;
        }
        OperationCity city = cityMapper.selectById(bankCityId);
        if (city == null || city.getStatus() == null || city.getStatus() != 1) {
            throw new BusinessException(400, "开户城市未开放或已停用");
        }
        return city;
    }
 
    private GuideTeam findEnabledGuideTeam(String guideType, Long teamId) {
        if ("personal".equals(guideType)) {
            return null;
        }
        if (teamId == null) {
            throw new BusinessException(400, "请选择讲解队");
        }
        GuideTeam team = teamMapper.selectById(teamId);
        if (team == null || team.getStatus() == null || team.getStatus() != 1) {
            throw new BusinessException(400, "讲解队不存在或已停用");
        }
        return team;
    }
 
    private String tokenKey(String token) { return TOKEN_KEY_PREFIX + token; }
}