diff --git a/fund-common/pom.xml b/fund-common/pom.xml
index f12dd62..31000ce 100644
--- a/fund-common/pom.xml
+++ b/fund-common/pom.xml
@@ -49,6 +49,13 @@
com.fasterxml.jackson.core
jackson-databind
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
diff --git a/fund-common/src/main/java/com/fundplatform/common/util/AESUtils.java b/fund-common/src/main/java/com/fundplatform/common/util/AESUtils.java
new file mode 100644
index 0000000..3111205
--- /dev/null
+++ b/fund-common/src/main/java/com/fundplatform/common/util/AESUtils.java
@@ -0,0 +1,201 @@
+package com.fundplatform.common.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * AES 加密工具类
+ * 用于敏感数据加密存储(手机号、身份证号等)
+ */
+public class AESUtils {
+
+ private static final Logger log = LoggerFactory.getLogger(AESUtils.class);
+
+ private static final String ALGORITHM = "AES";
+ private static final String TRANSFORMATION = "AES/GCM/NoPadding";
+ private static final int KEY_SIZE = 256; // AES-256
+ private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度
+ private static final int GCM_TAG_LENGTH = 128; // 认证标签长度(位)
+
+ // 默认密钥种子(生产环境应从配置中心或密钥管理服务获取)
+ // 通过SHA-256生成32字节的密钥
+ private static final String DEFAULT_KEY_SEED = "FundPlatform2026SecretKey!@#";
+ private static final byte[] DEFAULT_KEY;
+
+ static {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ DEFAULT_KEY = digest.digest(DEFAULT_KEY_SEED.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new RuntimeException("初始化默认密钥失败", e);
+ }
+ }
+
+ /**
+ * 生成AES密钥
+ */
+ public static String generateKey() {
+ try {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
+ keyGenerator.init(KEY_SIZE, new SecureRandom());
+ SecretKey secretKey = keyGenerator.generateKey();
+ return Base64.getEncoder().encodeToString(secretKey.getEncoded());
+ } catch (Exception e) {
+ log.error("生成AES密钥失败", e);
+ throw new RuntimeException("生成AES密钥失败", e);
+ }
+ }
+
+ /**
+ * 加密
+ * @param plainText 明文
+ * @return Base64编码的密文(包含IV)
+ */
+ public static String encrypt(String plainText) {
+ return encryptWithKey(plainText, DEFAULT_KEY);
+ }
+
+ /**
+ * 加密
+ * @param plainText 明文
+ * @param key 密钥(Base64编码)
+ * @return Base64编码的密文(包含IV)
+ */
+ public static String encrypt(String plainText, String key) {
+ try {
+ byte[] keyBytes = Base64.getDecoder().decode(key);
+ return encryptWithKey(plainText, keyBytes);
+ } catch (Exception e) {
+ log.error("解析密钥失败", e);
+ throw new RuntimeException("解析密钥失败", e);
+ }
+ }
+
+ /**
+ * 内部加密方法
+ */
+ private static String encryptWithKey(String plainText, byte[] keyBytes) {
+ try {
+ SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
+
+ // 生成随机IV
+ byte[] iv = new byte[GCM_IV_LENGTH];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(iv);
+
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
+
+ byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+
+ // 将IV和密文合并
+ byte[] encryptedWithIv = new byte[GCM_IV_LENGTH + encrypted.length];
+ System.arraycopy(iv, 0, encryptedWithIv, 0, GCM_IV_LENGTH);
+ System.arraycopy(encrypted, 0, encryptedWithIv, GCM_IV_LENGTH, encrypted.length);
+
+ return Base64.getEncoder().encodeToString(encryptedWithIv);
+ } catch (Exception e) {
+ log.error("AES加密失败", e);
+ throw new RuntimeException("AES加密失败", e);
+ }
+ }
+
+ /**
+ * 解密
+ * @param encryptedText Base64编码的密文(包含IV)
+ * @return 明文
+ */
+ public static String decrypt(String encryptedText) {
+ return decryptWithKey(encryptedText, DEFAULT_KEY);
+ }
+
+ /**
+ * 解密
+ * @param encryptedText Base64编码的密文(包含IV)
+ * @param key 密钥(Base64编码)
+ * @return 明文
+ */
+ public static String decrypt(String encryptedText, String key) {
+ try {
+ byte[] keyBytes = Base64.getDecoder().decode(key);
+ return decryptWithKey(encryptedText, keyBytes);
+ } catch (Exception e) {
+ log.error("解析密钥失败", e);
+ throw new RuntimeException("解析密钥失败", e);
+ }
+ }
+
+ /**
+ * 内部解密方法
+ */
+ private static String decryptWithKey(String encryptedText, byte[] keyBytes) {
+ try {
+ SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
+
+ byte[] decoded = Base64.getDecoder().decode(encryptedText);
+
+ // 分离IV和密文
+ byte[] iv = new byte[GCM_IV_LENGTH];
+ byte[] encrypted = new byte[decoded.length - GCM_IV_LENGTH];
+ System.arraycopy(decoded, 0, iv, 0, GCM_IV_LENGTH);
+ System.arraycopy(decoded, GCM_IV_LENGTH, encrypted, 0, encrypted.length);
+
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
+
+ byte[] decrypted = cipher.doFinal(encrypted);
+ return new String(decrypted, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ log.error("AES解密失败", e);
+ throw new RuntimeException("AES解密失败", e);
+ }
+ }
+
+ /**
+ * 手机号脱敏
+ * @param phone 手机号
+ * @return 脱敏后的手机号 (如: 138****8000)
+ */
+ public static String maskPhone(String phone) {
+ if (phone == null || phone.length() < 7) {
+ return phone;
+ }
+ return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+ }
+
+ /**
+ * 身份证号脱敏
+ * @param idCard 身份证号
+ * @return 脱敏后的身份证号 (如: 110***********1234)
+ */
+ public static String maskIdCard(String idCard) {
+ if (idCard == null || idCard.length() < 8) {
+ return idCard;
+ }
+ return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
+ }
+
+ /**
+ * 银行卡号脱敏
+ * @param bankCard 银行卡号
+ * @return 脱敏后的银行卡号 (如: 6222****1234)
+ */
+ public static String maskBankCard(String bankCard) {
+ if (bankCard == null || bankCard.length() < 8) {
+ return bankCard;
+ }
+ return bankCard.substring(0, 4) + "****" + bankCard.substring(bankCard.length() - 4);
+ }
+}
diff --git a/fund-common/src/test/java/com/fundplatform/common/util/AESUtilsTest.java b/fund-common/src/test/java/com/fundplatform/common/util/AESUtilsTest.java
new file mode 100644
index 0000000..a075e36
--- /dev/null
+++ b/fund-common/src/test/java/com/fundplatform/common/util/AESUtilsTest.java
@@ -0,0 +1,161 @@
+package com.fundplatform.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * AES加密工具类单元测试
+ */
+class AESUtilsTest {
+
+ @Test
+ void testGenerateKey() {
+ String key = AESUtils.generateKey();
+ assertNotNull(key);
+ assertTrue(key.length() > 0);
+ System.out.println("生成的密钥: " + key);
+ }
+
+ @Test
+ void testEncryptAndDecrypt() {
+ String plainText = "13800138000";
+
+ // 加密
+ String encrypted = AESUtils.encrypt(plainText);
+ assertNotNull(encrypted);
+ assertNotEquals(plainText, encrypted);
+ System.out.println("明文: " + plainText);
+ System.out.println("密文: " + encrypted);
+
+ // 解密
+ String decrypted = AESUtils.decrypt(encrypted);
+ assertEquals(plainText, decrypted);
+ System.out.println("解密后: " + decrypted);
+ }
+
+ @Test
+ void testEncryptAndDecryptWithCustomKey() {
+ String plainText = "320123199001011234";
+ String key = AESUtils.generateKey();
+
+ // 使用自定义密钥加密
+ String encrypted = AESUtils.encrypt(plainText, key);
+ assertNotNull(encrypted);
+
+ // 使用自定义密钥解密
+ String decrypted = AESUtils.decrypt(encrypted, key);
+ assertEquals(plainText, decrypted);
+ System.out.println("身份证加密测试通过: " + plainText + " -> " + encrypted + " -> " + decrypted);
+ }
+
+ @Test
+ void testEncryptAndDecryptChinese() {
+ String plainText = "张三丰";
+
+ String encrypted = AESUtils.encrypt(plainText);
+ String decrypted = AESUtils.decrypt(encrypted);
+
+ assertEquals(plainText, decrypted);
+ System.out.println("中文加密测试通过: " + plainText + " -> " + encrypted + " -> " + decrypted);
+ }
+
+ @Test
+ void testEncryptAndDecryptLongText() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 1000; i++) {
+ sb.append("测试数据");
+ }
+ String plainText = sb.toString();
+
+ String encrypted = AESUtils.encrypt(plainText);
+ String decrypted = AESUtils.decrypt(encrypted);
+
+ assertEquals(plainText, decrypted);
+ System.out.println("长文本加密测试通过, 明文长度: " + plainText.length() + ", 密文长度: " + encrypted.length());
+ }
+
+ @Test
+ void testMaskPhone() {
+ // 正常手机号(11位)
+ assertEquals("138****8000", AESUtils.maskPhone("13800138000"));
+
+ // 短号码(小于7位)不脱敏
+ assertEquals("123456", AESUtils.maskPhone("123456"));
+
+ // null
+ assertNull(AESUtils.maskPhone(null));
+
+ // 7位号码(前3位+****+后4位)
+ assertEquals("123****4567", AESUtils.maskPhone("1234567"));
+
+ System.out.println("手机号脱敏测试通过: 13800138000 -> " + AESUtils.maskPhone("13800138000"));
+ }
+
+ @Test
+ void testMaskIdCard() {
+ // 正常身份证号(18位)
+ assertEquals("320***********1234", AESUtils.maskIdCard("320123199001011234"));
+
+ // 短号码(小于8位)直接返回原值
+ assertEquals("1234567", AESUtils.maskIdCard("1234567"));
+
+ // null
+ assertNull(AESUtils.maskIdCard(null));
+
+ System.out.println("身份证脱敏测试通过: 320123199001011234 -> " + AESUtils.maskIdCard("320123199001011234"));
+ }
+
+ @Test
+ void testMaskBankCard() {
+ // 正常银行卡号
+ assertEquals("6222****1234", AESUtils.maskBankCard("62220212345678901234"));
+
+ // 短卡号(小于8位)直接返回原值
+ assertEquals("1234567", AESUtils.maskBankCard("1234567"));
+
+ // null
+ assertNull(AESUtils.maskBankCard(null));
+
+ // 8位卡号(前4位+****+后4位)
+ assertEquals("1234****5678", AESUtils.maskBankCard("12345678"));
+
+ System.out.println("银行卡脱敏测试通过: 62220212345678901234 -> " + AESUtils.maskBankCard("62220212345678901234"));
+ }
+
+ @Test
+ void testDecryptWithWrongKey() {
+ String plainText = "敏感数据";
+ String key1 = AESUtils.generateKey();
+ String key2 = AESUtils.generateKey();
+
+ String encrypted = AESUtils.encrypt(plainText, key1);
+
+ // 使用错误的密钥解密应该失败
+ assertThrows(RuntimeException.class, () -> {
+ AESUtils.decrypt(encrypted, key2);
+ });
+
+ System.out.println("错误密钥解密测试通过: 使用错误密钥解密时抛出异常");
+ }
+
+ @Test
+ void testMultipleEncryptionsAreDifferent() {
+ String plainText = "相同的数据";
+
+ // 多次加密同一明文,密文应该不同(因为使用了随机IV)
+ String encrypted1 = AESUtils.encrypt(plainText);
+ String encrypted2 = AESUtils.encrypt(plainText);
+ String encrypted3 = AESUtils.encrypt(plainText);
+
+ assertNotEquals(encrypted1, encrypted2);
+ assertNotEquals(encrypted2, encrypted3);
+
+ // 但都能正确解密
+ assertEquals(plainText, AESUtils.decrypt(encrypted1));
+ assertEquals(plainText, AESUtils.decrypt(encrypted2));
+ assertEquals(plainText, AESUtils.decrypt(encrypted3));
+
+ System.out.println("多次加密生成不同密文测试通过");
+ }
+}
diff --git a/fund-gateway/pom.xml b/fund-gateway/pom.xml
index cfeac27..9a72224 100644
--- a/fund-gateway/pom.xml
+++ b/fund-gateway/pom.xml
@@ -59,6 +59,16 @@
org.springframework.boot
spring-boot-starter-data-redis-reactive
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-sentinel
+
+
+ com.alibaba.cloud
+ spring-cloud-alibaba-sentinel-gateway
+
diff --git a/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelConfig.java b/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelConfig.java
new file mode 100644
index 0000000..0c897d5
--- /dev/null
+++ b/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelConfig.java
@@ -0,0 +1,47 @@
+package com.fundplatform.gateway.config;
+
+import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Sentinel 熔断降级配置
+ */
+@Configuration
+public class SentinelConfig {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @PostConstruct
+ public void init() {
+ // 自定义熔断降级响应
+ GatewayCallbackManager.setBlockHandler((exchange, t) -> {
+ Map result = new HashMap<>();
+ result.put("code", 429);
+ result.put("message", "服务繁忙,请稍后重试");
+ result.put("success", false);
+ result.put("data", null);
+
+ String body;
+ try {
+ body = objectMapper.writeValueAsString(result);
+ } catch (JsonProcessingException e) {
+ body = "{\"code\":429,\"message\":\"服务繁忙,请稍后重试\",\"success\":false}";
+ }
+
+ return ServerResponse
+ .status(HttpStatus.TOO_MANY_REQUESTS)
+ .contentType(MediaType.APPLICATION_JSON)
+ .bodyValue(body);
+ });
+ }
+}
diff --git a/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelRuleConfig.java b/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelRuleConfig.java
new file mode 100644
index 0000000..dbc4b8e
--- /dev/null
+++ b/fund-gateway/src/main/java/com/fundplatform/gateway/config/SentinelRuleConfig.java
@@ -0,0 +1,66 @@
+package com.fundplatform.gateway.config;
+
+import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
+import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
+import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
+import jakarta.annotation.PostConstruct;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Sentinel 网关流控规则配置
+ */
+@Configuration
+public class SentinelRuleConfig {
+
+ @PostConstruct
+ public void initRules() {
+ Set rules = new HashSet<>();
+
+ // fund-sys服务流控规则
+ rules.add(new GatewayFlowRule("fund-sys")
+ .setCount(100) // QPS阈值
+ .setIntervalSec(1) // 统计窗口
+ .setBurst(20) // 应对突发请求额外容量
+ );
+
+ // fund-req服务流控规则
+ rules.add(new GatewayFlowRule("fund-req")
+ .setCount(50)
+ .setIntervalSec(1)
+ .setBurst(10)
+ );
+
+ // fund-exp服务流控规则
+ rules.add(new GatewayFlowRule("fund-exp")
+ .setCount(50)
+ .setIntervalSec(1)
+ .setBurst(10)
+ );
+
+ // fund-receipt服务流控规则
+ rules.add(new GatewayFlowRule("fund-receipt")
+ .setCount(50)
+ .setIntervalSec(1)
+ .setBurst(10)
+ );
+
+ // 加载规则
+ GatewayRuleManager.loadRules(rules);
+ }
+
+ /**
+ * Sentinel Gateway 过滤器
+ */
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ public GlobalFilter sentinelGatewayFilter() {
+ return new SentinelGatewayFilter();
+ }
+}
diff --git a/fund-gateway/src/main/resources/application.yml b/fund-gateway/src/main/resources/application.yml
index 39a60cf..aaea386 100644
--- a/fund-gateway/src/main/resources/application.yml
+++ b/fund-gateway/src/main/resources/application.yml
@@ -16,6 +16,22 @@ spring:
cloud:
compatibility-verifier:
enabled: false
+
+ # Sentinel配置
+ sentinel:
+ transport:
+ dashboard: localhost:8080 # Sentinel Dashboard地址(可选)
+ port: 8719 # Sentinel客户端端口
+ eager: true # 服务启动时立即初始化
+ datasource:
+ # 从Nacos读取规则(可选)
+ # ds1:
+ # nacos:
+ # server-addr: localhost:8848
+ # data-id: sentinel-gateway-rules
+ # group-id: DEFAULT_GROUP
+ # rule-type: gw-flow
+
gateway:
# 默认限流配置
default-filters:
diff --git a/fund-sys/src/main/java/com/fundplatform/sys/config/HikariMonitorConfig.java b/fund-sys/src/main/java/com/fundplatform/sys/config/HikariMonitorConfig.java
new file mode 100644
index 0000000..8e69d11
--- /dev/null
+++ b/fund-sys/src/main/java/com/fundplatform/sys/config/HikariMonitorConfig.java
@@ -0,0 +1,45 @@
+package com.fundplatform.sys.config;
+
+import com.zaxxer.hikari.HikariConfigMXBean;
+import com.zaxxer.hikari.HikariDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import javax.sql.DataSource;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HikariCP 连接池监控配置
+ */
+@Configuration
+@EnableScheduling
+public class HikariMonitorConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(HikariMonitorConfig.class);
+
+ @Autowired
+ private DataSource dataSource;
+
+ /**
+ * 每5分钟打印一次连接池状态
+ */
+ @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
+ public void monitorHikariPool() {
+ if (dataSource instanceof HikariDataSource) {
+ HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
+ HikariConfigMXBean config = hikariDataSource.getHikariConfigMXBean();
+
+ log.info("=== HikariCP 连接池状态 ===");
+ log.info("连接池名称: {}", config.getPoolName());
+ log.info("活跃连接数: {}", hikariDataSource.getHikariPoolMXBean().getActiveConnections());
+ log.info("空闲连接数: {}", hikariDataSource.getHikariPoolMXBean().getIdleConnections());
+ log.info("等待获取连接的线程数: {}", hikariDataSource.getHikariPoolMXBean().getThreadsAwaitingConnection());
+ log.info("最大连接数: {}", config.getMaximumPoolSize());
+ log.info("最小空闲连接数: {}", config.getMinimumIdle());
+ }
+ }
+}
diff --git a/fund-sys/src/main/resources/application.yml b/fund-sys/src/main/resources/application.yml
index 2ce6595..6081bff 100644
--- a/fund-sys/src/main/resources/application.yml
+++ b/fund-sys/src/main/resources/application.yml
@@ -18,9 +18,20 @@ spring:
username: root
password: zjf@123456
hikari:
- maximum-pool-size: 10
- minimum-idle: 5
- connection-timeout: 30000
+ # 连接池大小配置
+ maximum-pool-size: 20 # 最大连接数(建议: CPU核心数*2 + 有效磁盘数)
+ minimum-idle: 5 # 最小空闲连接数
+ # 连接超时配置
+ connection-timeout: 30000 # 连接超时时间(毫秒)
+ idle-timeout: 600000 # 空闲连接超时时间(10分钟)
+ max-lifetime: 1800000 # 连接最大存活时间(30分钟)
+ # 连接验证配置
+ validation-timeout: 5000 # 连接验证超时时间
+ leak-detection-threshold: 60000 # 连接泄露检测阈值(60秒)
+ # 连接池名称
+ pool-name: FundSysHikariPool
+ # 连接初始化SQL
+ connection-init-sql: SELECT 1
# Redis配置
data: