feat: complete aihr mvp mobile workflow

This commit is contained in:
2026-07-03 16:43:37 +08:00
parent 0126b0c661
commit f869d66c90
46 changed files with 5879 additions and 219 deletions
@@ -7,13 +7,17 @@ import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.RegexConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody;
@@ -67,6 +71,9 @@ import java.util.concurrent.TimeUnit;
@RequestMapping("/auth")
public class AuthController {
private static final String MOBILE_CLIENT_ID = "428a8310cd442757ae699df5d894f051";
private static final String MOBILE_GRANT_TYPE = "sms";
private final SocialProperties socialProperties;
private final SysLoginService loginService;
private final SysRegisterService registerService;
@@ -114,6 +121,31 @@ public class AuthController {
return R.ok(loginVo);
}
/**
* 移动端短信登录,复用 app 客户端和 sms 授权策略。
*/
@PostMapping("/mobile/sms-login")
public R<LoginVo> mobileSmsLogin(@Validated @RequestBody MobileSmsLoginBody body) {
SysClientVo client = clientService.queryByClientId(MOBILE_CLIENT_ID);
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), MOBILE_GRANT_TYPE)) {
log.info("移动端客户端id: {} 认证类型:{} 异常!.", MOBILE_CLIENT_ID, MOBILE_GRANT_TYPE);
return R.fail(MessageUtils.message("auth.grant.type.error"));
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
}
String tenantId = StringUtils.blankToDefault(body.tenantId(), TenantConstants.DEFAULT_TENANT_ID);
loginService.checkTenant(tenantId);
Map<String, String> loginBody = new HashMap<>();
loginBody.put("clientId", MOBILE_CLIENT_ID);
loginBody.put("grantType", MOBILE_GRANT_TYPE);
loginBody.put("tenantId", tenantId);
loginBody.put("phonenumber", body.phonenumber());
loginBody.put("smsCode", body.smsCode());
return R.ok(IAuthStrategy.login(JsonUtils.toJsonString(loginBody), client, MOBILE_GRANT_TYPE));
}
/**
* 获取跳转URL
*
@@ -240,4 +272,14 @@ public class AuthController {
return R.ok(result);
}
public record MobileSmsLoginBody(
@NotBlank(message = "{user.phonenumber.not.blank}")
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}")
String phonenumber,
@NotBlank(message = "{sms.code.not.blank}")
String smsCode,
String tenantId
) {
}
}
@@ -7,10 +7,12 @@ import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.RegexConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
@@ -26,6 +28,7 @@ import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.web.domain.vo.CaptchaVo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -52,6 +55,9 @@ public class CaptchaController {
private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties;
@Value("${aihr.sms.login-template-id:}")
private String smsLoginTemplateId;
/**
* 短信验证码
*
@@ -59,20 +65,23 @@ public class CaptchaController {
*/
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
@GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
public R<Void> smsCode(
@NotBlank(message = "{user.phonenumber.not.blank}")
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}") String phonenumber) {
if (StringUtils.isBlank(smsLoginTemplateId)) {
return R.fail("短信模板未配置:请设置 aihr.sms.login-template-id");
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, smsLoginTemplateId, map);
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString());
return R.fail(String.valueOf(smsResponse.getData()));
}
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return R.ok();
}
@@ -12,6 +12,7 @@ import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SmsLoginBody;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
@@ -51,8 +52,8 @@ public class SmsAuthStrategy implements IAuthStrategy {
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByPhonenumber(phonenumber);
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode));
SysUserVo user = loadOrRegisterUserByPhonenumber(tenantId, phonenumber);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
@@ -87,11 +88,20 @@ public class SmsAuthStrategy implements IAuthStrategy {
return code.equals(smsCode);
}
private SysUserVo loadUserByPhonenumber(String phonenumber) {
private SysUserVo loadOrRegisterUserByPhonenumber(String tenantId, String phonenumber) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber);
SysUser sysUser = new SysUser();
sysUser.setTenantId(tenantId);
sysUser.setUserName(phonenumber);
sysUser.setNickName("手机用户" + phonenumber.substring(7));
sysUser.setUserType(UserType.APP_USER.getUserType());
sysUser.setPhonenumber(phonenumber);
sysUser.setStatus(SystemConstants.NORMAL);
sysUser.setRemark("移动端短信自动注册");
userMapper.insert(sysUser);
log.info("短信登录自动注册用户:{}", phonenumber);
user = userMapper.selectVoById(sysUser.getUserId());
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber);
@@ -155,6 +155,9 @@ mail:
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
aihr:
sms:
login-template-id: ${AIHR_SMS_LOGIN_TEMPLATE_ID:}
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
@@ -172,11 +175,11 @@ sms:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
access-key-id: 您的accessKey
access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID:您的accessKey}
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET:您的accessKeySecret}
signature: ${ALIYUN_SMS_SIGN_NAME:您的短信签名}
sdk-app-id: ${ALIYUN_SMS_SDK_APP_ID:您的sdkAppId}
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent
@@ -158,6 +158,9 @@ mail:
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
aihr:
sms:
login-template-id: ${AIHR_SMS_LOGIN_TEMPLATE_ID:}
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
@@ -175,11 +178,11 @@ sms:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
access-key-id: 您的accessKey
access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID:您的accessKey}
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET:您的accessKeySecret}
signature: ${ALIYUN_SMS_SIGN_NAME:您的短信签名}
sdk-app-id: ${ALIYUN_SMS_SDK_APP_ID:您的sdkAppId}
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent