完成登录功能 #37
@@ -10,11 +10,12 @@
|
|||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>gym-auth</artifactId>
|
<artifactId>gym-auth</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>Gym Auth</name>
|
<name>Gym Auth</name>
|
||||||
<description>Phone Authentication Module - Phone Number Login Services</description>
|
<description>Authentication module for Gym Management System</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -29,93 +30,46 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>manage-sys</artifactId>
|
<artifactId>gym-member</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>gym-member</artifactId>
|
<artifactId>manage-sys</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springdoc</groupId>
|
|
||||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.data</groupId>
|
|
||||||
<artifactId>spring-data-commons</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.projectreactor</groupId>
|
|
||||||
<artifactId>reactor-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-all</artifactId>
|
|
||||||
<version>5.8.25</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- 阿里云一键登录服务 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.aliyun</groupId>
|
|
||||||
<artifactId>aliyun-java-sdk-dypnsapi</artifactId>
|
|
||||||
<version>1.2.12</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||||
<version>4.6.0</version>
|
<version>4.6.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>dysmsapi20170525</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>2.0.0</version>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
|
||||||
<version>3.4.2</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>default-jar</id>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>jar</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.11.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>21</source>
|
<source>${java.version}</source>
|
||||||
<target>21</target>
|
<target>${java.version}</target>
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>${lombok.version}</version>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.auth.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class AuthConfig {
|
|
||||||
}
|
|
||||||
+1
-25
@@ -4,44 +4,20 @@ import lombok.Data;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
/**
|
|
||||||
* 阿里云短信配置属性类
|
|
||||||
*
|
|
||||||
* @author auto-generated
|
|
||||||
* @date 2026-06-20
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
|
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
|
||||||
public class SmsProperties {
|
public class SmsProperties {
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问密钥ID
|
|
||||||
*/
|
|
||||||
private String accessKeyId;
|
private String accessKeyId;
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问密钥密钥
|
|
||||||
*/
|
|
||||||
private String accessKeySecret;
|
private String accessKeySecret;
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信签名名称
|
|
||||||
*/
|
|
||||||
private String signName;
|
private String signName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信模板CODE
|
|
||||||
*/
|
|
||||||
private String templateCode;
|
private String templateCode;
|
||||||
|
|
||||||
/**
|
private int codeLength = 4;
|
||||||
* 短信验证码长度(默认6位)
|
|
||||||
*/
|
|
||||||
private int codeLength = 6;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信验证码有效期(秒,默认300秒=5分钟)
|
|
||||||
*/
|
|
||||||
private long codeExpireSeconds = 300;
|
private long codeExpireSeconds = 300;
|
||||||
}
|
}
|
||||||
-3
@@ -7,9 +7,6 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号验证码登录请求DTO
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
+4
-9
@@ -1,26 +1,21 @@
|
|||||||
package cn.novalon.gym.manage.auth.dto;
|
package cn.novalon.gym.manage.auth.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号一键登录请求DTO
|
|
||||||
* uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class PhoneLoginDto {
|
public class PhoneLoginDto {
|
||||||
|
|
||||||
@NotBlank(message = "access_token不能为空")
|
@NotBlank(message = "手机号不能为空")
|
||||||
private String accessToken;
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
@NotBlank(message = "openid不能为空")
|
|
||||||
private String openid;
|
|
||||||
|
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
|||||||
-22
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
+2
-3
@@ -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.PhoneCodeLoginDto;
|
||||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
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 cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -39,8 +38,8 @@ public class PhoneAuthHandler {
|
|||||||
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
|
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
|
||||||
log.info("收到发送短信验证码请求");
|
log.info("收到发送短信验证码请求");
|
||||||
|
|
||||||
return request.bodyToMono(SendCodeRequest.class)
|
return request.bodyToMono(PhoneLoginDto.class)
|
||||||
.flatMap(req -> phoneAuthService.sendSmsCode(req.getPhone()))
|
.flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone()))
|
||||||
.flatMap(success -> ServerResponse.ok()
|
.flatMap(success -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败")));
|
.bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败")));
|
||||||
|
|||||||
+1
-23
@@ -1,37 +1,15 @@
|
|||||||
package cn.novalon.gym.manage.auth.service;
|
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.PhoneCodeLoginDto;
|
||||||
|
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||||
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号认证服务接口
|
|
||||||
*/
|
|
||||||
public interface PhoneAuthService {
|
public interface PhoneAuthService {
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号一键登录(uniapp官方运营商认证)
|
|
||||||
* 已注册则直接登录,未注册则自动注册后登录
|
|
||||||
*
|
|
||||||
* @param request 登录请求
|
|
||||||
* @return 登录响应
|
|
||||||
*/
|
|
||||||
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
|
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送短信验证码(阿里云)
|
|
||||||
*
|
|
||||||
* @param phone 手机号
|
|
||||||
* @return 是否发送成功
|
|
||||||
*/
|
|
||||||
Mono<Boolean> sendSmsCode(String phone);
|
Mono<Boolean> sendSmsCode(String phone);
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号验证码登录(阿里云)
|
|
||||||
*
|
|
||||||
* @param request 登录请求
|
|
||||||
* @return 登录响应
|
|
||||||
*/
|
|
||||||
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
|
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
|
||||||
}
|
}
|
||||||
+5
-28
@@ -1,35 +1,12 @@
|
|||||||
package cn.novalon.gym.manage.auth.service;
|
package cn.novalon.gym.manage.auth.service;
|
||||||
|
|
||||||
/**
|
import reactor.core.publisher.Mono;
|
||||||
* 短信服务接口
|
|
||||||
*
|
|
||||||
* @author auto-generated
|
|
||||||
* @date 2026-06-20
|
|
||||||
*/
|
|
||||||
public interface SmsService {
|
public interface SmsService {
|
||||||
|
|
||||||
/**
|
Mono<Boolean> sendVerificationCode(String phone);
|
||||||
* 发送短信验证码
|
|
||||||
*
|
|
||||||
* @param phone 手机号
|
|
||||||
* @return 发送结果
|
|
||||||
*/
|
|
||||||
boolean sendVerificationCode(String phone);
|
|
||||||
|
|
||||||
/**
|
Mono<Boolean> verifyCode(String phone, String code);
|
||||||
* 验证短信验证码
|
|
||||||
*
|
|
||||||
* @param phone 手机号
|
|
||||||
* @param code 验证码
|
|
||||||
* @return 验证结果
|
|
||||||
*/
|
|
||||||
boolean verifyCode(String phone, String code);
|
|
||||||
|
|
||||||
/**
|
Mono<String> getVerificationCode(String phone);
|
||||||
* 获取验证码(用于测试或特殊场景)
|
|
||||||
*
|
|
||||||
* @param phone 手机号
|
|
||||||
* @return 验证码
|
|
||||||
*/
|
|
||||||
String getVerificationCode(String phone);
|
|
||||||
}
|
}
|
||||||
+6
-58
@@ -1,6 +1,5 @@
|
|||||||
package cn.novalon.gym.manage.auth.service.impl;
|
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.PhoneCodeLoginDto;
|
||||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||||
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
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.EsSyncUtils;
|
||||||
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
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 jakarta.annotation.PostConstruct;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -43,7 +35,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
private final EsSyncUtils esSyncUtils;
|
private final EsSyncUtils esSyncUtils;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final SmsService smsService;
|
private final SmsService smsService;
|
||||||
private final SmsProperties smsProperties;
|
|
||||||
|
|
||||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||||
|
|
||||||
@@ -54,12 +45,9 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
|
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
|
||||||
log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid());
|
log.info("手机号一键登录, phone: {}", request.getPhone());
|
||||||
|
|
||||||
return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid()))
|
String encryptedPhone = encryptPhone(request.getPhone());
|
||||||
.flatMap(phone -> {
|
|
||||||
log.info("通过access_token获取手机号成功: {}", maskPhone(phone));
|
|
||||||
String encryptedPhone = encryptPhone(phone);
|
|
||||||
|
|
||||||
return memberRepository.findByPhone(encryptedPhone)
|
return memberRepository.findByPhone(encryptedPhone)
|
||||||
.flatMap(existingMember -> {
|
.flatMap(existingMember -> {
|
||||||
@@ -70,60 +58,19 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
log.info("手机号未注册,创建新会员");
|
log.info("手机号未注册,创建新会员");
|
||||||
return createNewMemberAndLogin(request, encryptedPhone);
|
return createNewMemberAndLogin(request, encryptedPhone);
|
||||||
}));
|
}));
|
||||||
})
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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());
|
||||||
@@ -140,6 +87,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
log.info("手机号未注册,创建新会员");
|
log.info("手机号未注册,创建新会员");
|
||||||
PhoneLoginDto registerRequest = new PhoneLoginDto();
|
PhoneLoginDto registerRequest = new PhoneLoginDto();
|
||||||
|
registerRequest.setPhone(request.getPhone());
|
||||||
return createNewMemberAndLogin(registerRequest, encryptedPhone);
|
return createNewMemberAndLogin(registerRequest, encryptedPhone);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -205,7 +153,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
vo.setNeedCompleteInfo(needCompleteInfo);
|
vo.setNeedCompleteInfo(needCompleteInfo);
|
||||||
vo.setNickname(member.getNickname());
|
vo.setNickname(member.getNickname());
|
||||||
vo.setAvatar(member.getAvatar());
|
vo.setAvatar(member.getAvatar());
|
||||||
vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null);
|
vo.setPhone(decryptPhone(member.getPhone()));
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+132
-82
@@ -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);
|
||||||
|
|
||||||
|
String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit";
|
||||||
|
|
||||||
|
return redisUtil.get(rateLimitKey, Long.class)
|
||||||
|
.defaultIfEmpty(0L)
|
||||||
|
.flatMap(lastSendTime -> {
|
||||||
|
long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) {
|
||||||
|
long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime);
|
||||||
|
log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds);
|
||||||
|
return Mono.just(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.fromCallable(() -> {
|
||||||
try {
|
try {
|
||||||
// 生成验证码
|
IAcsClient client = createClient();
|
||||||
String code = generateCode();
|
|
||||||
|
|
||||||
// 发送短信
|
CommonRequest request = new CommonRequest();
|
||||||
boolean sent = sendSms(phone, code);
|
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");
|
||||||
|
|
||||||
if (sent) {
|
log.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}",
|
||||||
// 将验证码存入Redis,5分钟过期
|
smsProperties.getSignName(),
|
||||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
smsProperties.getTemplateCode(),
|
||||||
redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds())
|
"{\"code\":\"##code##\",\"min\":\"5\"}");
|
||||||
.doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s",
|
|
||||||
phone, code, smsProperties.getCodeExpireSeconds()))
|
|
||||||
.block();
|
|
||||||
|
|
||||||
log.info("短信验证码发送成功, phone: {}", phone);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("短信验证码发送失败, phone: {}", phone);
|
JsonNode messageNode = jsonNode.get("Message");
|
||||||
|
String message = messageNode != null ? messageNode.asText() : "unknown";
|
||||||
|
log.error("短信验证码发送失败, phone: {}, code: {}, message: {}", phone, code, message);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("发送短信验证码异常, phone: {}", phone, e);
|
log.error("发送短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e);
|
||||||
return false;
|
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);
|
||||||
|
|
||||||
|
return Mono.fromCallable(() -> {
|
||||||
try {
|
try {
|
||||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
IAcsClient client = createClient();
|
||||||
String cachedCode = redisUtil.get(cacheKey, String.class).block();
|
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedCode.equals(code)) {
|
String responseCode = codeNode.asText();
|
||||||
// 验证成功后删除验证码
|
JsonNode modelNode = jsonNode.get("Model");
|
||||||
redisUtil.delete(cacheKey).block();
|
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);
|
log.info("验证码验证成功, phone: {}", phone);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode);
|
JsonNode messageNode = jsonNode.get("Message");
|
||||||
|
String message = messageNode != null ? messageNode.asText() : "unknown";
|
||||||
|
log.warn("验证码验证失败, phone: {}, code: {}, message: {}, result: {}",
|
||||||
|
phone, responseCode, message, verifyResult);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("验证短信验证码异常, phone: {}", phone, e);
|
log.error("验证短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e);
|
||||||
return false;
|
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,9 +5,6 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
|
||||||
* 手机号一键登录响应VO
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
-4
@@ -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
|
cn.novalon.gym.manage.auth.config.SmsProperties
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
<artifactId>manage-common</artifactId>
|
<artifactId>manage-common</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
|
<artifactId>manage-sys</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>manage-db</artifactId>
|
<artifactId>manage-db</artifactId>
|
||||||
@@ -82,7 +87,7 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- ZXing QR Code依赖 -->
|
<!-- ZXing二维码生成库 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.zxing</groupId>
|
<groupId>com.google.zxing</groupId>
|
||||||
<artifactId>core</artifactId>
|
<artifactId>core</artifactId>
|
||||||
@@ -93,20 +98,20 @@
|
|||||||
<artifactId>javase</artifactId>
|
<artifactId>javase</artifactId>
|
||||||
<version>3.5.3</version>
|
<version>3.5.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里云OSS SDK -->
|
<!-- 阿里云OSS SDK -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun.oss</groupId>
|
<groupId>com.aliyun.oss</groupId>
|
||||||
<artifactId>aliyun-sdk-oss</artifactId>
|
<artifactId>aliyun-sdk-oss</artifactId>
|
||||||
<version>3.17.4</version>
|
<version>3.17.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
<version>3.4.2</version>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
+16
@@ -71,6 +71,22 @@ public class Member extends BaseEntity {
|
|||||||
@Column("official_open_id")
|
@Column("official_open_id")
|
||||||
private String officialOpenId;
|
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")
|
@Column("is_deleted")
|
||||||
private Boolean isDeleted;
|
private Boolean isDeleted;
|
||||||
|
|||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
package cn.novalon.gym.manage.app.config;
|
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.checkIn.handler.CheckInHandler;
|
||||||
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
||||||
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
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.GroupCourseBookingHandler;
|
||||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
||||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
|
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ logging:
|
|||||||
cn.novalon.manage: DEBUG
|
cn.novalon.manage: DEBUG
|
||||||
org.springframework.r2dbc: DEBUG
|
org.springframework.r2dbc: DEBUG
|
||||||
org.springframework.web: TRACE
|
org.springframework.web: TRACE
|
||||||
|
|
||||||
|
alibaba:
|
||||||
|
cloud:
|
||||||
|
sms:
|
||||||
|
access-key-id: LTAI5t8GhorWLu5WkEx8MDZz
|
||||||
|
access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz
|
||||||
|
sign-name: 云渚科技验证平台
|
||||||
|
template-code: 100001
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ wechat:
|
|||||||
|
|
||||||
# 手机号加密配置
|
# 手机号加密配置
|
||||||
phone-encryption:
|
phone-encryption:
|
||||||
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY}
|
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY:P8539ANjWJWsRbVHZKhM8Q==}
|
||||||
iv: ${PHONE_ENCRYPTION_IV}
|
iv: ${PHONE_ENCRYPTION_IV:3tHp07uMRYh1xKsIXvYJMA==}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
|
|||||||
+19
@@ -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;
|
||||||
|
}
|
||||||
+54
@@ -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<T> {
|
||||||
|
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
private Long timestamp;
|
||||||
|
|
||||||
|
public static <T> Result<T> success(T data) {
|
||||||
|
return Result.<T>builder()
|
||||||
|
.code(200)
|
||||||
|
.message("success")
|
||||||
|
.data(data)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Result<T> success(String message) {
|
||||||
|
return Result.<T>builder()
|
||||||
|
.code(200)
|
||||||
|
.message(message)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Result<T> fail(Integer code, String message) {
|
||||||
|
return Result.<T>builder()
|
||||||
|
.code(code)
|
||||||
|
.message(message)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Result<T> fail(String message) {
|
||||||
|
return Result.<T>builder()
|
||||||
|
.code(500)
|
||||||
|
.message(message)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -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<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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-微信授权';
|
||||||
Reference in New Issue
Block a user