diff --git a/gym-manage-api/gym-auth/pom.xml b/gym-manage-api/gym-auth/pom.xml index 42aa5be..23e8943 100644 --- a/gym-manage-api/gym-auth/pom.xml +++ b/gym-manage-api/gym-auth/pom.xml @@ -10,11 +10,12 @@ 1.0.0 + cn.novalon.gym.manage gym-auth jar Gym Auth - Phone Authentication Module - Phone Number Login Services + Authentication module for Gym Management System @@ -29,93 +30,46 @@ cn.novalon.gym.manage - manage-sys + gym-member ${project.version} cn.novalon.gym.manage - gym-member + manage-sys ${project.version} + org.springframework.boot spring-boot-starter-webflux - - org.springdoc - springdoc-openapi-starter-webflux-ui - org.springframework.boot - spring-boot-starter-security - - - org.springframework.data - spring-data-commons - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - - com.aliyun - aliyun-java-sdk-dypnsapi - 1.2.12 + spring-boot-starter-validation + com.aliyun aliyun-java-sdk-core - 4.6.0 + 4.6.3 + - com.aliyun - dysmsapi20170525 - 2.0.0 + org.projectlombok + lombok + true - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - default-jar - package - - jar - - - - org.apache.maven.plugins maven-compiler-plugin 3.11.0 - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - + ${java.version} + ${java.version} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java deleted file mode 100644 index d8d842d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package cn.novalon.gym.manage.auth.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AuthConfig { -} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java index fe9e35c..31551f3 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java @@ -4,44 +4,20 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -/** - * 阿里云短信配置属性类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Data @Configuration @ConfigurationProperties(prefix = "alibaba.cloud.sms") public class SmsProperties { - /** - * 访问密钥ID - */ private String accessKeyId; - /** - * 访问密钥密钥 - */ private String accessKeySecret; - /** - * 短信签名名称 - */ private String signName; - /** - * 短信模板CODE - */ private String templateCode; - /** - * 短信验证码长度(默认6位) - */ - private int codeLength = 6; + private int codeLength = 4; - /** - * 短信验证码有效期(秒,默认300秒=5分钟) - */ private long codeExpireSeconds = 300; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java index 4d21283..033da34 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java @@ -7,9 +7,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号验证码登录请求DTO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java index c3d0758..2cc0743 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java @@ -1,28 +1,23 @@ package cn.novalon.gym.manage.auth.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录请求DTO - * uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号 - */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PhoneLoginDto { - @NotBlank(message = "access_token不能为空") - private String accessToken; - - @NotBlank(message = "openid不能为空") - private String openid; + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; private String nickname; private String avatar; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java deleted file mode 100644 index c16602d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 发送短信验证码请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SendCodeRequest { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java deleted file mode 100644 index d97cff5..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 短信验证码登录请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SmsLoginDto { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; - - @NotBlank(message = "验证码不能为空") - private String code; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java index 562920a..af46968 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java @@ -2,7 +2,6 @@ package cn.novalon.gym.manage.auth.handler; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; -import cn.novalon.gym.manage.auth.dto.SendCodeRequest; import cn.novalon.gym.manage.auth.service.PhoneAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -39,8 +38,8 @@ public class PhoneAuthHandler { public Mono sendSmsCode(ServerRequest request) { log.info("收到发送短信验证码请求"); - return request.bodyToMono(SendCodeRequest.class) - .flatMap(req -> phoneAuthService.sendSmsCode(req.getPhone())) + return request.bodyToMono(PhoneLoginDto.class) + .flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone())) .flatMap(success -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败"))); diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java index 2407a9b..cd2848e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java @@ -1,37 +1,15 @@ package cn.novalon.gym.manage.auth.service; -import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; +import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.vo.PhoneLoginVO; import reactor.core.publisher.Mono; -/** - * 手机号认证服务接口 - */ public interface PhoneAuthService { - /** - * 手机号一键登录(uniapp官方运营商认证) - * 已注册则直接登录,未注册则自动注册后登录 - * - * @param request 登录请求 - * @return 登录响应 - */ Mono oneClickLogin(PhoneLoginDto request); - /** - * 发送短信验证码(阿里云) - * - * @param phone 手机号 - * @return 是否发送成功 - */ Mono sendSmsCode(String phone); - /** - * 手机号验证码登录(阿里云) - * - * @param request 登录请求 - * @return 登录响应 - */ Mono codeLogin(PhoneCodeLoginDto request); } \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java index 914f4ff..585074e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java @@ -1,35 +1,12 @@ package cn.novalon.gym.manage.auth.service; -/** - * 短信服务接口 - * - * @author auto-generated - * @date 2026-06-20 - */ +import reactor.core.publisher.Mono; + public interface SmsService { - /** - * 发送短信验证码 - * - * @param phone 手机号 - * @return 发送结果 - */ - boolean sendVerificationCode(String phone); + Mono sendVerificationCode(String phone); - /** - * 验证短信验证码 - * - * @param phone 手机号 - * @param code 验证码 - * @return 验证结果 - */ - boolean verifyCode(String phone, String code); + Mono verifyCode(String phone, String code); - /** - * 获取验证码(用于测试或特殊场景) - * - * @param phone 手机号 - * @return 验证码 - */ - String getVerificationCode(String phone); -} + Mono getVerificationCode(String phone); +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java index 468f5a1..fe96c7f 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java @@ -1,6 +1,5 @@ package cn.novalon.gym.manage.auth.service.impl; -import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.service.PhoneAuthService; @@ -16,13 +15,6 @@ import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.MemberNoGenerator; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; -import com.aliyuncs.DefaultAcsClient; -import com.aliyuncs.IAcsClient; -import com.aliyuncs.dypnsapi.model.v20170525.GetMobileRequest; -import com.aliyuncs.dypnsapi.model.v20170525.GetMobileResponse; -import com.aliyuncs.exceptions.ClientException; -import com.aliyuncs.exceptions.ServerException; -import com.aliyuncs.profile.DefaultProfile; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,7 +35,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { private final EsSyncUtils esSyncUtils; private final JwtTokenProvider jwtTokenProvider; private final SmsService smsService; - private final SmsProperties smsProperties; private EsSyncUtils.EntitySyncer memberSyncer; @@ -54,76 +45,32 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { @Override public Mono oneClickLogin(PhoneLoginDto request) { - log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid()); + log.info("手机号一键登录, phone: {}", request.getPhone()); - return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid())) - .flatMap(phone -> { - log.info("通过access_token获取手机号成功: {}", maskPhone(phone)); - String encryptedPhone = encryptPhone(phone); + String encryptedPhone = encryptPhone(request.getPhone()); - return memberRepository.findByPhone(encryptedPhone) - .flatMap(existingMember -> { - log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); - return doLogin(existingMember, false, request); - }) - .switchIfEmpty(Mono.defer(() -> { - log.info("手机号未注册,创建新会员"); - return createNewMemberAndLogin(request, encryptedPhone); - })); + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); + return doLogin(existingMember, false, request); }) - .onErrorResume(e -> { - log.error("一键登录失败", e); - return Mono.error(new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + e.getMessage())); - }); - } - - /** - * 通过access_token和openid获取手机号 - * 使用阿里云DYPNS API - */ - private String getPhoneByToken(String accessToken, String openid) throws Exception { - DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret()); - IAcsClient client = new DefaultAcsClient(profile); - GetMobileRequest request = new GetMobileRequest(); - request.setAccessToken(accessToken); - - try { - GetMobileResponse response = client.getAcsResponse(request); - if ("OK".equals(response.getCode())) { - return response.getGetMobileResultDTO().getMobile(); - } - log.warn("DYPNS API返回错误: code={}, message={}", response.getCode(), response.getMessage()); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + response.getMessage()); - } catch (ServerException e) { - log.error("阿里云DYPNS API服务端异常", e); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败"); - } catch (ClientException e) { - log.error("阿里云DYPNS API客户端异常", e); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败"); - } - } - - /** - * 手机号脱敏显示 - */ - private String maskPhone(String phone) { - if (phone == null || phone.length() < 11) { - return phone; - } - return phone.substring(0, 3) + "****" + phone.substring(7); + .switchIfEmpty(Mono.defer(() -> { + log.info("手机号未注册,创建新会员"); + return createNewMemberAndLogin(request, encryptedPhone); + })); } @Override public Mono sendSmsCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - return Mono.fromCallable(() -> smsService.sendVerificationCode(phone)); + return smsService.sendVerificationCode(phone); } @Override public Mono codeLogin(PhoneCodeLoginDto request) { log.info("手机号验证码登录, phone: {}", request.getPhone()); - return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode())) + return smsService.verifyCode(request.getPhone(), request.getCode()) .flatMap(verified -> { if (!verified) { log.warn("验证码验证失败, phone: {}", request.getPhone()); @@ -140,6 +87,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { .switchIfEmpty(Mono.defer(() -> { log.info("手机号未注册,创建新会员"); PhoneLoginDto registerRequest = new PhoneLoginDto(); + registerRequest.setPhone(request.getPhone()); return createNewMemberAndLogin(registerRequest, encryptedPhone); })); }); @@ -205,7 +153,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { vo.setNeedCompleteInfo(needCompleteInfo); vo.setNickname(member.getNickname()); vo.setAvatar(member.getAvatar()); - vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null); + vo.setPhone(decryptPhone(member.getPhone())); return vo; } @@ -226,4 +174,4 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { return null; } } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java index 2162e13..2a8b630 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java @@ -1,148 +1,198 @@ package cn.novalon.gym.manage.auth.service.impl; -import cn.hutool.core.util.RandomUtil; import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.service.SmsService; import cn.novalon.gym.manage.common.constant.RedisKeyConstants; import cn.novalon.gym.manage.common.util.RedisUtil; -import com.aliyun.dysmsapi20170525.Client; -import com.aliyun.dysmsapi20170525.models.SendSmsRequest; -import com.aliyun.dysmsapi20170525.models.SendSmsResponse; -import com.aliyun.teaopenapi.models.Config; +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; -/** - * 短信服务实现类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Slf4j @Service @RequiredArgsConstructor public class SmsServiceImpl implements SmsService { + private static final long SEND_INTERVAL_SECONDS = 60; + private static final long CODE_EXPIRE_SECONDS = 300; + private final SmsProperties smsProperties; private final RedisUtil redisUtil; + private final ObjectMapper objectMapper; @Override - public boolean sendVerificationCode(String phone) { + public Mono sendVerificationCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - try { - // 生成验证码 - String code = generateCode(); + String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit"; - // 发送短信 - boolean sent = sendSms(phone, code); + return redisUtil.get(rateLimitKey, Long.class) + .defaultIfEmpty(0L) + .flatMap(lastSendTime -> { + long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); - if (sent) { - // 将验证码存入Redis,5分钟过期 - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds()) - .doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s", - phone, code, smsProperties.getCodeExpireSeconds())) - .block(); + if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) { + long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime); + log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds); + return Mono.just(false); + } - log.info("短信验证码发送成功, phone: {}", phone); - return true; - } + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - log.warn("短信验证码发送失败, phone: {}", phone); - return false; + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("SendSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("SignName", smsProperties.getSignName()); + request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode()); + request.putQueryParameter("TemplateParam", "{\"code\":\"##code##\",\"min\":\"5\"}"); + request.putQueryParameter("ReturnVerifyCode", "true"); - } catch (Exception e) { - log.error("发送短信验证码异常, phone: {}", phone, e); - return false; - } + log.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}", + smsProperties.getSignName(), + smsProperties.getTemplateCode(), + "{\"code\":\"##code##\",\"min\":\"5\"}"); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String code = codeNode.asText(); + + if ("OK".equals(code)) { + JsonNode requestIdNode = jsonNode.get("RequestId"); + String requestId = requestIdNode != null ? requestIdNode.asText() : "unknown"; + log.info("短信验证码发送成功, phone: {}, requestId: {}", phone, requestId); + + // 提取验证码并存入Redis + JsonNode modelNode = jsonNode.get("Model"); + if (modelNode != null) { + JsonNode verifyCodeNode = modelNode.get("VerifyCode"); + if (verifyCodeNode != null) { + String verifyCode = verifyCodeNode.asText(); + String smsCodeKey = RedisKeyConstants.SMS_CODE + phone; + redisUtil.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe(); + log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS); + } else { + log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData); + } + } else { + log.warn("响应中未找到Model字段, 原始响应: {}", responseData); + } + + redisUtil.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe(); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.error("短信验证码发送失败, phone: {}, code: {}, message: {}", phone, code, message); + return false; + + } catch (Exception e) { + log.error("发送短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); + return false; + } + }); + }); } @Override - public boolean verifyCode(String phone, String code) { + public Mono verifyCode(String phone, String code) { log.info("验证短信验证码, phone: {}", phone); - try { - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - String cachedCode = redisUtil.get(cacheKey, String.class).block(); + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - if (cachedCode == null) { - log.warn("验证码已过期或不存在, phone: {}", phone); + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("CheckSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("VerifyCode", code); + + log.info("阿里云号码认证核验参数 - phone: {}, code: {}", phone, code); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证核验原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云核验响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String responseCode = codeNode.asText(); + JsonNode modelNode = jsonNode.get("Model"); + JsonNode verifyResultNode = modelNode != null ? modelNode.get("VerifyResult") : null; + boolean verifyResult = verifyResultNode != null && "PASS".equals(verifyResultNode.asText()); + + if ("OK".equals(responseCode) && verifyResult) { + log.info("验证码验证成功, phone: {}", phone); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.warn("验证码验证失败, phone: {}, code: {}, message: {}, result: {}", + phone, responseCode, message, verifyResult); + return false; + + } catch (Exception e) { + log.error("验证短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); return false; } - - if (cachedCode.equals(code)) { - // 验证成功后删除验证码 - redisUtil.delete(cacheKey).block(); - log.info("验证码验证成功, phone: {}", phone); - return true; - } - - log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode); - return false; - - } catch (Exception e) { - log.error("验证短信验证码异常, phone: {}", phone, e); - return false; - } + }); } @Override - public String getVerificationCode(String phone) { + public Mono getVerificationCode(String phone) { try { String cacheKey = RedisKeyConstants.SMS_CODE + phone; - return redisUtil.get(cacheKey, String.class).block(); + return redisUtil.get(cacheKey, String.class); } catch (Exception e) { log.error("获取验证码异常, phone: {}", phone, e); - return null; + return Mono.empty(); } } - /** - * 生成验证码 - */ - private String generateCode() { - return RandomUtil.randomNumbers(smsProperties.getCodeLength()); + private IAcsClient createClient() throws ClientException { + IClientProfile profile = DefaultProfile.getProfile( + "cn-hangzhou", + smsProperties.getAccessKeyId(), + smsProperties.getAccessKeySecret()); + DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", "Dypnsapi", "dypnsapi.aliyuncs.com"); + return new DefaultAcsClient(profile); } - - /** - * 发送短信 - */ - private boolean sendSms(String phone, String code) throws Exception { - log.info("调用阿里云短信API发送短信, phone: {}, code: {}", phone, code); - - // 创建阿里云短信客户端 - Config config = new Config() - .setAccessKeyId(smsProperties.getAccessKeyId()) - .setAccessKeySecret(smsProperties.getAccessKeySecret()) - .setEndpoint("dysmsapi.aliyuncs.com"); - - Client client = new Client(config); - - // 构建发送短信请求 - SendSmsRequest request = new SendSmsRequest() - .setPhoneNumbers(phone) - .setSignName(smsProperties.getSignName()) - .setTemplateCode(smsProperties.getTemplateCode()) - .setTemplateParam("{\"code\":\"" + code + "\"}"); - - // 发送短信 - SendSmsResponse response = client.sendSms(request); - - log.info("阿里云短信API响应, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - - // 判断发送结果 - if ("OK".equals(response.getBody().getCode())) { - return true; - } - - log.error("阿里云短信发送失败, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - return false; - } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java index f5f4651..795c103 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java @@ -5,9 +5,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录响应VO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 216a5f9..beb4996 100644 --- a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1 @@ -cn.novalon.gym.manage.auth.handler.PhoneAuthHandler -cn.novalon.gym.manage.auth.service.impl.PhoneAuthServiceImpl -cn.novalon.gym.manage.auth.service.impl.SmsServiceImpl -cn.novalon.gym.manage.auth.config.AuthConfig cn.novalon.gym.manage.auth.config.SmsProperties \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index 12e0171..bfafbbb 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -35,6 +35,11 @@ manage-common ${project.version} + + cn.novalon.gym.manage + manage-sys + ${project.version} + cn.novalon.gym.manage manage-db @@ -81,8 +86,8 @@ gym-member ${project.version} - - + + com.google.zxing core @@ -93,20 +98,20 @@ javase 3.5.3 + com.aliyun.oss aliyun-sdk-oss - 3.17.4 + 3.17.1 - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 + org.springframework.boot + spring-boot-maven-plugin diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java index 8983e20..5c96f8f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -71,6 +71,22 @@ public class Member extends BaseEntity { @Column("official_open_id") private String officialOpenId; + // 阿里云号码认证OpenID + @Column("dypns_open_id") + private String dypnsOpenId; + + // 身份证号码(AES加密存储) + @Column("id_card") + private String idCard; + + // 真实姓名(AES加密存储) + @Column("real_name") + private String realName; + + // 注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权 + @Column("register_channel") + private String registerChannel; + // 软删除 @Column("is_deleted") private Boolean isDeleted; diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 6e95ea5..7a15e65 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -58,7 +58,7 @@ gym-auth ${project.version} - + org.springframework.boot spring-boot-starter-webflux diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index 482d931..2a4da75 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -1,10 +1,11 @@ package cn.novalon.gym.manage.app.config; +import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; + import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler; diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index 46b6a66..d14149a 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -36,3 +36,11 @@ logging: cn.novalon.manage: DEBUG org.springframework.r2dbc: DEBUG org.springframework.web: TRACE + +alibaba: + cloud: + sms: + access-key-id: LTAI5t8GhorWLu5WkEx8MDZz + access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz + sign-name: 云渚科技验证平台 + template-code: 100001 diff --git a/gym-manage-api/manage-app/src/main/resources/member-config.yml b/gym-manage-api/manage-app/src/main/resources/member-config.yml index 05ce7cb..5338737 100644 --- a/gym-manage-api/manage-app/src/main/resources/member-config.yml +++ b/gym-manage-api/manage-app/src/main/resources/member-config.yml @@ -16,8 +16,8 @@ wechat: # 手机号加密配置 phone-encryption: - secret-key: ${PHONE_ENCRYPTION_SECRET_KEY} - iv: ${PHONE_ENCRYPTION_IV} + secret-key: ${PHONE_ENCRYPTION_SECRET_KEY:P8539ANjWJWsRbVHZKhM8Q==} + iv: ${PHONE_ENCRYPTION_IV:3tHp07uMRYh1xKsIXvYJMA==} spring: elasticsearch: diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java new file mode 100644 index 0000000..d4995e8 --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java @@ -0,0 +1,19 @@ +package cn.novalon.gym.manage.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "auth") +public class AuthConfig { + + private String accessKeyId; + + private String accessKeySecret; + + private Integer tokenExpireSeconds = 86400; + + private Integer refreshTokenExpireSeconds = 604800; +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java new file mode 100644 index 0000000..7b0efbc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java @@ -0,0 +1,54 @@ +package cn.novalon.gym.manage.common.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + private Integer code; + + private String message; + + private T data; + + private Long timestamp; + + public static Result success(T data) { + return Result.builder() + .code(200) + .message("success") + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result success(String message) { + return Result.builder() + .code(200) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(Integer code, String message) { + return Result.builder() + .code(code) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(String message) { + return Result.builder() + .code(500) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java new file mode 100644 index 0000000..bae13cc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java @@ -0,0 +1,86 @@ +package cn.novalon.gym.manage.common.util; + +import cn.novalon.gym.manage.common.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtil { + + private final JwtProperties jwtProperties; + + private SecretKey getSigningKey() { + String secret = jwtProperties.getSecret(); + if (secret.length() < 32) { + StringBuilder sb = new StringBuilder(secret); + while (sb.length() < 32) { + sb.append(secret); + } + secret = sb.substring(0, 32); + } + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String userId, String phone) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration()); + + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("phone", phone); + + return Jwts.builder() + .setClaims(claims) + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public String generateRefreshToken(String userId) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration() * 7); + + return Jwts.builder() + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String getUserIdFromToken(String token) { + return parseToken(token).getSubject(); + } + + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (Exception e) { + log.warn("JWT token validation failed: {}", e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql new file mode 100644 index 0000000..05765d1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql @@ -0,0 +1,21 @@ +-- ============================================ +-- V20: 更新 member_user 表 - 添加阿里云号码认证字段 +-- 支持一键登录功能 +-- ============================================ + +-- 添加阿里云号码认证相关字段 +ALTER TABLE IF EXISTS member_user + ADD COLUMN IF NOT EXISTS dypns_open_id VARCHAR(100), + ADD COLUMN IF NOT EXISTS id_card VARCHAR(50), + ADD COLUMN IF NOT EXISTS real_name VARCHAR(50), + ADD COLUMN IF NOT EXISTS register_channel VARCHAR(50) DEFAULT 'SMS'; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_member_user_dypns_open_id ON member_user(dypns_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_register_channel ON member_user(register_channel); + +-- 添加字段注释 +COMMENT ON COLUMN member_user.dypns_open_id IS '阿里云号码认证OpenID(一键登录用户唯一标识)'; +COMMENT ON COLUMN member_user.id_card IS '身份证号码(AES加密存储)'; +COMMENT ON COLUMN member_user.real_name IS '真实姓名(AES加密存储)'; +COMMENT ON COLUMN member_user.register_channel IS '注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权';