Author SHA1 Message Date
future 840e24dc3e 合并dev分支 2026-06-24 23:11:01 +08:00
future b5f8523554 完成登录功能 2026-06-24 23:11:01 +08:00
future b291dacf3d 修复丢失文件 2026-06-24 23:11:01 +08:00
future 8cb7bc6ae8 feat: 添加签到模块和数据库表结构
- 新增gym-checkIn签到模块
- 创建sign_in_record签到记录表(V13迁移脚本)
- 添加签到相关路由配置
- 更新应用配置以支持签到模块
- 添加Redis密钥常量
2026-06-24 23:11:00 +08:00
future 91b005975a 签到模块 2026-06-24 23:08:54 +08:00
27 changed files with 516 additions and 596 deletions
-202
View File
@@ -1,202 +0,0 @@
import java.util.Base64;
import java.math.BigInteger;
import java.io.ByteArrayInputStream;
public class KeyAnalyzer {
public static void main(String[] args) {
String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcxAwrhZF+izz1gxQKOr4jnC05obIBHbl0DzHOcd4CaaDV7Kv1NRJKi33JhdDsW4JGgu16e5Rtzq1VzU5VWz5EKgGL46maOFwCkngJUTP/LC3JVf/wtJmYCm8xNE8C7GNNIqKzYEvUOEfXqagpVvVrGsQ9FpzFp2rP9hBmHY3yizVyPX/uT9S+af5TRxiaItj3SSJGgloaEMrnKOpb/EH7JwPSS0liAyT/NxPfOyyZHc22AvaAIOE5y+0PMUIKPuIdfpOrej3LVpO1Arc2hSgmdB+YIPSiBVYPXa6AuAmil9mpbtSikQJ7Uu7lX4JyTW4QxQ06rPFKnFWVKkzivAElAgMBAAECggEAJd0AJ37iTlMpDQ90xqe7hvRQxAu256gbQ9nrqLY97g0/KIw6WEZSPakFX6gvdvb/NzKmUyAIEKGLoh6tXdZk6qfOqc/6BeK47nIcBfwT9/zerjNUVvn34w4aHyNINieMMHQ+Id8PUZmqWH+Euz9ilVTosuyEPwUZulLvUQqwXzU5VnwVghURbUhDd+ecBJACWgemRun6d5241PQXNYAdH1k7cETd8GfIi3qclhhJrxi7tu5tq4YGCXQIoz7HCLim7GIvT0M+FRgSw2EOrHnAQNFeQ/vQbP71ttLoTxehL6Se9dfWrV5OI+Y/T7vR2F84Qt0iNbaxyJGir7siKDFIwQKBgQDXDzinx3/TasplM78pR/0CtuuKr1Ch02LOPrTosJ1qf1OohxQThowhOTxMsBlgYSKu9s1QRffUUXEqYXxd2B6lzDKfggwO6U2XxIcxWeNow0xoFfqcXYSg7Ga2sCr9uhdwxIdFQNF7SNBpT8ht4fJrRX6mWY1nHybpyTDQ4xoQNQKBgQC6m+yiOoi5JD3zVSSJq/iq5DJPA5B4aoP+t5u9lp2Q7iVO0QI5ilBlEKGE4VOU0glnXlDTfuqEYooMY85ekl4WGb3AOT0PhLL2i+gO2nlWBzf4HPzB/hibjfyPyniRM03cHkG3HXucL7Sne6FwERcfjEqjUd2cdP1l89PNrq4rMQKBgQCMmjABSWYh8/y1I5rEQ4OAJdVjC3GdC1Xa35ZpVybjvLEWSpHunhW5lvD8dllw8LC7UTI0XDpGPqTM/4VO2YBYB2PFc0Gs8g0/v0ZgFpOeJ6kpl80MM/wFNemFYTIKRoMSv/psZY9PmfBgGcBBTuquBXZjDcNr+yr2yAm5V/DvTQKBgHqRi94KoF8q5N39IKCkqhJlDH5FkxDktYoKw2rFkPzuzuZz9gghRyj6wXxsG9/2DWMt2dzw0czehFoa/CO188KEadPmRKr6uCmkP2nyKhxNZX+8WnB5G2Sg4DD6BjMpBYz8+qDx5ozx8LDJTYI0V4HLPgMD9JGdbgsXGhlREOkhAoGALr6IQOXnviWNAhCdc7rrsaMLMPbLZ1wqzWtQUG1JxDobbpzEP4CW/mvW5pMn58mSBg5qbXhyDI4fFP0CPb98QIz2tGnIzYyFzdKmF5Z1N7X1OF9O+tsSqASoBZzTqB4fr/o4mz0s9JCeriBR2LWjsbsDU13DTLsfQpWbtOnIy70=";
System.out.println("========== 密钥分析开始 ==========");
System.out.println("密钥长度: " + privateKeyBase64.length());
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
System.out.println("解码后长度: " + keyBytes.length + " bytes");
// 打印前30字节
System.out.print("前30字节(hex): ");
for (int i = 0; i < 30 && i < keyBytes.length; i++) {
System.out.printf("%02X ", keyBytes[i]);
}
System.out.println();
// 解析ASN.1结构
try {
parseASN1(keyBytes);
} catch (Exception e) {
System.out.println("解析失败: " + e.getMessage());
e.printStackTrace();
}
}
static void parseASN1(byte[] keyBytes) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(keyBytes);
// 读取SEQUENCE
int tag = bis.read();
System.out.println("第一个tag: 0x" + Integer.toHexString(tag));
if (tag == 0x30) {
System.out.println("这是SEQUENCE");
// 读取长度
int len = readASN1Length(bis);
System.out.println("SEQUENCE长度: " + len);
// 检查下一个tag
int nextTag = bis.read();
System.out.println("下一个tag: 0x" + Integer.toHexString(nextTag));
if (nextTag == 0x02) {
// INTEGER
int intLen = readASN1Length(bis);
byte[] versionBytes = new byte[intLen];
bis.read(versionBytes);
int version = new BigInteger(versionBytes).intValue();
System.out.println("版本INTEGER值: " + version);
// 检查这是PKCS#8还是PKCS#1
// PKCS#8: version=0后是SEQUENCE(算法标识符)
// PKCS#1: version=0后是INTEGER(modulus)
int afterVersionTag = bis.read();
System.out.println("版本后的tag: 0x" + Integer.toHexString(afterVersionTag));
if (afterVersionTag == 0x30) {
System.out.println(">>> 这是PKCS#8格式 (version后是SEQUENCE)");
// 解析PKCS#8
// AlgorithmIdentifier: SEQUENCE { OID, NULL }
int algLen = readASN1Length(bis);
System.out.println("AlgorithmIdentifier长度: " + algLen);
// 跳过AlgorithmIdentifier内容
byte[] algBytes = new byte[algLen];
bis.read(algBytes);
// 打印OID
System.out.print("OID bytes: ");
for (int i = 0; i < algBytes.length; i++) {
System.out.printf("%02X ", algBytes[i]);
}
System.out.println();
// 下一个应该是OCTET STRING (包含私钥)
int octetTag = bis.read();
System.out.println("下一个tag: 0x" + Integer.toHexString(octetTag));
if (octetTag == 0x04) {
System.out.println("这是OCTET STRING (包含私钥数据)");
int octetLen = readASN1Length(bis);
System.out.println("OCTET STRING长度: " + octetLen);
// OCTET STRING内容是PKCS#1私钥
byte[] pkcs1Bytes = new byte[octetLen];
bis.read(pkcs1Bytes);
System.out.print("PKCS#1私钥前20字节: ");
for (int i = 0; i < 20; i++) {
System.out.printf("%02X ", pkcs1Bytes[i]);
}
System.out.println();
// 解析PKCS#1私钥
ByteArrayInputStream pkcs1Stream = new ByteArrayInputStream(pkcs1Bytes);
int pkcs1Tag = pkcs1Stream.read();
System.out.println("PKCS#1第一个tag: 0x" + Integer.toHexString(pkcs1Tag));
if (pkcs1Tag == 0x30) {
int pkcs1Len = readASN1Length(pkcs1Stream);
System.out.println("PKCS#1 SEQUENCE长度: " + pkcs1Len);
// version
int vTag = pkcs1Stream.read();
int vLen = readASN1Length(pkcs1Stream);
byte[] vBytes = new byte[vLen];
pkcs1Stream.read(vBytes);
System.out.println("PKCS#1版本: " + new BigInteger(vBytes));
// 解析私钥参数
parsePKCS1(pkcs1Stream);
}
}
} else if (afterVersionTag == 0x02) {
System.out.println(">>> 这是PKCS#1格式 (version后是INTEGER)");
// 继续解析PKCS#1
parsePKCS1(bis);
} else {
System.out.println(">>> 未知格式 (tag: 0x" + Integer.toHexString(afterVersionTag) + ")");
}
}
}
}
static void parsePKCS1(ByteArrayInputStream bis) throws Exception {
System.out.println("\n========== 解析PKCS#1私钥参数 ==========");
// modulus
BigInteger modulus = readASN1Integer(bis);
System.out.println("modulus长度: " + modulus.bitLength() + " bits");
// publicExponent
BigInteger publicExponent = readASN1Integer(bis);
System.out.println("publicExponent: " + publicExponent);
// privateExponent
BigInteger privateExponent = readASN1Integer(bis);
System.out.println("privateExponent长度: " + privateExponent.bitLength() + " bits");
// prime1
BigInteger prime1 = readASN1Integer(bis);
System.out.println("prime1长度: " + prime1.bitLength() + " bits");
// prime2
BigInteger prime2 = readASN1Integer(bis);
System.out.println("prime2长度: " + prime2.bitLength() + " bits");
// 验证 prime1 * prime2 == modulus
BigInteger calculatedModulus = prime1.multiply(prime2);
boolean valid = calculatedModulus.equals(modulus);
System.out.println("\n验证 prime1 * prime2 == modulus: " + valid);
if (!valid) {
System.out.println(">>> 密钥数学关系不正确!这是无效的RSA私钥");
System.out.println("计算得到的modulus长度: " + calculatedModulus.bitLength() + " bits");
}
// exponent1
BigInteger exponent1 = readASN1Integer(bis);
System.out.println("exponent1长度: " + exponent1.bitLength() + " bits");
// exponent2
BigInteger exponent2 = readASN1Integer(bis);
System.out.println("exponent2长度: " + exponent2.bitLength() + " bits");
// coefficient
BigInteger coefficient = readASN1Integer(bis);
System.out.println("coefficient长度: " + coefficient.bitLength() + " bits");
System.out.println("\n========== 解析完成 ==========");
}
static int readASN1Length(ByteArrayInputStream bis) throws Exception {
int firstByte = bis.read();
if ((firstByte & 0x80) == 0) {
return firstByte;
}
int numBytes = firstByte & 0x7F;
int length = 0;
for (int i = 0; i < numBytes; i++) {
length = (length << 8) | (bis.read() & 0xFF);
}
return length;
}
static BigInteger readASN1Integer(ByteArrayInputStream bis) throws Exception {
int tag = bis.read();
if (tag != 0x02) throw new Exception("期望INTEGER tag: 0x02, 实际: 0x" + Integer.toHexString(tag));
int len = readASN1Length(bis);
byte[] data = new byte[len];
bis.read(data);
return new BigInteger(1, data);
}
}
+14 -60
View File
@@ -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>
@@ -1,7 +0,0 @@
package cn.novalon.gym.manage.auth.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthConfig {
}
@@ -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;
} }
@@ -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
@@ -1,28 +1,23 @@
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;
private String avatar; private String avatar;
} }
@@ -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,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,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);
} }
@@ -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);
}
@@ -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,76 +45,32 @@ 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 -> {
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
return doLogin(existingMember, false, request); return doLogin(existingMember, false, request);
})
.switchIfEmpty(Mono.defer(() -> {
log.info("手机号未注册,创建新会员");
return createNewMemberAndLogin(request, encryptedPhone);
}));
}) })
.onErrorResume(e -> { .switchIfEmpty(Mono.defer(() -> {
log.error("一键登录失败", e); log.info("手机号未注册,创建新会员");
return Mono.error(new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + e.getMessage())); return createNewMemberAndLogin(request, encryptedPhone);
}); }));
}
/**
* 通过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;
} }
@@ -226,4 +174,4 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
return null; return null;
} }
} }
} }
@@ -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,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
@@ -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
+11 -6
View File
@@ -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>
@@ -81,8 +86,8 @@
<artifactId>gym-member</artifactId> <artifactId>gym-member</artifactId>
<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>
@@ -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;
+1 -1
View File
@@ -63,7 +63,7 @@
<artifactId>gym-auth</artifactId> <artifactId>gym-auth</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>
@@ -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:
@@ -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;
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -0,0 +1,99 @@
-- ============================================
-- 签到记录表(sign_in_record
-- ============================================
-- Step 1: 创建 sign_in_record 表
CREATE TABLE IF NOT EXISTS sign_in_record (
-- ========== 主键和基础字段 ==========
id BIGSERIAL PRIMARY KEY, -- 自增主键
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录更新时间
-- ========== 会员相关字段 ==========
member_id BIGINT NOT NULL, -- 会员ID,关联member_user表
member_card_id BIGINT, -- 签到时使用的会员卡ID
-- ========== 签到核心字段 ==========
sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间
sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败
-- ========== 验证和错误信息 ==========
verification_details TEXT, -- JSONB格式,存储会员卡验证时的快照数据
fail_reason VARCHAR(500), -- 失败时的具体原因文案
-- ========== 操作人信息 ==========
operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL
operator_name VARCHAR(100), -- 操作人姓名冗余
-- ========== 设备和环境信息 ==========
device_info VARCHAR(200), -- 签到设备标识或型号
ip_address VARCHAR(50), -- 客户端IP地址
source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端
-- ========== 软删除字段 ==========
is_delete BOOLEAN DEFAULT FALSE -- 软删除标识:false-未删除,true-已删除
);
-- Step 2: 创建索引
-- 会员ID索引(加速按会员查询签到记录)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
-- 签到时间索引(加速按时间范围查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
-- 签到状态索引(加速按状态筛选)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
-- 会员卡ID索引(加速按会员卡查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
-- 操作人ID索引(加速按操作人查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
-- 签到来源索引(加速按来源统计)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
-- 软删除索引(加速查询未删除的记录)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
-- 复合索引:会员ID + 签到时间(加速会员签到历史查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
-- 复合索引:签到状态 + 签到时间(加速统计数据查询)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
-- 复合索引:签到日期(用于每日统计,使用表达式索引)
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_date ON sign_in_record(DATE(sign_in_time));
-- Step 3: 添加外键约束(可选,根据业务需求决定)
-- 注意:如果member_user表使用了软删除,外键约束可能需要谨慎使用
-- ALTER TABLE sign_in_record
-- ADD CONSTRAINT fk_sign_in_record_member
-- FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE SET NULL;
-- Step 4: 添加注释
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member_user表';
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据(包含卡类型、剩余次数/金额、有效期等)';
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余(避免关联查询)';
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
@@ -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-微信授权';
@@ -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;