diff --git a/pom.xml b/pom.xml
index 1ae5f43..0f794b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
33.0.0-jre
5.8.26
3.12.0
+ 2.0.0
@@ -168,6 +169,13 @@
commons-lang3
${commons-lang3.version}
+
+
+
+ com.aliyun
+ dypnsapi20170525
+ ${dypnsapi.version}
+
diff --git a/xiaoyi-auth/pom.xml b/xiaoyi-auth/pom.xml
index 94d3c06..dc79e32 100644
--- a/xiaoyi-auth/pom.xml
+++ b/xiaoyi-auth/pom.xml
@@ -76,6 +76,12 @@
org.apache.commons
commons-pool2
+
+
+
+ com.aliyun
+ dypnsapi20170525
+
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/constant/RedisKeyConstants.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/constant/RedisKeyConstants.java
index c28763b..2f60fc2 100644
--- a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/constant/RedisKeyConstants.java
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/constant/RedisKeyConstants.java
@@ -15,7 +15,10 @@ public class RedisKeyConstants {
* 验证码 KEY 前缀
*/
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
-
+ /**
+ * 验证码 KEY 过期时间 (分钟)
+ */
+ public static final long VERIFICATION_CODE_EXPIRE_TIME = 3;
/**
* 构建验证码 KEY
*
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/enums/ResponseCodeEnum.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/enums/ResponseCodeEnum.java
index 37fe4b0..096307b 100644
--- a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/enums/ResponseCodeEnum.java
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/enums/ResponseCodeEnum.java
@@ -2,8 +2,6 @@ package top.crushtj.xiaoyishu.auth.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
-import java.lang.String;
-
import top.crushtj.framework.common.exception.BaseExceptionInterface;
/**
@@ -18,9 +16,10 @@ import top.crushtj.framework.common.exception.BaseExceptionInterface;
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// -------- 通用异常状态码 --------
- SYSTEM_ERROR("AUTH-10000", "系统错误"),
- PARAM_NOT_VALID("AUTH-10001", "参数错误"),
- VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试");
+ SYSTEM_ERROR("AUTH-10000", "系统错误"), PARAM_NOT_VALID("AUTH-10001", "参数错误"), // -------- 验证码异常状态码 --------
+ VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"),
+ SMS_SEND_FAILED("AUTH-20001", "短信发送失败,请稍后再试"),
+ SMS_SEND_TIMEOUT("AUTH-20002", "短信发送超时,请稍后再试");
/**
* 错误码
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/model/vo/verificationcode/SendVerificationCodeReqVO.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/model/vo/verificationcode/SendVerificationCodeReqVO.java
index 80c727b..19f022b 100644
--- a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/model/vo/verificationcode/SendVerificationCodeReqVO.java
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/model/vo/verificationcode/SendVerificationCodeReqVO.java
@@ -25,6 +25,6 @@ public class SendVerificationCodeReqVO {
* 手机号
*/
@NotBlank(message = "手机号不能为空")
- private String phone;
+ private String phoneNumber;
}
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/service/impl/VerificationCodeServiceImpl.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/service/impl/VerificationCodeServiceImpl.java
index a08993f..84b9a27 100644
--- a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/service/impl/VerificationCodeServiceImpl.java
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/service/impl/VerificationCodeServiceImpl.java
@@ -1,51 +1,102 @@
package top.crushtj.xiaoyishu.auth.service.impl;
+
import cn.hutool.core.util.RandomUtil;
-import top.crushtj.framework.common.exception.BizException;
-import top.crushtj.framework.common.response.Response;
-import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
-import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
-import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
-
+import top.crushtj.framework.common.exception.BizException;
+import top.crushtj.framework.common.response.Response;
+import top.crushtj.framework.common.utils.MaskUtils;
+import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
+import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
+import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import top.crushtj.xiaoyishu.auth.service.VerificationCodeService;
+import top.crushtj.xiaoyishu.auth.sms.AliyunSmsHelper;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
+
+/**
+ * 验证码服务实现类
+ * 修复点:异步异常捕获、Redis过期时间、验证码日志脱敏、手机号校验、线程任务跟踪
+ */
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
+ // 短信发送超时时间(秒)
+ private static final int SMS_SEND_TIMEOUT_SECONDS = 5;
@Resource
private RedisTemplate redisTemplate;
+ @Resource(name = "taskExecutor")
+ private ThreadPoolTaskExecutor threadPoolTaskExecutor;
+
+ @Resource
+ private AliyunSmsHelper aliyunSmsHelper;
+
@Override
public Response> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
- // 手机号
- String phone = sendVerificationCodeReqVO.getPhone();
+ // 1. 获取并校验手机号格式
+ String phoneNumber = sendVerificationCodeReqVO.getPhoneNumber();
- // 构建验证码 redis key
- String key = RedisKeyConstants.buildVerificationCodeKey(phone);
-
- // 判断是否已发送验证码
+ // 2. 构建Redis Key并检查发送频率
+ String key = RedisKeyConstants.buildVerificationCodeKey(phoneNumber);
boolean isSent = redisTemplate.hasKey(key);
if (isSent) {
- // 若之前发送的验证码未过期,则提示发送频繁
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
}
- // 生成 6 位随机数字验证码
+ // 3. 生成6位随机验证码
String verificationCode = RandomUtil.randomNumbers(6);
- // todo: 调用第三方短信发送服务
+ // 4. 异步发送短信(用CompletableFuture跟踪任务状态,捕获异常)
+ CompletableFuture smsSendFuture = CompletableFuture.supplyAsync(() -> {
+ // 设置线程名称,便于日志排查
+ Thread.currentThread().setName("sms-send-" + MaskUtils.maskMobile(phoneNumber));
+ String signName = "速通互联验证码";
+ String templateCode = "100001";
+ String templateParam = String.format("{\"code\":\"%s\",\"min\":\"%d\"}", verificationCode, VERIFICATION_CODE_EXPIRE_TIME);
+ try {
+ return aliyunSmsHelper.sendMessage(signName, templateCode, phoneNumber, templateParam);
+ } catch (Exception e) {
+ log.error("==> 手机号: {}, 短信发送接口调用异常", MaskUtils.maskMobile(phoneNumber), e);
+ return false;
+ }
+ }, threadPoolTaskExecutor);
- log.info("==> 手机号: {}, 已发送验证码:【{}】", phone, verificationCode);
+ // 5. 同步等待短信发送结果(超时控制,避免主线程阻塞过久)
+ boolean smsSendSuccess;
+ try {
+ smsSendSuccess = smsSendFuture.get(SMS_SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ log.error("==> 手机号: {}, 短信发送任务被中断", MaskUtils.maskMobile(phoneNumber), e);
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
+ } catch (ExecutionException e) {
+ log.error("==> 手机号: {}, 短信发送任务执行异常", MaskUtils.maskMobile(phoneNumber), e);
+ throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
+ } catch (TimeoutException e) {
+ log.error("==> 手机号: {}, 短信发送任务超时({}秒)", MaskUtils.maskMobile(phoneNumber), SMS_SEND_TIMEOUT_SECONDS, e);
+ throw new BizException(ResponseCodeEnum.SMS_SEND_TIMEOUT);
+ }
- // 存储验证码到 redis, 并设置过期时间为 3 分钟
- redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);
+ // 6. 短信发送失败则直接抛异常,不存储Redis
+ if (!smsSendSuccess) {
+ log.error("==> 手机号: {}, 发送验证码失败(第三方接口返回失败)", MaskUtils.maskMobile(phoneNumber));
+ throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
+ }
+
+ // 7. 短信发送成功后,记录日志(验证码脱敏,仅保留后2位)+ 存储Redis
+ log.info("==> 手机号: {}, 已发送验证码:【****{}】", MaskUtils.maskMobile(phoneNumber), verificationCode.substring(4));
+ redisTemplate.opsForValue().set(key, verificationCode, VERIFICATION_CODE_EXPIRE_TIME, TimeUnit.MINUTES);
return Response.success();
}
-}
+}
\ No newline at end of file
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunAccessKeyProperties.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunAccessKeyProperties.java
new file mode 100644
index 0000000..13fdcc2
--- /dev/null
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunAccessKeyProperties.java
@@ -0,0 +1,21 @@
+package top.crushtj.xiaoyishu.auth.sms;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author ayi
+ * @version V1.0
+ * @title AliyunAccessKeyProperties
+ * @date 2026/01/16 23:31
+ * @description 阿里云访问密钥配置
+ */
+
+@ConfigurationProperties(prefix = "aliyun")
+@Component
+@Data
+public class AliyunAccessKeyProperties {
+ private String accessKeyId;
+ private String accessKeySecret;
+}
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsClientConfig.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsClientConfig.java
new file mode 100644
index 0000000..e81a5ab
--- /dev/null
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsClientConfig.java
@@ -0,0 +1,42 @@
+package top.crushtj.xiaoyishu.auth.sms;
+
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author ayi
+ * @version V1.0
+ * @title AliyunSmsClientConfig
+ * @date 2026/01/16 23:48
+ * @description 阿里云短信配置
+ */
+
+@Slf4j
+@Configuration
+public class AliyunSmsClientConfig {
+ @Resource
+ private AliyunAccessKeyProperties aliyunAccessKeyProperties;
+
+ @Bean
+ public com.aliyun.dypnsapi20170525.Client smsClient() {
+ try {
+ com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
+
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+ .setCredential(credential);
+
+ // Endpoint 请参考 https://api.aliyun.com/product/Dypnsapi
+ config.endpoint = "dypnsapi.aliyuncs.com";
+
+ config.accessKeyId = aliyunAccessKeyProperties.getAccessKeyId();
+ config.accessKeySecret = aliyunAccessKeyProperties.getAccessKeySecret();
+
+ return new com.aliyun.dypnsapi20170525.Client(config);
+ } catch (Exception e) {
+ log.error("初始化阿里云短信发送客户端错误: ", e);
+ return null;
+ }
+ }
+}
diff --git a/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsHelper.java b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsHelper.java
new file mode 100644
index 0000000..41bff0f
--- /dev/null
+++ b/xiaoyi-auth/src/main/java/top/crushtj/xiaoyishu/auth/sms/AliyunSmsHelper.java
@@ -0,0 +1,49 @@
+package top.crushtj.xiaoyishu.auth.sms;
+
+import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import top.crushtj.framework.common.utils.JsonUtils;
+import top.crushtj.framework.common.utils.MaskUtils;
+
+/**
+ * @author ayi
+ * @version V1.0
+ * @title AliyunSmsHelper
+ * @date 2026/01/16 23:36
+ * @description 阿里云短信工具
+ */
+
+@Slf4j
+@Component
+public class AliyunSmsHelper {
+
+ @Resource
+ private com.aliyun.dypnsapi20170525.Client client;
+
+ public boolean sendMessage(String signName, String templateCode, String phoneNumber, String templateParam) {
+ com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest sendSmsVerifyCodeRequest = new com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest()
+ .setSignName(signName)
+ .setTemplateCode(templateCode)
+ .setPhoneNumber(phoneNumber)
+ .setTemplateParam(templateParam);
+
+ com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
+
+ try {
+ log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", MaskUtils.maskMobile(phoneNumber), signName, templateCode, templateParam);
+
+ // 发送短信
+ SendSmsVerifyCodeResponse response = client.sendSmsVerifyCodeWithOptions(sendSmsVerifyCodeRequest, runtime);
+
+ boolean success = response.getBody().success;
+ log.info("==> 短信发送状态: {}, response: {}", success, JsonUtils.toJsonString(response));
+ return success;
+ } catch (Exception error) {
+ log.error("==> 短信发送错误: ", error);
+ return false;
+ }
+ }
+
+}
diff --git a/xiaoyi-auth/src/main/resources/config/application-dev.yml b/xiaoyi-auth/src/main/resources/config/application-dev.yml
index 0a3d0bc..b75cb09 100644
--- a/xiaoyi-auth/src/main/resources/config/application-dev.yml
+++ b/xiaoyi-auth/src/main/resources/config/application-dev.yml
@@ -71,4 +71,8 @@ mybatis-plus:
logging:
level:
- top.crushtj.xiaoyishu.auth.domain.mappers: debug
\ No newline at end of file
+ top.crushtj.xiaoyishu.auth.domain.mappers: debug
+
+aliyun: # 接入阿里云(发送短信使用)
+ accessKeyId:
+ accessKeySecret:
\ No newline at end of file
diff --git a/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/DruidTest.java b/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/DruidTest.java
deleted file mode 100644
index 654f273..0000000
--- a/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/DruidTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package top.crushtj.xiaoyishu.auth;
-
-import com.alibaba.druid.filter.config.ConfigTools;
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-/**
- * @author ayi
- * @version V1.0
- * @title DruidTest
- * @description
- * @date 2025/12/01
- */
-
-@SpringBootTest
-@Slf4j
-public class DruidTest {
-
- /**
- * Druid 密码加密
- */
- @Test
- @SneakyThrows
- void testEncodePassword() {
- // 你的密码
- String password = "HhpxE2HWE4bGTyB5";
- String[] arr = ConfigTools.genKeyPair(512);
-
- // 私钥
- log.info("privateKey: {}", arr[0]);
- // 公钥
- log.info("publicKey: {}", arr[1]);
-
- // 通过私钥加密密码
- String encodePassword = ConfigTools.encrypt(arr[0], password);
- log.info("password: {}", encodePassword);
- }
-}
diff --git a/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/EncryptTest.java b/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/EncryptTest.java
new file mode 100644
index 0000000..8d0c99d
--- /dev/null
+++ b/xiaoyi-auth/src/test/java/top/crushtj/xiaoyishu/auth/EncryptTest.java
@@ -0,0 +1,63 @@
+package top.crushtj.xiaoyishu.auth;
+
+import com.alibaba.druid.filter.config.ConfigTools;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
+import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author ayi
+ * @version V1.0
+ * @title DruidTest
+ * @description
+ * @date 2025/12/01
+ */
+
+@SpringBootTest
+@Slf4j
+public class EncryptTest {
+ //@Autowired
+ //private StringEncryptor defaultLazyEncryptor;
+ //private StringEncryptor pooledPbeStringEncryptor;
+
+ /**
+ * Druid 密码加密
+ */
+ @Test
+ @SneakyThrows
+ void testEncodePassword() {
+ // 你的密码
+ String password = "HhpxE2HWE4bGTyB5";
+ String[] arr = ConfigTools.genKeyPair(512);
+
+ // 私钥
+ log.info("privateKey: {}", arr[0]);
+ // 公钥
+ log.info("publicKey: {}", arr[1]);
+
+ // 通过私钥加密密码
+ String encodePassword = ConfigTools.encrypt(arr[0], password);
+ log.info("password: {}", encodePassword);
+ }
+
+ @Test
+ void smsEncode() {
+ String accessKeyId = manualEncrypt("", "Yu020320.");
+ System.out.println("accessKeyId:" + accessKeyId);
+ String accessKeySecret = manualEncrypt("", "Yu020320.");
+ System.out.println("accessKeySecret:" + accessKeySecret);
+ }
+
+ private String manualEncrypt(String plainText, String secretKey) {
+ PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
+ SimpleStringPBEConfig config = new SimpleStringPBEConfig();
+ config.setPassword(secretKey); // 密钥
+ config.setAlgorithm("PBEWithMD5AndDES"); // 算法
+ config.setPoolSize(1); // 池大小(默认)
+ encryptor.setConfig(config);
+ return encryptor.encrypt(plainText);
+ }
+}
diff --git a/xiaoyi-framework/xiaoyi-common/src/main/java/top/crushtj/framework/common/utils/MaskUtils.java b/xiaoyi-framework/xiaoyi-common/src/main/java/top/crushtj/framework/common/utils/MaskUtils.java
new file mode 100644
index 0000000..8a22336
--- /dev/null
+++ b/xiaoyi-framework/xiaoyi-common/src/main/java/top/crushtj/framework/common/utils/MaskUtils.java
@@ -0,0 +1,143 @@
+package top.crushtj.framework.common.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author ayi
+ * @version V1.0
+ * @title MaskUtils
+ * @date 2026/01/17 13:22
+ * @description 数据脱敏工具类
+ */
+
+public class MaskUtils {
+
+ // ===================== 通用脱敏方法(基础) =====================
+ /**
+ * 通用脱敏方法:保留前prefixLen位,后suffixLen位,中间用*填充
+ * @param content 原始字符串
+ * @param prefixLen 保留前缀长度
+ * @param suffixLen 保留后缀长度
+ * @return 脱敏后的字符串
+ */
+ public static String maskGeneral(String content, int prefixLen, int suffixLen) {
+ // 空值处理:null/空串直接返回,避免NPE
+ if (StringUtils.isBlank(content)) {
+ return content;
+ }
+ int contentLen = content.length();
+ // 若长度小于等于前缀+后缀,直接返回原字符串(避免过度脱敏)
+ if (contentLen <= prefixLen + suffixLen) {
+ return content;
+ }
+ // 拼接前缀 + 中间* + 后缀
+ StringBuilder sb = new StringBuilder();
+ // 添加前缀
+ sb.append(content.substring(0, prefixLen));
+ // 添加中间掩码(长度=总长度-前缀-后缀)
+ sb.append("*".repeat(Math.max(0, contentLen - prefixLen - suffixLen)));
+ // 添加后缀
+ sb.append(content.substring(contentLen - suffixLen));
+ return sb.toString();
+ }
+
+ // ===================== 常用敏感字段脱敏方法(业务封装) =====================
+
+ /**
+ * 手机号脱敏:保留前3位,后4位,中间4位掩码(如:138****1234)
+ * @param mobile 手机号(支持11位常规手机号)
+ * @return 脱敏后的手机号
+ */
+ public static String maskMobile(String mobile) {
+ // 先校验手机号格式(简单校验,可根据业务调整)
+ if (StringUtils.isBlank(mobile) || !mobile.matches("^1[3-9]\\d{9}$")) {
+ return mobile;
+ }
+ return maskGeneral(mobile, 3, 4);
+ }
+
+ /**
+ * 邮箱脱敏:保留前缀前3位,@及域名完整,中间掩码(如:123****@qq.com)
+ * @param email 邮箱地址
+ * @return 脱敏后的邮箱
+ */
+ public static String maskEmail(String email) {
+ if (StringUtils.isBlank(email) || !email.contains("@")) {
+ return email;
+ }
+ String[] parts = email.split("@");
+ String prefix = parts[0];
+ String domain = parts[1];
+ // 前缀长度<=3时不脱敏,否则保留前3位
+ String maskedPrefix = prefix.length() <= 3 ? prefix : maskGeneral(prefix, 3, 0);
+ return maskedPrefix + "@" + domain;
+ }
+
+ /**
+ * 姓名脱敏:
+ * - 单字名:直接返回(如:李 → 李)
+ * - 两字名:保留姓,名掩码(如:李白 → 李*)
+ * - 三字及以上:保留姓和最后一个字,中间掩码(如:李世民 → 李*民)
+ * @param name 姓名
+ * @return 脱敏后的姓名
+ */
+ public static String maskName(String name) {
+ if (StringUtils.isBlank(name)) {
+ return name;
+ }
+ int nameLen = name.length();
+ if (nameLen == 1) {
+ return name;
+ } else if (nameLen == 2) {
+ return name.substring(0, 1) + "*";
+ } else {
+ // 姓 + 中间* + 最后一个字
+ return name.substring(0, 1) +
+ "*".repeat(nameLen - 2) +
+ name.substring(nameLen - 1);
+ }
+ }
+
+ /**
+ * 身份证号脱敏:保留前3位,后4位,中间掩码(如:110***********1234)
+ * @param idCard 身份证号(支持15位/18位)
+ * @return 脱敏后的身份证号
+ */
+ public static String maskIdCard(String idCard) {
+ if (StringUtils.isBlank(idCard) || (idCard.length() != 15 && idCard.length() != 18)) {
+ return idCard;
+ }
+ return maskGeneral(idCard, 3, 4);
+ }
+
+ /**
+ * 银行卡号脱敏:保留前6位,后4位,中间掩码(如:622260**********1234)
+ * @param bankCard 银行卡号(常规16/19位)
+ * @return 脱敏后的银行卡号
+ */
+ public static String maskBankCard(String bankCard) {
+ if (StringUtils.isBlank(bankCard) || bankCard.length() < 10) {
+ return bankCard;
+ }
+ return maskGeneral(bankCard, 6, 4);
+ }
+
+ // ===================== 测试用例(可直接运行验证) =====================
+ public static void main(String[] args) {
+ // 测试手机号脱敏
+ System.out.println("手机号脱敏:" + maskMobile("13812345678")); // 输出:138****5678
+ // 测试邮箱脱敏
+ System.out.println("邮箱脱敏:" + maskEmail("1234567890@qq.com")); // 输出:123****@qq.com
+ // 测试姓名脱敏
+ System.out.println("单字名:" + maskName("李")); // 输出:李
+ System.out.println("两字名:" + maskName("李白")); // 输出:李*
+ System.out.println("三字名:" + maskName("李世民")); // 输出:李*民
+ // 测试身份证脱敏
+ System.out.println("身份证脱敏:" + maskIdCard("110101199001011234")); // 输出:110***********1234
+ // 测试银行卡脱敏
+ System.out.println("银行卡脱敏:" + maskBankCard("6222600000000001234")); // 输出:622260**********1234
+ // 测试空值/异常值
+ System.out.println("空手机号:" + maskMobile(null)); // 输出:null
+ System.out.println("无效邮箱:" + maskEmail("123456")); // 输出:123456
+ }
+}