完成登录功能 #41

Merged
fujia merged 2 commits from feature/member-register-integration into dev 2026-06-25 01:14:27 +08:00
11 changed files with 193 additions and 145 deletions
+7 -1
View File
@@ -68,7 +68,13 @@
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>5.8.25</version> <version>5.8.25</version>
</dependency> </dependency>
<!-- 阿里云一键登录服务 --> <!-- 阿里云号码认证服务(新版SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dypnsapi20170525</artifactId>
<version>1.0.6</version>
</dependency>
<!-- 阿里云一键登录服务(旧版SDK,保留兼容) -->
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dypnsapi</artifactId> <artifactId>aliyun-java-sdk-dypnsapi</artifactId>
@@ -1,5 +1,7 @@
package cn.novalon.gym.manage.auth.service; package cn.novalon.gym.manage.auth.service;
import reactor.core.publisher.Mono;
/** /**
* 短信服务接口 * 短信服务接口
* *
@@ -14,7 +16,7 @@ public interface SmsService {
* @param phone 手机号 * @param phone 手机号
* @return 发送结果 * @return 发送结果
*/ */
boolean sendVerificationCode(String phone); Mono<Boolean> sendVerificationCode(String phone);
/** /**
* 验证短信验证码 * 验证短信验证码
@@ -23,7 +25,7 @@ public interface SmsService {
* @param code 验证码 * @param code 验证码
* @return 验证结果 * @return 验证结果
*/ */
boolean verifyCode(String phone, String code); Mono<Boolean> verifyCode(String phone, String code);
/** /**
* 获取验证码(用于测试或特殊场景) * 获取验证码(用于测试或特殊场景)
@@ -31,5 +33,5 @@ public interface SmsService {
* @param phone 手机号 * @param phone 手机号
* @return 验证码 * @return 验证码
*/ */
String getVerificationCode(String phone); Mono<String> getVerificationCode(String phone);
} }
@@ -116,14 +116,14 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
@Override @Override
public Mono<Boolean> sendSmsCode(String phone) { public Mono<Boolean> sendSmsCode(String phone) {
log.info("发送短信验证码, phone: {}", phone); log.info("发送短信验证码, phone: {}", phone);
return Mono.fromCallable(() -> smsService.sendVerificationCode(phone)); return smsService.sendVerificationCode(phone);
} }
@Override @Override
public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) { public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) {
log.info("手机号验证码登录, phone: {}", request.getPhone()); log.info("手机号验证码登录, phone: {}", request.getPhone());
return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode())) return smsService.verifyCode(request.getPhone(), request.getCode())
.flatMap(verified -> { .flatMap(verified -> {
if (!verified) { if (!verified) {
log.warn("验证码验证失败, phone: {}", request.getPhone()); log.warn("验证码验证失败, phone: {}", request.getPhone());
@@ -1,148 +1,198 @@
package cn.novalon.gym.manage.auth.service.impl; 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.config.SmsProperties;
import cn.novalon.gym.manage.auth.service.SmsService; import cn.novalon.gym.manage.auth.service.SmsService;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.util.RedisUtil;
import com.aliyun.dysmsapi20170525.Client; import com.aliyuncs.CommonRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest; import com.aliyuncs.CommonResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse; import com.aliyuncs.DefaultAcsClient;
import com.aliyun.teaopenapi.models.Config; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; 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 @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SmsServiceImpl implements SmsService { 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 SmsProperties smsProperties;
private final RedisUtil redisUtil; private final RedisUtil redisUtil;
private final ObjectMapper objectMapper;
@Override @Override
public boolean sendVerificationCode(String phone) { public Mono<Boolean> sendVerificationCode(String phone) {
log.info("发送短信验证码, phone: {}", phone); log.info("发送短信验证码, phone: {}", phone);
try { String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit";
// 生成验证码
String code = generateCode();
// 发送短信 return redisUtil.get(rateLimitKey, Long.class)
boolean sent = sendSms(phone, code); .defaultIfEmpty(0L)
.flatMap(lastSendTime -> {
long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
if (sent) { if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) {
// 将验证码存入Redis,5分钟过期 long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime);
String cacheKey = RedisKeyConstants.SMS_CODE + phone; log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds);
redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds()) return Mono.just(false);
.doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s", }
phone, code, smsProperties.getCodeExpireSeconds()))
.block();
log.info("短信验证码发送成功, phone: {}", phone); return Mono.fromCallable(() -> {
return true; try {
} IAcsClient client = createClient();
log.warn("短信验证码发送失败, phone: {}", phone); CommonRequest request = new CommonRequest();
return false; 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.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}",
log.error("发送短信验证码异常, phone: {}", phone, e); smsProperties.getSignName(),
return false; 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 @Override
public boolean verifyCode(String phone, String code) { public Mono<Boolean> verifyCode(String phone, String code) {
log.info("验证短信验证码, phone: {}", phone); log.info("验证短信验证码, phone: {}", phone);
try { return Mono.fromCallable(() -> {
String cacheKey = RedisKeyConstants.SMS_CODE + phone; try {
String cachedCode = redisUtil.get(cacheKey, String.class).block(); IAcsClient client = createClient();
if (cachedCode == null) { CommonRequest request = new CommonRequest();
log.warn("验证码已过期或不存在, phone: {}", phone); 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; 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 @Override
public String getVerificationCode(String phone) { public Mono<String> getVerificationCode(String phone) {
try { try {
String cacheKey = RedisKeyConstants.SMS_CODE + phone; String cacheKey = RedisKeyConstants.SMS_CODE + phone;
return redisUtil.get(cacheKey, String.class).block(); return redisUtil.get(cacheKey, String.class);
} catch (Exception e) { } catch (Exception e) {
log.error("获取验证码异常, phone: {}", phone, e); log.error("获取验证码异常, phone: {}", phone, e);
return null; return Mono.empty();
} }
} }
/** private IAcsClient createClient() throws ClientException {
* 生成验证码 IClientProfile profile = DefaultProfile.getProfile(
*/ "cn-hangzhou",
private String generateCode() { smsProperties.getAccessKeyId(),
return RandomUtil.randomNumbers(smsProperties.getCodeLength()); 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;
}
}
+5
View File
@@ -118,6 +118,11 @@
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>5.8.25</version> <version>5.8.25</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>com.google.zxing</groupId> <groupId>com.google.zxing</groupId>
<artifactId>core</artifactId> <artifactId>core</artifactId>
@@ -17,7 +17,6 @@ import cn.novalon.gym.manage.member.handler.MemberHandler;
import cn.novalon.gym.manage.member.handler.WechatAuthHandler; import cn.novalon.gym.manage.member.handler.WechatAuthHandler;
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
import cn.novalon.gym.manage.payment.handler.PaymentHandler;
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
@@ -79,8 +78,7 @@ public class SystemRouter {
CourseLabelHandler courseLabelHandler, CourseLabelHandler courseLabelHandler,
CheckInHandler checkInHandler, CheckInHandler checkInHandler,
DataStatisticsHandler dataStatisticsHandler, DataStatisticsHandler dataStatisticsHandler,
PhoneAuthHandler phoneAuthHandler, PhoneAuthHandler phoneAuthHandler) {
PaymentHandler paymentHandler) {
return route() return route()
// ========== 诊断路由 ========== // ========== 诊断路由 ==========
@@ -362,19 +360,6 @@ public class SystemRouter {
.GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics) .GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics)
.GET("/api/datacount/export", dataStatisticsHandler::exportStatistics) .GET("/api/datacount/export", dataStatisticsHandler::exportStatistics)
// ========================================
// ========== 支付模块路由 =================
// ========================================
// ===== 支付宝App支付 =====
.POST("/api/payment/create", paymentHandler::createPayment)
.GET("/api/payment/{orderId}", paymentHandler::getPaymentStatus)
.POST("/api/payment/{orderId}/refund", paymentHandler::refundPayment)
.POST("/api/payment/notify", paymentHandler::alipayNotify)
// ===== 支付宝扫码支付 =====
.POST("/api/payment/qrcode/create", paymentHandler::createQrCodePayment)
.build(); .build();
} }
} }
@@ -2,9 +2,9 @@ spring:
cache: cache:
type: none type: none
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system url: r2dbc:postgresql://localhost:5432/manage_system
username: novalon username: postgres
password: novalon123 password: 123456
pool: pool:
initial-size: 5 initial-size: 5
max-size: 20 max-size: 20
@@ -12,10 +12,10 @@ spring:
max-life-time: 30m max-life-time: 30m
acquire-timeout: 3s acquire-timeout: 3s
flyway: flyway:
url: jdbc:postgresql://localhost:55432/manage_system url: jdbc:postgresql://localhost:5432/manage_system
user: novalon user: postgres
password: novalon123 password: 123456
enabled: true enabled: false
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
validate-on-migrate: false validate-on-migrate: false
@@ -19,7 +19,7 @@ spring:
password: 123456 password: 123456
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
enabled: true enabled: false
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
baseline-version: 0 baseline-version: 0
@@ -4,7 +4,7 @@ spring:
username: ${DB_USERNAME} username: ${DB_USERNAME}
password: ${DB_PASSWORD} password: ${DB_PASSWORD}
flyway: flyway:
enabled: true enabled: false
logging: logging:
level: level:
@@ -15,9 +15,9 @@ spring:
exclude: exclude:
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
r2dbc: r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:novalon} username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:novalon123} password: ${DB_PASSWORD:123456}
pool: pool:
initial-size: 10 initial-size: 10
max-size: 50 max-size: 50
@@ -25,12 +25,12 @@ spring:
max-life-time: 1h max-life-time: 1h
acquire-timeout: 5s acquire-timeout: 5s
datasource: datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:novalon} username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:novalon123} password: ${DB_PASSWORD:123456}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
enabled: true enabled: false
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
baseline-version: 0 baseline-version: 0
@@ -98,9 +98,9 @@ springdoc:
alibaba: alibaba:
cloud: cloud:
sms: sms:
access-key-id: ${ALIBABA_ACCESS_KEY_ID:} access-key-id: LTAI5t8GhorWLu5WkEx8MDZz
access-key-secret: ${ALIBABA_ACCESS_KEY_SECRET:} access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz
sign-name: ${ALIBABA_SMS_SIGN_NAME:} sign-name: 云渚科技验证平台
template-code: ${ALIBABA_SMS_TEMPLATE_CODE:} template-code: 100001
code-length: 6 code-length: 6
code-expire-seconds: 300 code-expire-seconds: 300
@@ -1,4 +1,4 @@
package cn.novalon.gym.manage.sys.config; package cn.novalon.gym.manage.sys.config;
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter; import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter; import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;