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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package com.ruoyi.auth.service;
 
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.auth.enums.SmsSceneEnum;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.system.api.RemoteAccountUserService;
import com.ruoyi.system.api.domain.AccountMember;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.AccountRegisterRequest;
import com.ruoyi.system.api.model.LoginUser;
 
/**
 * C端(用户端小程序)登录与注册编排服务
 *
 * @author hugray
 */
@Service
public class AccountLoginService
{
    private static final Logger log = LoggerFactory.getLogger(AccountLoginService.class);
 
    /** 手机号正则 */
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
 
    /** 注册分布式锁持有时长(秒) */
    private static final long REGISTER_LOCK_SECONDS = 10L;
 
    @Autowired
    private RedisService redisService;
 
    @Autowired
    private SmsCodeService smsCodeService;
 
    @Autowired
    private WeChatMiniappService weChatMiniappService;
 
    @Autowired
    private RemoteAccountUserService remoteAccountUserService;
 
    @Autowired
    private TokenService tokenService;
 
    /**
     * 手机号一键登录
     *
     * @param code 微信 getPhoneNumber 回调返回的动态令牌
     * @return 令牌信息(access_token、expires_in)
     */
    public Map<String, Object> phoneLogin(String code)
    {
        // STEP 1: 授权凭证非空校验
        if (StringUtils.isEmpty(code))
        {
            throw new ServiceException("微信授权凭证不能为空");
        }
 
        // STEP 2-3: 微信凭证换取手机号(内部含 access_token 获取与异常兜底)
        String phone = weChatMiniappService.getPhoneNumber(code);
 
        // STEP 4: 查询会员并校验状态
        AccountMember member = getRegisteredMember(phone);
        checkMemberStatus(member);
 
        // STEP 5: 组装登录态并签发令牌
        return issueToken(member, "phone code-login");
    }
 
    /**
     * 手机号验证码登录(短信验证码一次性消费)
     *
     * @param phone 手机号
     * @param code 短信验证码
     * @return 令牌信息(access_token、expires_in)
     */
    public Map<String, Object> phoneCodeLogin(String phone, String code)
    {
        // STEP 1: 手机号格式校验(Controller 已校验,此处兜底)
        if (StringUtils.isEmpty(phone) || !PHONE_PATTERN.matcher(phone).matches())
        {
            throw new ServiceException("请输入正确手机号");
        }
 
        // STEP 2: 先查询会员与状态,避免无谓消费验证码
        AccountMember member = getRegisteredMember(phone);
        checkMemberStatus(member);
 
        // STEP 3: 校验并消费短信验证码(一次性)
        smsCodeService.verifyAndConsume(phone, SmsSceneEnum.LOGIN.getCode(), code);
 
        // STEP 4: 组装登录态并签发令牌
        return issueToken(member, "phone sms-login");
    }
 
    /**
     * C端会员注册
     *
     * @param phone 手机号
     * @param inputCode 短信验证码
     * @param referralCode 推荐码
     */
    public void accountRegister(String phone, String inputCode, String referralCode)
    {
        // STEP 1: 手机号与推荐码格式校验(Controller 已校验,此处兜底)
        if (StringUtils.isEmpty(phone) || !PHONE_PATTERN.matcher(phone).matches())
        {
            throw new ServiceException("请输入正确手机号");
        }
        if (StringUtils.isEmpty(referralCode))
        {
            throw new ServiceException("请输入推荐人码");
        }
 
        // STEP 2: 校验并消费短信验证码(一次性)
        smsCodeService.verifyAndConsume(phone, SmsSceneEnum.REGISTER.getCode(), inputCode);
 
        // STEP 3: 获取注册分布式锁,防并发与重复提交
        String lockKey = CacheConstants.ACCOUNT_REGISTER_LOCK_KEY + phone;
        boolean locked = redisService.setCacheObjectIfAbsent(lockKey, "1", REGISTER_LOCK_SECONDS, TimeUnit.SECONDS);
        if (!locked)
        {
            throw new ServiceException("操作过于频繁,请稍后重试");
        }
 
        try
        {
            // STEP 4: 锁内二次校验手机号未被注册
            if (StringUtils.isNotNull(getMemberOrNull(phone)))
            {
                throw new ServiceException("手机号已注册,请前往登录");
            }
 
            // STEP 5: 调用 ruoyi-user 落库注册
            AccountRegisterRequest request = new AccountRegisterRequest();
            request.setPhone(phone);
            request.setReferralCode(referralCode);
            R<AccountMember> registerResult = remoteAccountUserService.registerMember(request, SecurityConstants.INNER);
            if (R.FAIL == registerResult.getCode())
            {
                throw new ServiceException(registerResult.getMsg());
            }
            log.info("[ACCOUNT_REGISTER] register success, phone={}", maskPhone(phone));
        }
        finally
        {
            // STEP 6: 释放锁
            redisService.deleteObject(lockKey);
        }
    }
 
    /**
     * 按手机号查询会员,查询失败抛出异常,不存在返回 null
     *
     * @param phone 手机号
     * @return 会员信息或 null
     */
    private AccountMember getMemberOrNull(String phone)
    {
        R<AccountMember> memberResult = remoteAccountUserService.getMemberByPhone(phone, SecurityConstants.INNER);
        if (R.FAIL == memberResult.getCode())
        {
            throw new ServiceException(memberResult.getMsg());
        }
        return memberResult.getData();
    }
 
    /**
     * 按手机号查询会员,不存在时抛出「未注册」业务异常
     *
     * @param phone 手机号
     * @return 会员信息
     */
    private AccountMember getRegisteredMember(String phone)
    {
        AccountMember member = getMemberOrNull(phone);
        if (StringUtils.isNull(member))
        {
            throw new ServiceException("该手机号未注册,请先注册");
        }
        return member;
    }
 
    /**
     * 校验会员状态,停用账号拒绝登录
     *
     * @param member C端会员信息
     */
    private void checkMemberStatus(AccountMember member)
    {
        if (UserStatus.DISABLE.getCode().equals(member.getStatus()))
        {
            throw new ServiceException("账号已停用,请联系管理员");
        }
    }
 
    /**
     * 组装登录态并签发令牌
     *
     * @param member C端会员信息
     * @param scene 登录场景(仅用于日志标记)
     * @return 令牌信息(access_token、expires_in)
     */
    private Map<String, Object> issueToken(AccountMember member, String scene)
    {
        LoginUser loginUser = buildLoginUser(member);
        log.info("[ACCOUNT_LOGIN] {} success, userId={}", scene, member.getUserId());
        return tokenService.createToken(loginUser);
    }
 
    /**
     * 组装登录用户对象(以 SysUser 作为令牌载体,兼容 TokenService)
     *
     * @param member C端会员信息
     * @return 登录用户对象
     */
    private LoginUser buildLoginUser(AccountMember member)
    {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(member.getUserId());
        // TokenService 依赖 userName 生成令牌,缺失时回退为手机号
        sysUser.setUserName(StringUtils.isNotEmpty(member.getUserName()) ? member.getUserName() : member.getPhone());
        sysUser.setNickName(member.getNickname());
        sysUser.setPhonenumber(member.getPhone());
        sysUser.setAvatar(member.getAvatar());
        sysUser.setStatus(member.getStatus());
 
        LoginUser loginUser = new LoginUser();
        loginUser.setSysUser(sysUser);
        return loginUser;
    }
 
    /**
     * 手机号脱敏,形如 138****0000
     */
    private String maskPhone(String phone)
    {
        if (StringUtils.isEmpty(phone) || phone.length() < 7)
        {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }
}