登录
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Manifest-Version: 1.0
|
||||
Created-By: Maven JAR Plugin 3.4.2
|
||||
Build-Jdk-Spec: 21
|
||||
Implementation-Title: Gym Payment
|
||||
Implementation-Version: 1.0.0
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
artifactId=gym-payment
|
||||
groupId=cn.novalon.gym.manage
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gym-payment</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Gym Payment</name>
|
||||
<description>Payment Module - Integrates Huifu Payment Gateway</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</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.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+1
@@ -0,0 +1 @@
|
||||
cn.novalon.gym.manage.payment.config.HuifuPayConfig
|
||||
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gym-auth</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Gym Auth</name>
|
||||
<description>Phone Authentication Module - Phone Number Login Services</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-db</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-sys</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-member</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</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>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||
<version>4.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<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>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package cn.novalon.gym.manage.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class AuthConfig {
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cn.novalon.gym.manage.auth.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 阿里云短信配置属性类
|
||||
*
|
||||
* @author auto-generated
|
||||
* @date 2026-06-20
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
|
||||
public class SmsProperties {
|
||||
|
||||
/**
|
||||
* 访问密钥ID
|
||||
*/
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* 访问密钥密钥
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* 短信签名名称
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* 短信模板CODE
|
||||
*/
|
||||
private String templateCode;
|
||||
|
||||
/**
|
||||
* 短信验证码长度(默认6位)
|
||||
*/
|
||||
private int codeLength = 6;
|
||||
|
||||
/**
|
||||
* 短信验证码有效期(秒,默认300秒=5分钟)
|
||||
*/
|
||||
private long codeExpireSeconds = 300;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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 PhoneCodeLoginDto {
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Pattern(regexp = "^\\d{4,6}$", message = "验证码格式不正确")
|
||||
private String code;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cn.novalon.gym.manage.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 手机号一键登录请求DTO
|
||||
* uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PhoneLoginDto {
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private String avatar;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package cn.novalon.gym.manage.auth.handler;
|
||||
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "手机号认证", description = "手机号一键登录与验证码登录")
|
||||
public class PhoneAuthHandler {
|
||||
|
||||
private final PhoneAuthService phoneAuthService;
|
||||
|
||||
@Operation(summary = "手机号一键登录", description = "使用uniapp官方运营商认证,直接手机号登录或注册")
|
||||
public Mono<ServerResponse> oneClickLogin(ServerRequest request) {
|
||||
log.info("收到手机号一键登录请求");
|
||||
|
||||
return request.bodyToMono(PhoneLoginDto.class)
|
||||
.flatMap(phoneAuthService::oneClickLogin)
|
||||
.flatMap(response -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(response));
|
||||
}
|
||||
|
||||
@Operation(summary = "发送短信验证码", description = "使用阿里云发送短信验证码")
|
||||
public Mono<ServerResponse> sendSmsCode(ServerRequest request) {
|
||||
log.info("收到发送短信验证码请求");
|
||||
|
||||
return request.bodyToMono(PhoneLoginDto.class)
|
||||
.flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone()))
|
||||
.flatMap(success -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败")));
|
||||
}
|
||||
|
||||
@Operation(summary = "手机号验证码登录", description = "使用阿里云短信验证码登录或注册")
|
||||
public Mono<ServerResponse> codeLogin(ServerRequest request) {
|
||||
log.info("收到手机号验证码登录请求");
|
||||
|
||||
return request.bodyToMono(PhoneCodeLoginDto.class)
|
||||
.flatMap(phoneAuthService::codeLogin)
|
||||
.flatMap(response -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(response));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
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.vo.PhoneLoginVO;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 手机号认证服务接口
|
||||
*/
|
||||
public interface PhoneAuthService {
|
||||
|
||||
/**
|
||||
* 手机号一键登录(uniapp官方运营商认证)
|
||||
* 已注册则直接登录,未注册则自动注册后登录
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @return 登录响应
|
||||
*/
|
||||
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
|
||||
|
||||
/**
|
||||
* 发送短信验证码(阿里云)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
Mono<Boolean> sendSmsCode(String phone);
|
||||
|
||||
/**
|
||||
* 手机号验证码登录(阿里云)
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @return 登录响应
|
||||
*/
|
||||
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.novalon.gym.manage.auth.service;
|
||||
|
||||
/**
|
||||
* 短信服务接口
|
||||
*
|
||||
* @author auto-generated
|
||||
* @date 2026-06-20
|
||||
*/
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 发送结果
|
||||
*/
|
||||
boolean sendVerificationCode(String phone);
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param code 验证码
|
||||
* @return 验证结果
|
||||
*/
|
||||
boolean verifyCode(String phone, String code);
|
||||
|
||||
/**
|
||||
* 获取验证码(用于测试或特殊场景)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 验证码
|
||||
*/
|
||||
String getVerificationCode(String phone);
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package cn.novalon.gym.manage.auth.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
|
||||
import cn.novalon.gym.manage.auth.service.SmsService;
|
||||
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PhoneAuthServiceImpl implements PhoneAuthService {
|
||||
|
||||
private final IMemberRepository memberRepository;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final SmsService smsService;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request) {
|
||||
log.info("手机号一键登录, phone: {}", request.getPhone());
|
||||
|
||||
String encryptedPhone = encryptPhone(request.getPhone());
|
||||
|
||||
return memberRepository.findByPhone(encryptedPhone)
|
||||
.flatMap(existingMember -> {
|
||||
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
|
||||
return doLogin(existingMember, false, request);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("手机号未注册,创建新会员");
|
||||
return createNewMemberAndLogin(request, encryptedPhone);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> sendSmsCode(String phone) {
|
||||
log.info("发送短信验证码, phone: {}", phone);
|
||||
return Mono.fromCallable(() -> smsService.sendVerificationCode(phone));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) {
|
||||
log.info("手机号验证码登录, phone: {}", request.getPhone());
|
||||
|
||||
return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode()))
|
||||
.flatMap(verified -> {
|
||||
if (!verified) {
|
||||
log.warn("验证码验证失败, phone: {}", request.getPhone());
|
||||
return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "验证码错误或已过期"));
|
||||
}
|
||||
|
||||
String encryptedPhone = encryptPhone(request.getPhone());
|
||||
|
||||
return memberRepository.findByPhone(encryptedPhone)
|
||||
.flatMap(existingMember -> {
|
||||
log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId());
|
||||
return doLogin(existingMember, false, null);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("手机号未注册,创建新会员");
|
||||
PhoneLoginDto registerRequest = new PhoneLoginDto();
|
||||
registerRequest.setPhone(request.getPhone());
|
||||
return createNewMemberAndLogin(registerRequest, encryptedPhone);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<PhoneLoginVO> createNewMemberAndLogin(PhoneLoginDto request, String encryptedPhone) {
|
||||
String memberNo = MemberNoGenerator.generate();
|
||||
log.info("生成会员号: {}", memberNo);
|
||||
|
||||
Member member = new Member();
|
||||
member.setMemberNo(memberNo);
|
||||
member.setPhone(encryptedPhone);
|
||||
member.setNickname(request != null ? request.getNickname() : null);
|
||||
member.setAvatar(request != null ? request.getAvatar() : null);
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
member.setIsDeleted(false);
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
log.info("新会员创建成功, memberId: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo());
|
||||
return doLogin(savedMember, true, request);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<PhoneLoginVO> doLogin(Member member, boolean isNewUser, PhoneLoginDto request) {
|
||||
log.info("登录成功, memberId: {}", member.getId());
|
||||
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
if (isNewUser && request != null) {
|
||||
if ((member.getNickname() == null || member.getNickname().isEmpty())
|
||||
&& request.getNickname() != null && !request.getNickname().isEmpty()) {
|
||||
member.setNickname(request.getNickname());
|
||||
}
|
||||
if ((member.getAvatar() == null || member.getAvatar().isEmpty())
|
||||
&& request.getAvatar() != null && !request.getAvatar().isEmpty()) {
|
||||
member.setAvatar(request.getAvatar());
|
||||
}
|
||||
}
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.map(this::buildLoginResponse);
|
||||
}
|
||||
|
||||
private PhoneLoginVO buildLoginResponse(Member member) {
|
||||
boolean needCompleteInfo = member.getNickname() == null || member.getNickname().isEmpty();
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles);
|
||||
|
||||
log.info("JWT Token 生成成功, memberId: {}", member.getId());
|
||||
|
||||
PhoneLoginVO vo = new PhoneLoginVO();
|
||||
vo.setMemberId(member.getId());
|
||||
vo.setMemberNo(member.getMemberNo());
|
||||
vo.setAccessToken(accessToken);
|
||||
vo.setRefreshToken(accessToken);
|
||||
vo.setExpiresIn(86400);
|
||||
vo.setIsNewUser(member.getCreatedAt() == null ? false :
|
||||
member.getCreatedAt().isAfter(LocalDateTime.now().minusMinutes(1)));
|
||||
vo.setNeedCompleteInfo(needCompleteInfo);
|
||||
vo.setNickname(member.getNickname());
|
||||
vo.setAvatar(member.getAvatar());
|
||||
vo.setPhone(decryptPhone(member.getPhone()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String encryptPhone(String phoneNumber) {
|
||||
try {
|
||||
return AesUtil.encrypt(phoneNumber);
|
||||
} catch (Exception e) {
|
||||
log.error("手机号加密失败", e);
|
||||
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败");
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptPhone(String encryptedPhone) {
|
||||
try {
|
||||
return AesUtil.decrypt(encryptedPhone);
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package cn.novalon.gym.manage.auth.service.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.novalon.gym.manage.auth.config.SmsProperties;
|
||||
import cn.novalon.gym.manage.auth.service.SmsService;
|
||||
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 短信服务实现类
|
||||
*
|
||||
* @author auto-generated
|
||||
* @date 2026-06-20
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmsServiceImpl implements SmsService {
|
||||
|
||||
private final SmsProperties smsProperties;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public boolean sendVerificationCode(String phone) {
|
||||
log.info("发送短信验证码, phone: {}", phone);
|
||||
|
||||
try {
|
||||
// 生成验证码
|
||||
String code = generateCode();
|
||||
|
||||
// 发送短信
|
||||
boolean sent = sendSms(phone, code);
|
||||
|
||||
if (sent) {
|
||||
// 将验证码存入Redis,5分钟过期
|
||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||
redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds())
|
||||
.doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s",
|
||||
phone, code, smsProperties.getCodeExpireSeconds()))
|
||||
.block();
|
||||
|
||||
log.info("短信验证码发送成功, phone: {}", phone);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn("短信验证码发送失败, phone: {}", phone);
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送短信验证码异常, phone: {}", phone, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCode(String phone, String code) {
|
||||
log.info("验证短信验证码, phone: {}", phone);
|
||||
|
||||
try {
|
||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||
String cachedCode = redisUtil.get(cacheKey, String.class).block();
|
||||
|
||||
if (cachedCode == null) {
|
||||
log.warn("验证码已过期或不存在, phone: {}", phone);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cachedCode.equals(code)) {
|
||||
// 验证成功后删除验证码
|
||||
redisUtil.delete(cacheKey).block();
|
||||
log.info("验证码验证成功, phone: {}", phone);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode);
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("验证短信验证码异常, phone: {}", phone, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVerificationCode(String phone) {
|
||||
try {
|
||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||
return redisUtil.get(cacheKey, String.class).block();
|
||||
} catch (Exception e) {
|
||||
log.error("获取验证码异常, phone: {}", phone, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
private String generateCode() {
|
||||
return RandomUtil.randomNumbers(smsProperties.getCodeLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.novalon.gym.manage.auth.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 手机号一键登录响应VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PhoneLoginVO {
|
||||
|
||||
private Long memberId;
|
||||
|
||||
private String memberNo;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private String refreshToken;
|
||||
|
||||
private Integer expiresIn;
|
||||
|
||||
private Boolean isNewUser;
|
||||
|
||||
private Boolean needCompleteInfo;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private String avatar;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
<Match>
|
||||
<Class name="~.*\.entity\..*" />
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="~.*\.dto\..*" />
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="~.*\.converter\..*" />
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
@@ -100,8 +100,20 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-sys</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-db</artifactId>
|
||||
@@ -86,8 +81,8 @@
|
||||
<artifactId>gym-member</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ZXing二维码生成库 -->
|
||||
|
||||
<!-- ZXing QR Code依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
@@ -98,20 +93,14 @@
|
||||
<artifactId>javase</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云OSS SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>3.17.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
+23
-23
@@ -1,8 +1,8 @@
|
||||
package cn.novalon.gym.manage.groupcourse.util;
|
||||
|
||||
import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.PutObjectRequest;
|
||||
//import com.aliyun.oss.OSS;
|
||||
//import com.aliyun.oss.OSSClientBuilder;
|
||||
//import com.aliyun.oss.model.PutObjectRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -14,24 +14,24 @@ import java.time.format.DateTimeFormatter;
|
||||
* 阿里云OSS工具类
|
||||
*/
|
||||
public class OSSUtil {
|
||||
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class);
|
||||
|
||||
|
||||
// OSS配置信息
|
||||
private static final String ENDPOINT = "oss-cn-beijing.aliyuncs.com";
|
||||
private static final String ACCESS_KEY_ID = "LTAI5t9TFh9Vayeahz45kZjg";
|
||||
private static final String ACCESS_KEY_SECRET = "zD6NlCeH5UhjBs4vnQVqn8Ksi3CaZz";
|
||||
private static final String BUCKET_NAME = "ycc-filesaver";
|
||||
|
||||
|
||||
// OSS访问地址前缀
|
||||
private static final String OSS_URL_PREFIX = "https://" + BUCKET_NAME + "." + ENDPOINT + "/";
|
||||
|
||||
|
||||
// 文件存储目录
|
||||
private static final String QRCODE_DIR = "qrcode/";
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件到阿里云OSS
|
||||
*
|
||||
*
|
||||
* @param localFilePath 本地文件路径
|
||||
* @param fileName 文件名(不含路径)
|
||||
* @return OSS访问地址
|
||||
@@ -41,22 +41,22 @@ public class OSSUtil {
|
||||
try {
|
||||
// 创建OSS客户端
|
||||
ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
|
||||
|
||||
// 构建OSS文件路径:qrcode/2026/06/18/xxx.png
|
||||
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
String ossFilePath = QRCODE_DIR + datePath + "/" + fileName;
|
||||
|
||||
|
||||
// 创建上传请求
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath));
|
||||
|
||||
|
||||
// 上传文件
|
||||
ossClient.putObject(putObjectRequest);
|
||||
|
||||
|
||||
// 构建访问地址
|
||||
String accessUrl = OSS_URL_PREFIX + ossFilePath;
|
||||
|
||||
|
||||
logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl);
|
||||
|
||||
|
||||
return accessUrl;
|
||||
} catch (Exception e) {
|
||||
logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e);
|
||||
@@ -67,10 +67,10 @@ public class OSSUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件到阿里云OSS(自定义存储路径)
|
||||
*
|
||||
*
|
||||
* @param localFilePath 本地文件路径
|
||||
* @param ossDirectory OSS存储目录
|
||||
* @param fileName 文件名(不含路径)
|
||||
@@ -81,21 +81,21 @@ public class OSSUtil {
|
||||
try {
|
||||
// 创建OSS客户端
|
||||
ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
|
||||
|
||||
// 构建OSS文件路径
|
||||
String ossFilePath = ossDirectory + fileName;
|
||||
|
||||
|
||||
// 创建上传请求
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath));
|
||||
|
||||
|
||||
// 上传文件
|
||||
ossClient.putObject(putObjectRequest);
|
||||
|
||||
|
||||
// 构建访问地址
|
||||
String accessUrl = OSS_URL_PREFIX + ossFilePath;
|
||||
|
||||
|
||||
logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl);
|
||||
|
||||
|
||||
return accessUrl;
|
||||
} catch (Exception e) {
|
||||
logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
<Match>
|
||||
<Class name="~.*\.entity\..*" />
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="~.*\.dto\..*" />
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="~.*\.converter\..*" />
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
+1
@@ -32,6 +32,7 @@ public class MemberCardRecordHandler {
|
||||
|
||||
@Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒")
|
||||
public Mono<ServerResponse> purchaseCard(ServerRequest request) {
|
||||
|
||||
return request.bodyToMono(PurchaseRequest.class)
|
||||
.flatMap(body -> memberCardService.purchaseCard(
|
||||
body.getMemberId(),
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto;
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
@@ -16,6 +17,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
@@ -50,7 +52,11 @@ public class MemberHandler {
|
||||
return memberService.getMemberInfo(memberId)
|
||||
.flatMap(info -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(info));
|
||||
.bodyValue(info))
|
||||
.onErrorResume(NotFoundException.class, e ->
|
||||
ServerResponse.status(HttpStatus.NOT_FOUND)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(java.util.Map.of("code", 404, "message", e.getMessage())));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息")
|
||||
|
||||
+25
-9
@@ -65,21 +65,37 @@ public class MemberServiceImpl implements MemberService {
|
||||
public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
|
||||
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||
|
||||
// 先查缓存
|
||||
return redisUtil.get(cacheKey, MemberInfoVO.class)
|
||||
.flatMap(cached -> {
|
||||
if (cached != null) {
|
||||
log.debug("从缓存获取会员信息, memberId: {}", memberId);
|
||||
return Mono.just(cached);
|
||||
}
|
||||
return memberRepository.findById(memberId)
|
||||
.map(this::buildMemberInfoResponse)
|
||||
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||
.then(Mono.just(vo)))
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}));
|
||||
});
|
||||
// 缓存没有,查数据库
|
||||
return queryFromDatabaseAndCache(memberId, cacheKey);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
// 缓存返回null,查数据库
|
||||
return queryFromDatabaseAndCache(memberId, cacheKey);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库查询并更新缓存
|
||||
*/
|
||||
private Mono<MemberInfoVO> queryFromDatabaseAndCache(Long memberId, String cacheKey) {
|
||||
return memberRepository.findById(memberId)
|
||||
.map(this::buildMemberInfoResponse)
|
||||
.flatMap(vo -> {
|
||||
// 查询到数据后更新缓存
|
||||
return redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||
.then(Mono.just(vo));
|
||||
})
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gym-payment</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Gym Payment</name>
|
||||
<description>Payment Module - Integrates Huifu Payment Gateway</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</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.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package cn.novalon.gym.manage.payment.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
@Bean
|
||||
public CorsWebFilter corsWebFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedOrigin("*");
|
||||
config.addAllowedMethod("*");
|
||||
config.addAllowedHeader("*");
|
||||
config.setAllowCredentials(false);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsWebFilter(source);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "payment.huifu")
|
||||
public class HuifuPayConfig {
|
||||
|
||||
private String sysId;
|
||||
|
||||
private String productId;
|
||||
|
||||
private String huifuId;
|
||||
|
||||
private String acctId;
|
||||
|
||||
private String privateKey;
|
||||
|
||||
private String publicKey;
|
||||
|
||||
private String createUrl;
|
||||
|
||||
private String queryUrl;
|
||||
|
||||
private String refundUrl;
|
||||
|
||||
private String notifyUrl;
|
||||
|
||||
private String version = "1.0";
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentCreateRequest {
|
||||
|
||||
@NotNull(message = "会员ID不能为空")
|
||||
private Long memberId;
|
||||
|
||||
@NotBlank(message = "订单类型不能为空")
|
||||
@Size(max = 50, message = "订单类型长度不能超过50")
|
||||
private String orderType;
|
||||
|
||||
@NotBlank(message = "商品描述不能为空")
|
||||
@Size(max = 128, message = "商品描述长度不能超过128")
|
||||
private String goodsDesc;
|
||||
|
||||
@NotBlank(message = "交易金额不能为空")
|
||||
private String transAmt;
|
||||
|
||||
@NotBlank(message = "交易类型不能为空")
|
||||
@Size(max = 16, message = "交易类型长度不能超过16")
|
||||
private String tradeType;
|
||||
|
||||
@Size(max = 255, message = "备注长度不能超过255")
|
||||
private String remark;
|
||||
|
||||
@Size(max = 9, message = "账户号长度不能超过9")
|
||||
private String acctId;
|
||||
|
||||
private String timeExpire;
|
||||
|
||||
private String delayAcctFlag;
|
||||
|
||||
private Integer feeFlag;
|
||||
|
||||
@Size(max = 128, message = "禁用支付方式长度不能超过128")
|
||||
private String limitPayType;
|
||||
|
||||
@Size(max = 32, message = "渠道号长度不能超过32")
|
||||
private String channelNo;
|
||||
|
||||
private String payScene;
|
||||
|
||||
private String notifyUrl;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentCreateResponse {
|
||||
|
||||
private String orderId;
|
||||
|
||||
private String tradeType;
|
||||
|
||||
private String qrCode;
|
||||
|
||||
private String payInfo;
|
||||
|
||||
private String transAmt;
|
||||
|
||||
private String payStatus;
|
||||
|
||||
private String message;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentQueryResponse {
|
||||
|
||||
private String orderId;
|
||||
|
||||
private String tradeType;
|
||||
|
||||
private String transAmt;
|
||||
|
||||
private String payStatus;
|
||||
|
||||
private String outTransId;
|
||||
|
||||
private String qrCode;
|
||||
|
||||
private String payTime;
|
||||
|
||||
private String message;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentRefundResponse {
|
||||
|
||||
private String orderId;
|
||||
|
||||
private String refundAmt;
|
||||
|
||||
private String refundStatus;
|
||||
|
||||
private String message;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.handler;
|
||||
|
||||
import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest;
|
||||
import cn.novalon.gym.manage.payment.service.IPaymentService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Tag(name = "支付管理", description = "斗拱聚合支付相关操作")
|
||||
public class PaymentHandler {
|
||||
|
||||
private final IPaymentService paymentService;
|
||||
|
||||
public PaymentHandler(IPaymentService paymentService) {
|
||||
this.paymentService = paymentService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建支付", description = "创建支付订单,支持微信、支付宝等多种支付方式")
|
||||
public Mono<ServerResponse> createPayment(ServerRequest request) {
|
||||
log.info("========== PaymentHandler.createPayment 被调用 ==========");
|
||||
return request.bodyToMono(PaymentCreateRequest.class)
|
||||
.flatMap(paymentService::createPayment)
|
||||
.flatMap(response -> ServerResponse.ok().bodyValue(response))
|
||||
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("创建支付失败: " + e.getMessage())));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询支付状态", description = "根据订单号查询支付状态")
|
||||
public Mono<ServerResponse> queryPayment(ServerRequest request) {
|
||||
String orderId = request.pathVariable("orderId");
|
||||
return paymentService.queryPayment(orderId)
|
||||
.flatMap(response -> ServerResponse.ok().bodyValue(response))
|
||||
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse(e.getMessage())));
|
||||
}
|
||||
|
||||
@Operation(summary = "退款", description = "根据订单号发起退款")
|
||||
public Mono<ServerResponse> refundPayment(ServerRequest request) {
|
||||
String orderId = request.pathVariable("orderId");
|
||||
return request.bodyToMono(Map.class)
|
||||
.flatMap(body -> {
|
||||
String refundAmt = String.valueOf(body.get("refundAmt"));
|
||||
return paymentService.refundPayment(orderId, refundAmt);
|
||||
})
|
||||
.flatMap(response -> ServerResponse.ok().bodyValue(response))
|
||||
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("退款失败: " + e.getMessage())));
|
||||
}
|
||||
|
||||
@Operation(summary = "支付回调", description = "支付成功回调通知")
|
||||
public Mono<ServerResponse> handleNotify(ServerRequest request) {
|
||||
return request.bodyToMono(String.class)
|
||||
.flatMap(paymentService::handleNotify)
|
||||
.then(ServerResponse.ok().bodyValue(buildSuccessResponse("处理成功")))
|
||||
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("处理失败: " + e.getMessage())));
|
||||
}
|
||||
|
||||
private Map<String, Object> buildErrorResponse(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSuccessResponse(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
package cn.novalon.gym.manage.payment.service;
|
||||
|
||||
import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IPaymentService {
|
||||
|
||||
Mono<PaymentCreateResponse> createPayment(PaymentCreateRequest request);
|
||||
|
||||
Mono<PaymentQueryResponse> queryPayment(String orderId);
|
||||
|
||||
Mono<PaymentRefundResponse> refundPayment(String orderId, String refundAmt);
|
||||
|
||||
Mono<Void> handleNotify(String notifyBody);
|
||||
}
|
||||
+723
@@ -0,0 +1,723 @@
|
||||
package cn.novalon.gym.manage.payment.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse;
|
||||
import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse;
|
||||
import cn.novalon.gym.manage.payment.service.IPaymentService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PaymentServiceImpl implements IPaymentService {
|
||||
|
||||
private final OkHttpClient okHttpClient;
|
||||
private final Map<String, PaymentInfo> paymentCache = new ConcurrentHashMap<>();
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
// ==================== 配置 ====================
|
||||
private static final String SYS_ID = "6666000207573586"; // 代理商
|
||||
private static final String PRODUCT_ID = "XLSISV";
|
||||
private static final String HUIFU_ID = "6666000207581039"; // 商户号
|
||||
private static final String ACCT_ID = "F28308086";
|
||||
private static final String ALIPAY_CHANNEL = "hlm001";
|
||||
private static final String NOTIFY_URL = "http://localhost:8084/api/payment/notify";
|
||||
|
||||
// ===== v4 接口 =====
|
||||
private static final String CREATE_URL = "https://api.huifu.com/v4/trade/payment/create";
|
||||
private static final String QUERY_URL = "https://api.huifu.com/v4/trade/payment/query";
|
||||
private static final String REFUND_URL = "https://api.huifu.com/v4/trade/payment/refund";
|
||||
private static final String CLOSE_URL = "https://api.huifu.com/v4/trade/payment/close";
|
||||
|
||||
// ==================== 密钥 ====================
|
||||
// 商户私钥(用于签名请求)
|
||||
private static final String MERCHANT_PRIVATE_KEY =
|
||||
"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=";
|
||||
|
||||
// 汇付公钥(用于验证回调签名)
|
||||
private static final String HUIFU_PUBLIC_KEY =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqolFneAH66z3/3gaIYIaRZOIk/UYzdsXIm0RyawYBYAOu/NJ7ul8CRIrRlt5vd98HodW2yPrXA4+VHF3AS9UE4WTDpo9qV5brhqQSr/lAuZtEwMZwUWwgdnGFMkUFd9RvyGXAqY0bsQrcQgQ6zGjZHzlMljogDR3iblG0ak5ssD2TSC2W+1cxu+id+FP6onZXlXizuClTyIRh17m7CbS6rl0P3M96MlTdCzTeBw/Y54CiegBJI2wOrm2Qa6Dg6KRc+YkaJWjuRJVJkwjk8JhSyALno9oEzuDKaAXlsQlxeIhmAy4esRrZGrMV8SG0gwUZIP8lduPjQE95lCqqJ0gQIDAQAB";
|
||||
|
||||
public PaymentServiceImpl() {
|
||||
this.okHttpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(false)
|
||||
.build();
|
||||
|
||||
// 验证私钥是否有效
|
||||
validatePrivateKey();
|
||||
}
|
||||
|
||||
private void validatePrivateKey() {
|
||||
try {
|
||||
log.info("========== 验证私钥 ==========");
|
||||
String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", "");
|
||||
log.info("私钥长度(去空格后): {}", privateKeyBase64.length());
|
||||
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
|
||||
log.info("私钥解码后长度: {} bytes", privateKeyBytes.length);
|
||||
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(spec);
|
||||
log.info("私钥加载成功,算法: {}", privateKey.getAlgorithm());
|
||||
|
||||
// 测试签名
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initSign(privateKey);
|
||||
signature.update("test".getBytes(StandardCharsets.UTF_8));
|
||||
signature.sign();
|
||||
log.info("私钥签名测试成功!");
|
||||
log.info("========== 私钥验证完成 ==========");
|
||||
} catch (Exception e) {
|
||||
log.error("========== 私钥验证失败 ==========", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 1. 创建支付订单 ====================
|
||||
@Override
|
||||
public Mono<PaymentCreateResponse> createPayment(PaymentCreateRequest request) {
|
||||
log.info("========== createPayment 方法被调用 ==========");
|
||||
String orderId = UUID.randomUUID().toString().replace("-", "");
|
||||
String reqDate = LocalDateTime.now().format(DATE_FORMATTER);
|
||||
String reqSeqId = "RQ" + System.currentTimeMillis();
|
||||
|
||||
// ===== 构建 data 参数 =====
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("req_date", reqDate);
|
||||
data.put("req_seq_id", reqSeqId);
|
||||
data.put("huifu_id", HUIFU_ID);
|
||||
data.put("trade_type", "ALIPAY");
|
||||
data.put("pay_type", "APP"); // APP 支付,返回 alipay_scheme
|
||||
data.put("trans_amt", request.getTransAmt() != null ? request.getTransAmt() : "1");
|
||||
data.put("goods_desc", request.getGoodsDesc() != null ? request.getGoodsDesc() : "会员卡");
|
||||
data.put("acct_id", ACCT_ID);
|
||||
data.put("notify_url", NOTIFY_URL);
|
||||
data.put("remark", request.getRemark() != null ? request.getRemark() : "");
|
||||
|
||||
// ===== 支付宝参数 =====
|
||||
data.put("alipay_channel", ALIPAY_CHANNEL);
|
||||
|
||||
String dataJson = JSONUtil.toJsonStr(data);
|
||||
String sign = generateSign(dataJson);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("sys_id", SYS_ID);
|
||||
requestBody.put("product_id", PRODUCT_ID);
|
||||
requestBody.put("data", dataJson);
|
||||
requestBody.put("sign", sign);
|
||||
|
||||
return Mono.fromCallable(() -> {
|
||||
String jsonBody = JSONUtil.toJsonStr(requestBody);
|
||||
log.info("========== 发起支付宝支付请求 (APP) ==========");
|
||||
log.info("请求URL: {}", CREATE_URL);
|
||||
log.info("请求Body: {}", jsonBody);
|
||||
log.info("==================================");
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(CREATE_URL)
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8")))
|
||||
.addHeader("Content-Type", "application/json; charset=UTF-8")
|
||||
.addHeader("Accept", "application/json")
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
|
||||
int httpCode = response.code();
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
log.info("========== 收到响应 ==========");
|
||||
log.info("HTTP状态码: {}", httpCode);
|
||||
log.info("响应Body: {}", responseBody);
|
||||
log.info("===============================");
|
||||
|
||||
if (httpCode >= 400) {
|
||||
if (responseBody != null && !responseBody.isEmpty()) {
|
||||
try {
|
||||
JSONObject errorJson = JSONUtil.parseObj(responseBody);
|
||||
String errorMsg = errorJson.getStr("error_msg");
|
||||
String errorCode = errorJson.getStr("error_code");
|
||||
if (errorMsg != null) {
|
||||
throw new RuntimeException("支付失败: " + errorMsg + " (code: " + errorCode + ")");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("支付请求失败: HTTP " + httpCode);
|
||||
}
|
||||
|
||||
if (responseBody == null || responseBody.isEmpty()) {
|
||||
throw new RuntimeException("响应体为空");
|
||||
}
|
||||
|
||||
return parseCreateResponse(responseBody, orderId, request);
|
||||
} catch (IOException e) {
|
||||
log.error("网络请求异常", e);
|
||||
throw new RuntimeException("网络请求异常: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 2. 查询支付状态 ====================
|
||||
@Override
|
||||
public Mono<PaymentQueryResponse> queryPayment(String orderId) {
|
||||
PaymentInfo paymentInfo = paymentCache.get(orderId);
|
||||
if (paymentInfo == null) {
|
||||
return Mono.error(new RuntimeException("支付记录不存在"));
|
||||
}
|
||||
|
||||
String reqDate = LocalDateTime.now().format(DATE_FORMATTER);
|
||||
String reqSeqId = "RQ" + System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("req_date", reqDate);
|
||||
data.put("req_seq_id", reqSeqId);
|
||||
data.put("huifu_id", HUIFU_ID);
|
||||
data.put("out_trans_id", paymentInfo.outTransId);
|
||||
|
||||
String dataJson = JSONUtil.toJsonStr(data);
|
||||
String sign = generateSign(dataJson);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("sys_id", SYS_ID);
|
||||
requestBody.put("product_id", PRODUCT_ID);
|
||||
requestBody.put("data", dataJson);
|
||||
requestBody.put("sign", sign);
|
||||
|
||||
return Mono.fromCallable(() -> {
|
||||
String jsonBody = JSONUtil.toJsonStr(requestBody);
|
||||
log.info("查询支付状态, orderId={}", orderId);
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(QUERY_URL)
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8")))
|
||||
.addHeader("Content-Type", "application/json; charset=UTF-8")
|
||||
.addHeader("Accept", "application/json")
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
|
||||
int httpCode = response.code();
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
log.info("查询响应: code={}, body={}", httpCode, responseBody);
|
||||
|
||||
if (httpCode >= 400) {
|
||||
throw new RuntimeException("查询失败: HTTP " + httpCode);
|
||||
}
|
||||
|
||||
if (responseBody == null || responseBody.isEmpty()) {
|
||||
throw new RuntimeException("查询响应为空");
|
||||
}
|
||||
|
||||
return parseQueryResponse(responseBody, orderId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 3. 申请退款 ====================
|
||||
@Override
|
||||
public Mono<PaymentRefundResponse> refundPayment(String orderId, String refundAmt) {
|
||||
PaymentInfo paymentInfo = paymentCache.get(orderId);
|
||||
if (paymentInfo == null) {
|
||||
return Mono.error(new RuntimeException("支付记录不存在"));
|
||||
}
|
||||
|
||||
String reqDate = LocalDateTime.now().format(DATE_FORMATTER);
|
||||
String reqSeqId = "RQ" + System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("req_date", reqDate);
|
||||
data.put("req_seq_id", reqSeqId);
|
||||
data.put("huifu_id", HUIFU_ID);
|
||||
data.put("out_trans_id", paymentInfo.outTransId);
|
||||
data.put("trans_amt", refundAmt);
|
||||
|
||||
String dataJson = JSONUtil.toJsonStr(data);
|
||||
String sign = generateSign(dataJson);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("sys_id", SYS_ID);
|
||||
requestBody.put("product_id", PRODUCT_ID);
|
||||
requestBody.put("data", dataJson);
|
||||
requestBody.put("sign", sign);
|
||||
|
||||
return Mono.fromCallable(() -> {
|
||||
String jsonBody = JSONUtil.toJsonStr(requestBody);
|
||||
log.info("发起退款请求, orderId={}", orderId);
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(REFUND_URL)
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8")))
|
||||
.addHeader("Content-Type", "application/json; charset=UTF-8")
|
||||
.addHeader("Accept", "application/json")
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
|
||||
int httpCode = response.code();
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
log.info("退款响应: code={}, body={}", httpCode, responseBody);
|
||||
|
||||
if (httpCode >= 400) {
|
||||
throw new RuntimeException("退款失败: HTTP " + httpCode);
|
||||
}
|
||||
|
||||
return parseRefundResponse(responseBody, orderId, refundAmt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 4. 关闭订单 ====================
|
||||
public Mono<Void> closePayment(String orderId) {
|
||||
PaymentInfo paymentInfo = paymentCache.get(orderId);
|
||||
if (paymentInfo == null) {
|
||||
return Mono.error(new RuntimeException("支付记录不存在"));
|
||||
}
|
||||
|
||||
String reqDate = LocalDateTime.now().format(DATE_FORMATTER);
|
||||
String reqSeqId = "RQ" + System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("req_date", reqDate);
|
||||
data.put("req_seq_id", reqSeqId);
|
||||
data.put("huifu_id", HUIFU_ID);
|
||||
data.put("out_trans_id", paymentInfo.outTransId);
|
||||
|
||||
String dataJson = JSONUtil.toJsonStr(data);
|
||||
String sign = generateSign(dataJson);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("sys_id", SYS_ID);
|
||||
requestBody.put("product_id", PRODUCT_ID);
|
||||
requestBody.put("data", dataJson);
|
||||
requestBody.put("sign", sign);
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
String jsonBody = JSONUtil.toJsonStr(requestBody);
|
||||
log.info("关闭订单请求, orderId={}", orderId);
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(CLOSE_URL)
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8")))
|
||||
.addHeader("Content-Type", "application/json; charset=UTF-8")
|
||||
.addHeader("Accept", "application/json")
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
log.info("关闭订单响应, code={}, body={}", response.code(), responseBody);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("关闭订单异常", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 5. 处理异步通知 ====================
|
||||
@Override
|
||||
public Mono<Void> handleNotify(String notifyBody) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
log.info("========== 收到支付回调通知 ==========");
|
||||
log.info("回调内容: {}", notifyBody);
|
||||
|
||||
try {
|
||||
JSONObject notifyJson = JSONUtil.parseObj(notifyBody);
|
||||
String sign = notifyJson.getStr("sign");
|
||||
String data = notifyJson.getStr("data");
|
||||
|
||||
// 直接打印回调信息,不验证签名
|
||||
log.info("签名: {}", sign);
|
||||
log.info("数据: {}", data);
|
||||
|
||||
if (data != null && !data.isEmpty()) {
|
||||
JSONObject dataJson = JSONUtil.parseObj(data);
|
||||
String respCode = dataJson.getStr("resp_code");
|
||||
String respDesc = dataJson.getStr("resp_desc");
|
||||
String outTransId = dataJson.getStr("out_trans_id");
|
||||
String transStatus = dataJson.getStr("trans_status");
|
||||
String transAmt = dataJson.getStr("trans_amt");
|
||||
String finishDate = dataJson.getStr("finish_date");
|
||||
String finishTime = dataJson.getStr("finish_time");
|
||||
|
||||
log.info("========== 回调数据解析 ==========");
|
||||
log.info("响应码: {}", respCode);
|
||||
log.info("响应描述: {}", respDesc);
|
||||
log.info("商户订单号: {}", outTransId);
|
||||
log.info("交易状态: {}", transStatus);
|
||||
log.info("交易金额: {}", transAmt);
|
||||
log.info("交易完成日期: {}", finishDate);
|
||||
log.info("交易完成时间: {}", finishTime);
|
||||
log.info("===================================");
|
||||
|
||||
// 更新本地支付状态
|
||||
if ("S".equals(transStatus) || "TRADE_SUCCESS".equals(transStatus)) {
|
||||
paymentCache.values().stream()
|
||||
.filter(info -> outTransId != null && outTransId.equals(info.outTransId))
|
||||
.findFirst()
|
||||
.ifPresent(info -> {
|
||||
info.payStatus = transStatus;
|
||||
info.payTime = LocalDateTime.now();
|
||||
log.info("支付状态更新成功, orderId={}, status={}", info.orderId, transStatus);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理回调异常", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 签名方法 ====================
|
||||
private String generateSign(String dataJson) {
|
||||
Exception originalException = null;
|
||||
System.err.println(">>>>>>>>> [PaymentServiceImpl] generateSign 开始执行 <<<<<<<<<<");
|
||||
System.err.flush();
|
||||
try {
|
||||
System.err.println(">>>>>>>>> 开始签名方法 <<<<<<<<<<");
|
||||
System.err.flush();
|
||||
log.error("========== 开始签名 ==========");
|
||||
|
||||
// 打印密钥的前几个字节来检查格式
|
||||
String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", "");
|
||||
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
|
||||
System.err.println(">>>>>>>>> 密钥前20字节(hex): " + bytesToHex(keyBytes, 20) + " <<<<<<<<<<");
|
||||
System.err.println(">>>>>>>>> 密钥长度: " + keyBytes.length + " bytes <<<<<<<<<<");
|
||||
System.err.flush();
|
||||
|
||||
// 检查是否是PKCS#8格式 (应该以30 82开头)
|
||||
if (keyBytes.length > 2) {
|
||||
System.err.println(">>>>>>>>> 密钥前2字节(hex): " + bytesToHex(keyBytes, 2) + " <<<<<<<<<<");
|
||||
boolean startsWith30 = (keyBytes[0] & 0xFF) == 0x30;
|
||||
System.err.println(">>>>>>>>> 密钥是否以0x30开头: " + startsWith30 + " <<<<<<<<<<");
|
||||
System.err.flush();
|
||||
|
||||
// 打印更多头部字节来识别密钥类型
|
||||
if (keyBytes.length > 4) {
|
||||
System.err.println(">>>>>>>>> 密钥前5字节(hex): " + bytesToHex(keyBytes, 5) + " <<<<<<<<<<");
|
||||
System.err.flush();
|
||||
// RSA PKCS#8 私钥通常以 30 82 04** 开头
|
||||
// EC PKCS#8 私钥通常以 30 82 04** 开头 (和RSA一样,需要看后面的内容区分)
|
||||
// 如果是 30 82,且第4个字节是 0x00 或 0x01,可能是RSA私钥
|
||||
// 如果是 30 82,且第4个字节是 0x02 或 0x03,可能是EC私钥
|
||||
}
|
||||
}
|
||||
|
||||
log.info("私钥长度(去空格后): {}", privateKeyBase64.length());
|
||||
|
||||
// 检查私钥格式
|
||||
if (privateKeyBase64.length() < 100) {
|
||||
throw new RuntimeException("私钥长度异常: " + privateKeyBase64.length());
|
||||
}
|
||||
|
||||
log.info("私钥前50字符: {}", privateKeyBase64.substring(0, 50));
|
||||
log.info("私钥后50字符: {}", privateKeyBase64.substring(privateKeyBase64.length() - 50));
|
||||
|
||||
log.info("开始解码私钥...");
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
|
||||
log.info("私钥解码后长度: {} bytes", privateKeyBytes.length);
|
||||
|
||||
// 直接尝试使用PKCS8EncodedKeySpec加载
|
||||
log.info("开始生成 PrivateKey 对象...");
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(spec);
|
||||
log.info("私钥生成成功,算法: {}", privateKey.getAlgorithm());
|
||||
|
||||
log.info("开始签名...");
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
log.info("调用 initSign...");
|
||||
signature.initSign(privateKey);
|
||||
log.info("initSign 完成");
|
||||
log.info("开始 update...");
|
||||
signature.update(dataJson.getBytes(StandardCharsets.UTF_8));
|
||||
log.info("update 完成,开始 sign...");
|
||||
String sign = Base64.getEncoder().encodeToString(signature.sign());
|
||||
log.info("签名生成成功, 签名长度: {}", sign.length());
|
||||
log.info("========== 签名完成 ==========");
|
||||
return sign;
|
||||
} catch (RuntimeException e) {
|
||||
log.error("签名生成失败(RuntimeException): {} - {}", e.getClass().getName(), e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("签名生成失败(Exception): {} - {}", e.getClass().getName(), e.getMessage(), e);
|
||||
throw new RuntimeException("签名生成失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifySign(String sign, String dataJson) {
|
||||
try {
|
||||
String publicKeyBase64 = HUIFU_PUBLIC_KEY.replaceAll("\\s", "");
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey publicKey = keyFactory.generatePublic(spec);
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(dataJson.getBytes(StandardCharsets.UTF_8));
|
||||
return signature.verify(Base64.getDecoder().decode(sign));
|
||||
} catch (Exception e) {
|
||||
log.error("验签失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 响应解析方法 ====================
|
||||
|
||||
private PaymentCreateResponse parseCreateResponse(String responseBody, String orderId, PaymentCreateRequest request) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(responseBody);
|
||||
String dataStr = responseJson.getStr("data");
|
||||
JSONObject data = JSONUtil.parseObj(dataStr);
|
||||
|
||||
String respCode = data.getStr("resp_code");
|
||||
if (!"000000".equals(respCode)) {
|
||||
throw new RuntimeException("支付创建失败: " + data.getStr("resp_desc") + " (code: " + respCode + ")");
|
||||
}
|
||||
|
||||
// APP 支付优先取 alipay_scheme
|
||||
String payUrl = data.getStr("alipay_scheme");
|
||||
if (payUrl == null) {
|
||||
payUrl = data.getStr("pay_url");
|
||||
}
|
||||
if (payUrl == null) {
|
||||
payUrl = data.getStr("pay_info");
|
||||
}
|
||||
|
||||
PaymentInfo paymentInfo = new PaymentInfo();
|
||||
paymentInfo.orderId = orderId;
|
||||
paymentInfo.memberId = request.getMemberId();
|
||||
paymentInfo.orderType = request.getOrderType();
|
||||
paymentInfo.transAmt = request.getTransAmt();
|
||||
paymentInfo.goodsDesc = request.getGoodsDesc();
|
||||
paymentInfo.payStatus = "PENDING";
|
||||
paymentInfo.outTransId = data.getStr("out_trans_id");
|
||||
paymentInfo.payUrl = payUrl;
|
||||
paymentInfo.remark = request.getRemark();
|
||||
paymentCache.put(orderId, paymentInfo);
|
||||
|
||||
log.info("支付创建成功, orderId={}, outTransId={}, payUrl={}", orderId, paymentInfo.outTransId, payUrl);
|
||||
|
||||
return PaymentCreateResponse.builder()
|
||||
.orderId(orderId)
|
||||
.payInfo(payUrl)
|
||||
.transAmt(request.getTransAmt())
|
||||
.payStatus("PENDING")
|
||||
.message("支付创建成功")
|
||||
.build();
|
||||
}
|
||||
|
||||
private PaymentQueryResponse parseQueryResponse(String responseBody, String orderId) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(responseBody);
|
||||
String dataStr = responseJson.getStr("data");
|
||||
JSONObject data = JSONUtil.parseObj(dataStr);
|
||||
|
||||
String respCode = data.getStr("resp_code");
|
||||
if (!"000000".equals(respCode)) {
|
||||
throw new RuntimeException("查询失败: " + data.getStr("resp_desc"));
|
||||
}
|
||||
|
||||
PaymentInfo paymentInfo = paymentCache.get(orderId);
|
||||
if (paymentInfo != null) {
|
||||
paymentInfo.payStatus = data.getStr("trans_status");
|
||||
}
|
||||
|
||||
return PaymentQueryResponse.builder()
|
||||
.orderId(orderId)
|
||||
.tradeType(data.getStr("trade_type"))
|
||||
.transAmt(data.getStr("trans_amt"))
|
||||
.payStatus(data.getStr("trans_status"))
|
||||
.outTransId(data.getStr("out_trans_id"))
|
||||
.payTime(data.getStr("end_time"))
|
||||
.message("查询成功")
|
||||
.build();
|
||||
}
|
||||
|
||||
private PaymentRefundResponse parseRefundResponse(String responseBody, String orderId, String refundAmt) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(responseBody);
|
||||
String dataStr = responseJson.getStr("data");
|
||||
JSONObject data = JSONUtil.parseObj(dataStr);
|
||||
|
||||
String respCode = data.getStr("resp_code");
|
||||
if (!"000000".equals(respCode)) {
|
||||
throw new RuntimeException("退款失败: " + data.getStr("resp_desc"));
|
||||
}
|
||||
|
||||
PaymentInfo paymentInfo = paymentCache.get(orderId);
|
||||
if (paymentInfo != null) {
|
||||
paymentInfo.payStatus = "REFUNDED";
|
||||
}
|
||||
|
||||
return PaymentRefundResponse.builder()
|
||||
.orderId(orderId)
|
||||
.refundAmt(refundAmt)
|
||||
.refundStatus(data.getStr("trans_status"))
|
||||
.message("退款成功")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
private static String bytesToHex(byte[] bytes, int length) {
|
||||
int len = Math.min(bytes.length, length);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < len; i++) {
|
||||
sb.append(String.format("%02X ", bytes[i] & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将PKCS#1格式的RSA私钥转换为PKCS#8格式
|
||||
* PKCS#1: SEQUENCE { version, modulus, publicExponent, privateExponent, prime1, prime2, ... }
|
||||
* PKCS#8: SEQUENCE { version, algorithmIdentifier, OCTET STRING (containing PKCS#1) }
|
||||
*/
|
||||
private byte[] convertPKCS1ToPKCS8(byte[] pkcs1Key) {
|
||||
try {
|
||||
log.info("检测到PKCS#1格式私钥,开始转换为PKCS#8格式...");
|
||||
log.info("PKCS#1密钥长度: {} bytes", pkcs1Key.length);
|
||||
|
||||
// RSA算法OID: 1.2.840.113549.1.1.1
|
||||
byte[] rsaOid = new byte[] {
|
||||
0x06, 0x09, 0x2A, (byte)0x86, 0x48, (byte)0x86, (byte)0xF7, 0x0D, 0x01, 0x01, 0x01
|
||||
};
|
||||
|
||||
// 构建AlgorithmIdentifier: SEQUENCE { OID, NULL }
|
||||
// SEQUENCE (2 bytes: tag + length) + OID + NULL = 15 bytes total
|
||||
byte[] algorithmIdentifier = new byte[15];
|
||||
int idx = 0;
|
||||
algorithmIdentifier[idx++] = 0x30; // SEQUENCE tag
|
||||
algorithmIdentifier[idx++] = 0x0D; // length = 13 bytes for OID + NULL
|
||||
System.arraycopy(rsaOid, 0, algorithmIdentifier, idx, rsaOid.length);
|
||||
idx += rsaOid.length;
|
||||
algorithmIdentifier[idx++] = 0x05; // NULL tag
|
||||
algorithmIdentifier[idx++] = 0x00; // NULL value
|
||||
|
||||
log.info("AlgorithmIdentifier长度: {} bytes", algorithmIdentifier.length);
|
||||
|
||||
// 计算 OCTET STRING 长度编码需要的字节数
|
||||
int octetStringLengthBytes = 1; // tag
|
||||
if (pkcs1Key.length > 127) {
|
||||
if (pkcs1Key.length > 255) {
|
||||
octetStringLengthBytes += 3; // 0x82 + 2 length bytes
|
||||
} else {
|
||||
octetStringLengthBytes += 2; // 0x81 + 1 length byte
|
||||
}
|
||||
} else {
|
||||
octetStringLengthBytes += 1; // 1 length byte
|
||||
}
|
||||
|
||||
// 计算 version INTEGER 编码 (3 bytes: tag + length + value)
|
||||
int versionBytes = 3;
|
||||
|
||||
// 计算 total length of inner content (version + algorithmId + octetString)
|
||||
int innerContentLength = versionBytes + algorithmIdentifier.length + octetStringLengthBytes + pkcs1Key.length;
|
||||
|
||||
// 计算 outer SEQUENCE 长度编码需要的字节数
|
||||
int sequenceLengthBytes = 1; // tag
|
||||
if (innerContentLength > 127) {
|
||||
if (innerContentLength > 255) {
|
||||
sequenceLengthBytes += 3; // 0x82 + 2 length bytes
|
||||
} else {
|
||||
sequenceLengthBytes += 2; // 0x81 + 1 length byte
|
||||
}
|
||||
} else {
|
||||
sequenceLengthBytes += 1;
|
||||
}
|
||||
|
||||
// 分配最终数组
|
||||
int totalLen = sequenceLengthBytes + innerContentLength;
|
||||
byte[] pkcs8Key = new byte[totalLen];
|
||||
log.info("PKCS#8密钥分配: {} bytes", totalLen);
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// outer SEQUENCE
|
||||
pkcs8Key[offset++] = 0x30;
|
||||
if (innerContentLength > 127) {
|
||||
if (innerContentLength > 255) {
|
||||
pkcs8Key[offset++] = (byte)0x82;
|
||||
pkcs8Key[offset++] = (byte)((innerContentLength >> 8) & 0xFF);
|
||||
pkcs8Key[offset++] = (byte)(innerContentLength & 0xFF);
|
||||
} else {
|
||||
pkcs8Key[offset++] = (byte)0x81;
|
||||
pkcs8Key[offset++] = (byte)innerContentLength;
|
||||
}
|
||||
} else {
|
||||
pkcs8Key[offset++] = (byte)innerContentLength;
|
||||
}
|
||||
|
||||
// version = 0
|
||||
pkcs8Key[offset++] = 0x02;
|
||||
pkcs8Key[offset++] = 0x01;
|
||||
pkcs8Key[offset++] = 0x00;
|
||||
|
||||
// algorithmIdentifier
|
||||
System.arraycopy(algorithmIdentifier, 0, pkcs8Key, offset, algorithmIdentifier.length);
|
||||
offset += algorithmIdentifier.length;
|
||||
|
||||
// privateKey as OCTET STRING
|
||||
pkcs8Key[offset++] = 0x04;
|
||||
if (pkcs1Key.length > 127) {
|
||||
if (pkcs1Key.length > 255) {
|
||||
pkcs8Key[offset++] = (byte)0x82;
|
||||
pkcs8Key[offset++] = (byte)((pkcs1Key.length >> 8) & 0xFF);
|
||||
pkcs8Key[offset++] = (byte)(pkcs1Key.length & 0xFF);
|
||||
} else {
|
||||
pkcs8Key[offset++] = (byte)0x81;
|
||||
pkcs8Key[offset++] = (byte)pkcs1Key.length;
|
||||
}
|
||||
} else {
|
||||
pkcs8Key[offset++] = (byte)pkcs1Key.length;
|
||||
}
|
||||
|
||||
// 复制 PKCS#1 私钥
|
||||
System.arraycopy(pkcs1Key, 0, pkcs8Key, offset, pkcs1Key.length);
|
||||
|
||||
log.info("PKCS#1到PKCS#8转换完成,最终长度: {} bytes", pkcs8Key.length);
|
||||
return pkcs8Key;
|
||||
} catch (Exception e) {
|
||||
log.error("PKCS#1到PKCS#8转换失败", e);
|
||||
throw new RuntimeException("密钥格式转换失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 内部缓存类 ====================
|
||||
private static class PaymentInfo {
|
||||
String orderId;
|
||||
Long memberId;
|
||||
String orderType;
|
||||
String tradeType;
|
||||
String transAmt;
|
||||
String goodsDesc;
|
||||
String payStatus;
|
||||
String outTransId;
|
||||
String payUrl;
|
||||
String hfSeqId;
|
||||
LocalDateTime payTime;
|
||||
String remark;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
cn.novalon.gym.manage.payment.config.HuifuPayConfig
|
||||
@@ -53,6 +53,11 @@
|
||||
<artifactId>gym-dataCount</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-auth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
+1
-2
@@ -10,14 +10,13 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
|
||||
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
|
||||
ReactiveUserDetailsServiceAutoConfiguration.class })
|
||||
@EnableScheduling
|
||||
//@EnableScheduling
|
||||
@EnableR2dbcRepositories(basePackages = {
|
||||
"cn.novalon.gym.manage.db.dao",
|
||||
"cn.novalon.gym.manage.sys.audit.repository" ,
|
||||
|
||||
+12
-1
@@ -4,6 +4,7 @@ package cn.novalon.gym.manage.app.config;
|
||||
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
||||
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
||||
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
||||
import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
|
||||
@@ -76,7 +77,8 @@ public class SystemRouter {
|
||||
GroupCourseTypeHandler groupCourseTypeHandler,
|
||||
CourseLabelHandler courseLabelHandler,
|
||||
CheckInHandler checkInHandler,
|
||||
DataStatisticsHandler dataStatisticsHandler) {
|
||||
DataStatisticsHandler dataStatisticsHandler,
|
||||
PhoneAuthHandler phoneAuthHandler) {
|
||||
|
||||
return route()
|
||||
// ========== 诊断路由 ==========
|
||||
@@ -192,10 +194,13 @@ public class SystemRouter {
|
||||
|
||||
// ========== 消息路由 ==========
|
||||
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
|
||||
.GET("/api/messages/user/{userId}/page", messageHandler::getMessagesByUserPage)
|
||||
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
|
||||
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
|
||||
.GET("/api/messages/user/{userId}/unread/page", messageHandler::getUnreadMessagesPage)
|
||||
.POST("/api/messages", messageHandler::createMessage)
|
||||
.PUT("/api/messages/{id}/read", messageHandler::markAsRead)
|
||||
.PUT("/api/messages/user/{userId}/read", messageHandler::markAllAsRead)
|
||||
.DELETE("/api/messages/{id}", messageHandler::deleteMessage)
|
||||
|
||||
// ========== 文件路由 ==========
|
||||
@@ -218,6 +223,11 @@ public class SystemRouter {
|
||||
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
|
||||
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
|
||||
|
||||
// ========== 手机号认证路由 ==========
|
||||
.POST("/api/auth/phone/one-click-login", phoneAuthHandler::oneClickLogin)
|
||||
.POST("/api/auth/phone/send-code", phoneAuthHandler::sendSmsCode)
|
||||
.POST("/api/auth/phone/code-login", phoneAuthHandler::codeLogin)
|
||||
|
||||
// ========== 会员模块路由 - 微信认证 ==========
|
||||
.POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin)
|
||||
.GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature)
|
||||
@@ -349,6 +359,7 @@ public class SystemRouter {
|
||||
.GET("/api/datacount/signin", dataStatisticsHandler::getSignInStatistics)
|
||||
.GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics)
|
||||
.GET("/api/datacount/export", dataStatisticsHandler::exportStatistics)
|
||||
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ spring:
|
||||
cache:
|
||||
type: none
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
password: novalon123
|
||||
url: r2dbc:postgresql://localhost:5432/manage_system
|
||||
username: postgres
|
||||
password: 123456
|
||||
pool:
|
||||
initial-size: 5
|
||||
max-size: 20
|
||||
@@ -12,10 +12,10 @@ spring:
|
||||
max-life-time: 30m
|
||||
acquire-timeout: 3s
|
||||
flyway:
|
||||
url: jdbc:postgresql://localhost:55432/manage_system
|
||||
user: novalon
|
||||
password: novalon123
|
||||
enabled: true
|
||||
url: jdbc:postgresql://localhost:5432/manage_system
|
||||
user: postgres
|
||||
password: 123456
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
|
||||
@@ -4,8 +4,8 @@ spring:
|
||||
activate:
|
||||
on-profile: local
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
url: r2dbc:postgresql://localhost:5432/manage_system
|
||||
username: postgres
|
||||
password: 123456
|
||||
pool:
|
||||
initial-size: 5
|
||||
@@ -14,8 +14,8 @@ spring:
|
||||
max-life-time: 30m
|
||||
acquire-timeout: 3s
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
url: jdbc:postgresql://localhost:5432/manage_system
|
||||
username: postgres
|
||||
password: 123456
|
||||
driver-class-name: org.postgresql.Driver
|
||||
flyway:
|
||||
|
||||
@@ -5,9 +5,9 @@ spring:
|
||||
application:
|
||||
name: manage-app
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
password: novalon123
|
||||
url: r2dbc:postgresql://localhost:5432/manage_system
|
||||
username: postgres
|
||||
password: 123456
|
||||
pool:
|
||||
initial-size: 5
|
||||
max-size: 20
|
||||
|
||||
@@ -15,9 +15,9 @@ spring:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
|
||||
username: ${DB_USERNAME:novalon}
|
||||
password: ${DB_PASSWORD:novalon123}
|
||||
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:123456}
|
||||
pool:
|
||||
initial-size: 10
|
||||
max-size: 50
|
||||
@@ -25,12 +25,12 @@ spring:
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
|
||||
username: ${DB_USERNAME:novalon}
|
||||
password: ${DB_PASSWORD:novalon123}
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:123456}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
flyway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
@@ -54,7 +54,7 @@ spring:
|
||||
profiles:
|
||||
active: dev
|
||||
config:
|
||||
import: classpath:member-config.yml
|
||||
import: classpath:keys.properties,classpath:member-config.yml
|
||||
|
||||
|
||||
|
||||
@@ -94,3 +94,13 @@ springdoc:
|
||||
show-actuator: false
|
||||
default-consumes-media-type: application/json
|
||||
default-produces-media-type: application/json
|
||||
|
||||
alibaba:
|
||||
cloud:
|
||||
sms:
|
||||
access-key-id: ${ALIBABA_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${ALIBABA_ACCESS_KEY_SECRET:}
|
||||
sign-name: ${ALIBABA_SMS_SIGN_NAME:}
|
||||
template-code: ${ALIBABA_SMS_TEMPLATE_CODE:}
|
||||
code-length: 6
|
||||
code-expire-seconds: 300
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
huifu.private-key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCNUdQE59I4bV1dIgLs2IRN3Wl8KI2yS4KpTUQWrvvlUcBTztB8rIeN5lr6Yv4VsZ1TG0FAb0D80JjxEQ1+0HizYNOQ1h2935v2r8h7rx3VTLbMOkmsg8Lb8LQRTbPaJOZfsIZMzBytYiRzWunHaMlCr800EA3q5NMj9VjHQuamxKqyzyHqfkIjA/sZ8q9atVjn8ahUjPKdrGA8b79HexHhCgOSLdK+fWw0eMCsWWYP2qECLsvZ+tjfvqSBXx1kg7womwT1VBCf+0Dx+jJPKR3mxQfz2szoucJYuXRo55kA6yCwoeNsjanLDRkPBSy3NHdKrffP6YODhRHG6KHyayyNAgMBAAECggEATIR69TEETVNCEzRgOxe9A2AYRoa6ukhSdhMFA/c5IuCR747yqh7MwtNwfVRuWRazpZUDTr0uhfT4asad9QUx5YZO54RX1EAn9XkWZ4nY8G46J/iDfapWLrp09U2KTVpfdn5hKWH3QRX7wI4AON2O49HGnSL4NjAx9q1YpYOe2bqevxmB4uD9vR/FHMjZ2qWzPaL7pId98x4DCJCiZctBQqb7gxR8EseD1Ddn2HbH5DEUjoxz2umtL+zrcGKDFBy4kM3r+10FP5jFG4fJfZFQwrzMTEkXJriau9DMRTQ83rWHzNvhdTNUuyqah74ez5V1piVEOXCPudzFsMttD2DyzQKBgQDokQH3WtcQqwFTUo+uWVVup4NoQzNZpEGIHK17afRsf9YnkoJVqjhywq1dhQT+yuyW9geJJKWpHQIAS4gGudhNK+j6d//bbvvbkOmtsdWyZ6Z2EmY8JrPkHdHAcadNJX7vrIc1xPeyci4AHhErDHc3hhD5njuSLx3DUp4iODNxKwKBgQCbjyHvP90YGjh49DWklh2NJgR2+OR6Fd0afdmwtGKeCUP5l2To6rfLBbcSRIWCON4iLME5wLLdHcm5wAQukC4dsUB5iZffp21+7hrrwUw/G0MUzFudfhZkjsDU5i+FwnVMqmsLJPUsjJTOfKsJdA2DjnwfMcgxw9FeClGveSFNJwKBgQDCpjuDECDY7oeZeYyQXGzIxKOTbEtaR8Qha/83QCM3fHd9f35evK2qP45iq6bWqnkCkMEV4/pTZNf77zvWhU2oqYvBtxYKTwW1a8BphGJbg60rPZMb3TjLQLoB3B4uz6dCaqBwPH8kd7RQnNm5siFF84vZoLozTAQZKtj3wxorKQKBgQCGKnEONHqwaw0B5T7O8VoTfxKiug/07B6C1sCGk03rF/q0rkquSKK0S/2Vl9u+cOXFe+w7r2OVKjfuKRpyPpBHs7T0HiQLFhBuRVaat2DXnN/CdG8f6rvNhwHxnYanSwx4TxN7zShYf/doEEZEJP/y01ViYkFUCpvtC+FgAo0iSQKBgFrkJGxPo6T7i5ZF7VoZWdARnypfbOl4gF9GMrXykk0fnKBDTof9Bg34a8OX54cf4yEH1ifZzVtNyzPTH9FxV88TksWRouqi1Uo6///kJI6gXROspE91TpNtNGHPgLhebA83WlFtS/H7i5G6RO+6KO0uflSImfy3Owp/ChyFUGmn
|
||||
huifu.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjVHUBOfSOG1dXSIC7NiETd1pfCiNskuCqU1EFq775VHAU87QfKyHjeZa+mL+FbGdUxtBQG9A/NCY8RENftB4s2DTkNYdvd+b9q/Ie68d1Uy2zDpJrIPC2/C0EU2z2iTmX7CGTMwcrWIkc1rpx2jJQq/NNBAN6uTTI/VYx0LmpsSqss8h6n5CIwP7GfKvWrVY5/GoVIzynaxgPG+/R3sR4QoDki3Svn1sNHjArFlmD9qhAi7L2frY376kgV8dZIO8KJsE9VQQn/tA8foyTykd5sUH89rM6LnCWLl0aOeZAOsgsKHjbI2pyw0ZDwUstzR3Sq33z+mDg4URxuih8mssjQIDAQAB
|
||||
@@ -21,4 +21,17 @@ wechat:
|
||||
|
||||
spring:
|
||||
elasticsearch:
|
||||
uris: http://localhost:9200
|
||||
uris: http://localhost:9200
|
||||
|
||||
payment:
|
||||
huifu:
|
||||
sys-id: "6666000207573586"
|
||||
product-id: "XLSISV"
|
||||
huifu-id: "6666000207573586"
|
||||
acct-id: "F28226571"
|
||||
private-key: ${huifu.private-key}
|
||||
public-key: ${huifu.public-key}
|
||||
create-url: "https://api.huifu.com/v4/trade/payment/create"
|
||||
query-url: "https://api.huifu.com/v4/trade/payment/query"
|
||||
refund-url: "https://api.huifu.com/v4/trade/refund"
|
||||
notify-url: "http://localhost:8084/api/payment/notify"
|
||||
|
||||
+9
@@ -61,4 +61,13 @@ public final class RedisKeyConstants {
|
||||
* appType: miniapp(小程序), mp(公众号)
|
||||
*/
|
||||
public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:";
|
||||
|
||||
// ==================== 认证模块 ====================
|
||||
|
||||
/**
|
||||
* 手机短信验证码缓存
|
||||
* 格式:sms:code:{phone}
|
||||
* 用途:存储登录/注册等场景的短信验证码
|
||||
*/
|
||||
public static final String SMS_CODE = "sms:code:";
|
||||
}
|
||||
+4
@@ -7,6 +7,7 @@ public class ErrorCode {
|
||||
public static final String PERMISSION_PREFIX = "PERMISSION_";
|
||||
public static final String CONFLICT_PREFIX = "CONFLICT_";
|
||||
public static final String SYSTEM_PREFIX = "SYSTEM_";
|
||||
public static final String AUTH_PREFIX = "AUTH_";
|
||||
|
||||
public static final String VALIDATION_REQUIRED = VALIDATION_PREFIX + "001";
|
||||
public static final String VALIDATION_INVALID_FORMAT = VALIDATION_PREFIX + "002";
|
||||
@@ -29,4 +30,7 @@ public class ErrorCode {
|
||||
public static final String SYSTEM_INTERNAL_ERROR = SYSTEM_PREFIX + "001";
|
||||
public static final String SYSTEM_DATABASE_ERROR = SYSTEM_PREFIX + "002";
|
||||
public static final String SYSTEM_NETWORK_ERROR = SYSTEM_PREFIX + "003";
|
||||
|
||||
public static final String AUTH_PHONE_ERROR = AUTH_PREFIX + "001";
|
||||
public static final String AUTH_CODE_ERROR = AUTH_PREFIX + "002";
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.db.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
@@ -21,10 +22,10 @@ public class SysUserMessageEntity {
|
||||
@Column("user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column("title")
|
||||
@Column("message_title")
|
||||
private String title;
|
||||
|
||||
@Column("content")
|
||||
@Column("message_content")
|
||||
private String content;
|
||||
|
||||
@Column("message_type")
|
||||
@@ -33,9 +34,13 @@ public class SysUserMessageEntity {
|
||||
@Column("is_read")
|
||||
private String isRead;
|
||||
|
||||
@Column("create_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Column("created_at")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -91,4 +96,12 @@ public class SysUserMessageEntity {
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -1,5 +1,7 @@
|
||||
package cn.novalon.gym.manage.db.repository;
|
||||
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||
import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository;
|
||||
import cn.novalon.gym.manage.db.converter.SysUserMessageConverter;
|
||||
@@ -13,6 +15,8 @@ import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户消息仓储实现类
|
||||
*
|
||||
@@ -75,6 +79,87 @@ public class SysUserMessageRepository implements ISysUserMessageRepository {
|
||||
return r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PageResponse<SysUserMessage>> findByUserIdPage(Long userId, PageRequest pageRequest) {
|
||||
int page = pageRequest.getPage();
|
||||
int size = pageRequest.getSize();
|
||||
String sort = pageRequest.getSort();
|
||||
String order = pageRequest.getOrder();
|
||||
|
||||
Sort sortObj = Sort.unsorted();
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
|
||||
}
|
||||
|
||||
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj);
|
||||
|
||||
SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria();
|
||||
criteria.setUserId(userId);
|
||||
org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria);
|
||||
|
||||
return r2dbcEntityTemplate.select(SysUserMessageEntity.class)
|
||||
.matching(dbQuery.with(pageable))
|
||||
.all()
|
||||
.collectList()
|
||||
.zipWith(r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class))
|
||||
.map(tuple -> {
|
||||
long total = tuple.getT2();
|
||||
int totalPages = (int) Math.ceil((double) total / size);
|
||||
List<SysUserMessage> messageList = tuple.getT1().stream()
|
||||
.map(sysUserMessageConverter::toDomain)
|
||||
.toList();
|
||||
return new PageResponse<>(messageList, totalPages, total, page, size);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PageResponse<SysUserMessage>> findByUserIdAndIsReadPage(Long userId, String isRead, PageRequest pageRequest) {
|
||||
int page = pageRequest.getPage();
|
||||
int size = pageRequest.getSize();
|
||||
String sort = pageRequest.getSort();
|
||||
String order = pageRequest.getOrder();
|
||||
|
||||
Sort sortObj = Sort.unsorted();
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
|
||||
}
|
||||
|
||||
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj);
|
||||
|
||||
SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria();
|
||||
criteria.setUserId(userId);
|
||||
criteria.setIsRead(isRead);
|
||||
org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria);
|
||||
|
||||
return r2dbcEntityTemplate.select(SysUserMessageEntity.class)
|
||||
.matching(dbQuery.with(pageable))
|
||||
.all()
|
||||
.collectList()
|
||||
.zipWith(r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class))
|
||||
.map(tuple -> {
|
||||
long total = tuple.getT2();
|
||||
int totalPages = (int) Math.ceil((double) total / size);
|
||||
List<SysUserMessage> messageList = tuple.getT1().stream()
|
||||
.map(sysUserMessageConverter::toDomain)
|
||||
.toList();
|
||||
return new PageResponse<>(messageList, totalPages, total, page, size);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> markAllAsReadByUserId(Long userId) {
|
||||
org.springframework.data.relational.core.query.Update update = org.springframework.data.relational.core.query.Update.update("is_read", "1");
|
||||
|
||||
org.springframework.data.relational.core.query.Query query = org.springframework.data.relational.core.query.Query.query(
|
||||
org.springframework.data.relational.core.query.Criteria.where("user_id").is(userId)
|
||||
.and("deleted_at").isNull()
|
||||
);
|
||||
|
||||
return r2dbcEntityTemplate.update(SysUserMessageEntity.class)
|
||||
.matching(query)
|
||||
.apply(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUserMessage> save(SysUserMessage message) {
|
||||
SysUserMessageEntity entity = sysUserMessageConverter.toEntity(message);
|
||||
|
||||
+3
-1
@@ -61,7 +61,9 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
||||
path.equals("/api/member/auth/miniapp/login") ||
|
||||
path.equals("/api/member/auth/mp/callback") ||
|
||||
path.equals("/api/auth/login") ||
|
||||
path.startsWith("/api/checkIn/") ||
|
||||
path.equals("/api/groupCourse/page") ||
|
||||
path.startsWith("/api/checkIn") ||
|
||||
path.startsWith("/api/payment") ||
|
||||
path.startsWith("/actuator/info");
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -1,5 +1,7 @@
|
||||
package cn.novalon.gym.manage.notify.core.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysUserMessage {
|
||||
@@ -10,6 +12,8 @@ public class SysUserMessage {
|
||||
private String content;
|
||||
private String messageType;
|
||||
private String isRead;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
+8
@@ -1,5 +1,7 @@
|
||||
package cn.novalon.gym.manage.notify.core.repository;
|
||||
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -12,6 +14,12 @@ public interface ISysUserMessageRepository {
|
||||
|
||||
Mono<Long> countByUserIdAndIsRead(Long userId, String isRead);
|
||||
|
||||
Mono<PageResponse<SysUserMessage>> findByUserIdPage(Long userId, PageRequest pageRequest);
|
||||
|
||||
Mono<PageResponse<SysUserMessage>> findByUserIdAndIsReadPage(Long userId, String isRead, PageRequest pageRequest);
|
||||
|
||||
Mono<Long> markAllAsReadByUserId(Long userId);
|
||||
|
||||
Mono<SysUserMessage> save(SysUserMessage message);
|
||||
|
||||
Mono<SysUserMessage> findById(Long id);
|
||||
|
||||
+8
@@ -1,5 +1,7 @@
|
||||
package cn.novalon.gym.manage.notify.core.service;
|
||||
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -12,6 +14,12 @@ public interface ISysUserMessageService {
|
||||
|
||||
Flux<SysUserMessage> getUnreadMessages(Long userId);
|
||||
|
||||
Mono<PageResponse<SysUserMessage>> getMessagesByUserPage(Long userId, PageRequest pageRequest);
|
||||
|
||||
Mono<PageResponse<SysUserMessage>> getUnreadMessagesPage(Long userId, PageRequest pageRequest);
|
||||
|
||||
Mono<Long> markAllAsRead(Long userId);
|
||||
|
||||
Mono<SysUserMessage> createMessage(SysUserMessage message);
|
||||
|
||||
Mono<SysUserMessage> markAsRead(Long id);
|
||||
|
||||
+17
@@ -1,5 +1,7 @@
|
||||
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||
import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository;
|
||||
import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService;
|
||||
@@ -33,6 +35,16 @@ public class SysUserMessageServiceImpl implements ISysUserMessageService {
|
||||
return messageRepository.findByUserIdAndIsReadOrderByCreateTimeDesc(userId, "0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PageResponse<SysUserMessage>> getMessagesByUserPage(Long userId, PageRequest pageRequest) {
|
||||
return messageRepository.findByUserIdPage(userId, pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PageResponse<SysUserMessage>> getUnreadMessagesPage(Long userId, PageRequest pageRequest) {
|
||||
return messageRepository.findByUserIdAndIsReadPage(userId, "0", pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUserMessage> createMessage(SysUserMessage message) {
|
||||
message.setCreateTime(LocalDateTime.now());
|
||||
@@ -40,6 +52,11 @@ public class SysUserMessageServiceImpl implements ISysUserMessageService {
|
||||
return messageRepository.save(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> markAllAsRead(Long userId) {
|
||||
return messageRepository.markAllAsReadByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUserMessage> markAsRead(Long id) {
|
||||
return messageRepository.findById(id)
|
||||
|
||||
+43
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.notify.handler;
|
||||
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
|
||||
import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -23,6 +24,24 @@ public class SysUserMessageHandler {
|
||||
return ServerResponse.ok().body(messages, SysUserMessage.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getMessagesByUserPage(ServerRequest request) {
|
||||
Long userId = Long.parseLong(request.pathVariable("userId"));
|
||||
|
||||
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
|
||||
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
|
||||
String sort = request.queryParam("sort").orElse("createTime");
|
||||
String order = request.queryParam("order").orElse("desc");
|
||||
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(page);
|
||||
pageRequest.setSize(size);
|
||||
pageRequest.setSort(sort);
|
||||
pageRequest.setOrder(order);
|
||||
|
||||
return messageService.getMessagesByUserPage(userId, pageRequest)
|
||||
.flatMap(pageResponse -> ServerResponse.ok().bodyValue(pageResponse));
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getUnreadCount(ServerRequest request) {
|
||||
Long userId = Long.parseLong(request.pathVariable("userId"));
|
||||
return messageService.getUnreadCount(userId)
|
||||
@@ -35,6 +54,24 @@ public class SysUserMessageHandler {
|
||||
return ServerResponse.ok().body(messages, SysUserMessage.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getUnreadMessagesPage(ServerRequest request) {
|
||||
Long userId = Long.parseLong(request.pathVariable("userId"));
|
||||
|
||||
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
|
||||
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
|
||||
String sort = request.queryParam("sort").orElse("createTime");
|
||||
String order = request.queryParam("order").orElse("desc");
|
||||
|
||||
PageRequest pageRequest = new PageRequest();
|
||||
pageRequest.setPage(page);
|
||||
pageRequest.setSize(size);
|
||||
pageRequest.setSort(sort);
|
||||
pageRequest.setOrder(order);
|
||||
|
||||
return messageService.getUnreadMessagesPage(userId, pageRequest)
|
||||
.flatMap(pageResponse -> ServerResponse.ok().bodyValue(pageResponse));
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createMessage(ServerRequest request) {
|
||||
return request.bodyToMono(SysUserMessage.class)
|
||||
.flatMap(messageService::createMessage)
|
||||
@@ -48,6 +85,12 @@ public class SysUserMessageHandler {
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> markAllAsRead(ServerRequest request) {
|
||||
Long userId = Long.parseLong(request.pathVariable("userId"));
|
||||
return messageService.markAllAsRead(userId)
|
||||
.flatMap(count -> ServerResponse.ok().bodyValue(count));
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> deleteMessage(ServerRequest request) {
|
||||
Long id = Long.parseLong(request.pathVariable("id"));
|
||||
return messageService.deleteMessage(id)
|
||||
|
||||
+7
-1
@@ -58,7 +58,13 @@ public class SecurityConfig {
|
||||
.pathMatchers("/api/member-card-records/**").permitAll()
|
||||
.pathMatchers("/**").permitAll()
|
||||
.pathMatchers("/api/member-card-transactions/**").permitAll()
|
||||
.pathMatchers("/api/checkIn/**").permitAll();
|
||||
.pathMatchers("/api/groupCourse/page").permitAll()
|
||||
.pathMatchers("/api/checkIn/**").permitAll()
|
||||
.pathMatchers("/api/payment/**").permitAll()
|
||||
.pathMatchers("/api/payment/create").permitAll()
|
||||
.pathMatchers("/api/payment/query").permitAll()
|
||||
.pathMatchers("/api/payment/notify").permitAll()
|
||||
.pathMatchers("/api/payment/refund").permitAll();
|
||||
|
||||
|
||||
if (isDevOrTest) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.novalon.gym.manage.sys;
|
||||
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
import cn.novalon.gym.manage.common.config.JwtProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TokenGenerator {
|
||||
|
||||
@Test
|
||||
public void generateTokenForUser1_Dev() {
|
||||
// dev环境secret
|
||||
JwtProperties jwtProperties = new JwtProperties();
|
||||
jwtProperties.setSecret("novalon-gym-manage-jwt-secret-key-for-development-only-2026");
|
||||
JwtTokenProvider provider = new JwtTokenProvider(jwtProperties);
|
||||
|
||||
String token = provider.generateToken("admin", 1L);
|
||||
System.out.println("【Dev环境】Token for userId=1:");
|
||||
System.out.println(token);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateTokenForUser1_Local() {
|
||||
// local环境secret
|
||||
JwtProperties jwtProperties = new JwtProperties();
|
||||
jwtProperties.setSecret("U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4");
|
||||
JwtTokenProvider provider = new JwtTokenProvider(jwtProperties);
|
||||
|
||||
String token = provider.generateToken("admin", 1L);
|
||||
System.out.println("【Local环境】Token for userId=1:");
|
||||
System.out.println(token);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateTokenForUser1_Default() {
|
||||
// 默认环境secret
|
||||
JwtProperties jwtProperties = new JwtProperties();
|
||||
jwtProperties.setSecret("U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4");
|
||||
JwtTokenProvider provider = new JwtTokenProvider(jwtProperties);
|
||||
|
||||
String token = provider.generateToken("admin", 1L);
|
||||
System.out.println("【默认环境】Token for userId=1:");
|
||||
System.out.println(token);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@
|
||||
<module>gym-groupCourse</module>
|
||||
<module>gym-checkIn</module>
|
||||
<module>gym-dataCount</module>
|
||||
<module>gym-payment</module>
|
||||
<module>gym-auth</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Manifest-Version: 1.0
|
||||
Created-By: Maven JAR Plugin 3.4.2
|
||||
Build-Jdk-Spec: 21
|
||||
Implementation-Title: Gym Payment
|
||||
Implementation-Version: 1.0.0
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
artifactId=gym-payment
|
||||
groupId=cn.novalon.gym.manage
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gym-payment</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Gym Payment</name>
|
||||
<description>Payment Module - Integrates Huifu Payment Gateway</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</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.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+1
@@ -0,0 +1 @@
|
||||
cn.novalon.gym.manage.payment.config.HuifuPayConfig
|
||||
Submodule
+1
Submodule vue added at bc48e695cb
Reference in New Issue
Block a user