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.system.api.RemoteAccountUserService;
|
import com.ruoyi.system.api.domain.AccountMember;
|
import com.ruoyi.system.api.model.AccountRegisterRequest;
|
|
/**
|
* 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 AccountTokenIssuer accountTokenIssuer;
|
|
/**
|
* 手机号一键登录
|
*
|
* @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)
|
{
|
return accountTokenIssuer.issue(member, scene);
|
}
|
|
/**
|
* 手机号脱敏,形如 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);
|
}
|
}
|