From 09fce0754d55a379e30d9e1fd0fb5ebeeb9e91f3 Mon Sep 17 00:00:00 2001 From: dzq Date: Thu, 6 Nov 2025 11:14:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BE=AE=E4=BF=A1):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=A0=81=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=BC=93=E5=AD=98=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增动态码生成工具类DynamicCodeGenerator,提供唯一6位数字动态码生成及验证功能 在CacheCenter和CaffeineCacheService中添加dynamicCodeCache支持动态码缓存 在WxController中添加生成动态码接口 新增build脚本用于构建不同模块 --- .../api/controller/WxController.java | 46 ++++ .../domain/common/cache/CacheCenter.java | 3 + .../common/cache/CaffeineCacheService.java | 14 +- .../domain/wx/utils/DynamicCodeGenerator.java | 201 ++++++++++++++++++ build-admin.bat | 1 + build-api.bat | 1 + build.bat | 2 - 7 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/wx/utils/DynamicCodeGenerator.java create mode 100644 build-admin.bat create mode 100644 build-api.bat delete mode 100644 build.bat diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/WxController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/WxController.java index 10f0e87..316b156 100644 --- a/agileboot-api/src/main/java/com/agileboot/api/controller/WxController.java +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/WxController.java @@ -5,9 +5,11 @@ import cn.hutool.json.JSONUtil; import com.agileboot.common.core.dto.ResponseDTO; import com.agileboot.common.exception.ApiException; import com.agileboot.common.exception.error.ErrorCode; +import com.agileboot.domain.common.cache.CacheCenter; import com.agileboot.domain.wx.WxService; import com.agileboot.domain.wx.user.WxUserApplicationService; import com.agileboot.domain.wx.user.dto.WxUserDTO; +import com.agileboot.domain.wx.utils.DynamicCodeGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -74,4 +76,48 @@ public class WxController { return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage())); } } + + /** + * 根据openid生成动态码 + * @param openid 微信用户openid + * @return 包含动态码的响应结果 + * @throws ApiException 当openid无效或用户不存在时抛出 + */ + @GetMapping("/generateDynamicCode") + public ResponseDTO> generateDynamicCode(@RequestParam("openid") String openid) { + try { + // 校验openid是否为空 + if (StringUtils.isBlank(openid)) { + throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "openid不能为空"); + } + + // 检查该openid对应的用户是否存在 + WxUserDTO wxUserDTO = wxUserApplicationService.getUserDetailByOpenid(openid); + if (wxUserDTO == null) { + throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "用户不存在,无法生成动态码"); + } + + // 生成唯一的6位数字动态码(使用动态码作为缓存键,自动应用前缀) + String dynamicCode = DynamicCodeGenerator.generateUniqueDynamicCodeWithPrefix( + cacheKey -> CacheCenter.dynamicCodeCache.get(cacheKey) + ); + + // 构建带前缀的缓存键并保存openid + String cacheKey = DynamicCodeGenerator.buildCacheKey(dynamicCode); + CacheCenter.dynamicCodeCache.put(cacheKey, openid); + + log.info("为openid {} 生成唯一动态码: {},已保存到缓存(键: {})", openid, dynamicCode, cacheKey); + + // 返回结果 + Map result = new HashMap<>(); + result.put("dynamicCode", dynamicCode); + result.put("validityMinutes", String.valueOf(DynamicCodeGenerator.getCodeValidityMinutes())); + result.put("cacheKey", cacheKey); + + return ResponseDTO.ok(result); + } catch (Exception e) { + log.error("生成动态码失败", e); + return ResponseDTO.fail(new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage())); + } + } } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java index 5537654..20ed564 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CacheCenter.java @@ -36,6 +36,8 @@ public class CacheCenter { public static AbstractCaffeineCacheTemplate qyUseridCache; + public static AbstractCaffeineCacheTemplate dynamicCodeCache; + @PostConstruct public void init() { GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class); @@ -50,6 +52,7 @@ public class CacheCenter { roleCache = caffeineCache.roleCache; postCache = caffeineCache.postCache; qyUseridCache = caffeineCache.qyUseridCache; + dynamicCodeCache = caffeineCache.dynamicCodeCache; } } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java index 4a9f1f1..a677a39 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/common/cache/CaffeineCacheService.java @@ -76,7 +76,7 @@ public class CaffeineCacheService { } }; - // 企业微信用户ID缓存:6小时过期和刷新(通过API获取,有一定成本) + // 企业微信用户ID缓存:12小时过期和刷新(通过API获取,有一定成本) public AbstractCaffeineCacheTemplate qyUseridCache = new AbstractCaffeineCacheTemplate( 12, java.util.concurrent.TimeUnit.HOURS, 12, java.util.concurrent.TimeUnit.HOURS) { @@ -87,6 +87,17 @@ public class CaffeineCacheService { } }; + // 动态码缓存:30分钟过期和刷新(动态码通常短时间内有效) + public AbstractCaffeineCacheTemplate dynamicCodeCache = new AbstractCaffeineCacheTemplate( + 30, java.util.concurrent.TimeUnit.MINUTES, + 30, java.util.concurrent.TimeUnit.MINUTES) { + @Override + public String getObjectFromDb(Object id) { + // 动态码不需要从数据库获取,这里返回null + return null; + } + }; + /** * 获取缓存统计信息 * @return 统计信息字符串 @@ -100,6 +111,7 @@ public class CaffeineCacheService { stats.append("Role Cache: ").append(roleCache.getStats()).append("\n"); stats.append("Post Cache: ").append(postCache.getStats()).append("\n"); stats.append("QyUserid Cache: ").append(qyUseridCache.getStats()).append("\n"); + stats.append("Dynamic Code Cache: ").append(dynamicCodeCache.getStats()).append("\n"); return stats.toString(); } } \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/wx/utils/DynamicCodeGenerator.java b/agileboot-domain/src/main/java/com/agileboot/domain/wx/utils/DynamicCodeGenerator.java new file mode 100644 index 0000000..1ff1a78 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/wx/utils/DynamicCodeGenerator.java @@ -0,0 +1,201 @@ +package com.agileboot.domain.wx.utils; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.function.Function; + +/** + * 动态码生成工具类 + * 用于生成6位数字动态码 + * @author valarchie + */ +public class DynamicCodeGenerator { + + /** + * 动态码长度 + */ + private static final int CODE_LENGTH = 6; + + /** + * 动态码有效期(分钟) + */ + private static final int CODE_VALIDITY_MINUTES = 30; + + /** + * 动态码缓存键前缀 + */ + public static final String DYNAMIC_CODE_CACHE_PREFIX = "wx_dynamic_code_"; + + private static final Random RANDOM; + + static { + try { + // 使用SecureRandom提供更安全的随机数生成 + RANDOM = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to initialize SecureRandom", e); + } + } + + /** + * 生成6位数字动态码 + * @return 6位数字字符串 + */ + public static String generateDynamicCode() { + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(RANDOM.nextInt(10)); + } + return code.toString(); + } + + /** + * 生成指定长度的数字动态码 + * @param length 动态码长度 + * @return 数字字符串 + */ + public static String generateDynamicCode(int length) { + if (length <= 0) { + throw new IllegalArgumentException("Length must be positive"); + } + + StringBuilder code = new StringBuilder(); + for (int i = 0; i < length; i++) { + code.append(RANDOM.nextInt(10)); + } + return code.toString(); + } + + /** + * 验证动态码格式是否正确 + * @param code 要验证的动态码 + * @return 是否为6位数字 + */ + public static boolean isValidDynamicCode(String code) { + if (code == null || code.length() != CODE_LENGTH) { + return false; + } + + // 检查是否全为数字 + for (char c : code.toCharArray()) { + if (!Character.isDigit(c)) { + return false; + } + } + + return true; + } + + /** + * 获取动态码默认有效期(分钟) + * @return 有效期分钟数 + */ + public static int getCodeValidityMinutes() { + return CODE_VALIDITY_MINUTES; + } + + /** + * 获取动态码长度 + * @return 动态码长度 + */ + public static int getCodeLength() { + return CODE_LENGTH; + } + + /** + * 生成唯一的6位数字动态码 + * 通过检查缓存避免重复 + * @param existsChecker 检查动态码是否已存在的函数,参数为动态码,返回值为缓存中存储的值 + * @return 唯一的6位数字动态码 + * @throws RuntimeException 当超过最大尝试次数时抛出 + */ + public static String generateUniqueDynamicCode(Function existsChecker) { + final int MAX_ATTEMPTS = 20; // 最大尝试次数 + int attempts = 0; + + while (attempts < MAX_ATTEMPTS) { + // 生成6位数字动态码 + String dynamicCode = generateDynamicCode(); + + try { + // 检查缓存中是否已存在此动态码 + String result = existsChecker.apply(dynamicCode); + + // 如果缓存中没有此动态码,说明是唯一的,可以使用 + if (result == null) { + return dynamicCode; + } + } catch (Exception e) { + // 检查异常,记录日志但继续尝试 + System.err.println("检查动态码是否存在时发生错误: " + e.getMessage()); + } + + attempts++; + System.err.println("动态码 " + dynamicCode + " 已存在,尝试第 " + attempts + " 次重新生成"); + } + + // 如果尝试多次后仍无法生成唯一动态码,抛出异常 + throw new RuntimeException("生成唯一动态码失败,已尝试 " + MAX_ATTEMPTS + " 次"); + } + + /** + * 构建带前缀的动态码缓存键 + * @param dynamicCode 动态码 + * @return 带前缀的缓存键 + */ + public static String buildCacheKey(String dynamicCode) { + return DYNAMIC_CODE_CACHE_PREFIX + dynamicCode; + } + + /** + * 从带前缀的缓存键中提取原始动态码 + * @param cacheKey 带前缀的缓存键 + * @return 原始动态码,如果键格式不正确则返回null + */ + public static String extractDynamicCode(String cacheKey) { + if (cacheKey != null && cacheKey.startsWith(DYNAMIC_CODE_CACHE_PREFIX)) { + return cacheKey.substring(DYNAMIC_CODE_CACHE_PREFIX.length()); + } + return null; + } + + /** + * 生成唯一的6位数字动态码(带缓存键前缀检查) + * 通过检查缓存避免重复,自动使用缓存键前缀 + * @param cacheProvider 提供缓存访问的函数,参数为带前缀的缓存键,返回值为缓存中存储的值 + * @return 唯一的6位数字动态码 + * @throws RuntimeException 当超过最大尝试次数时抛出 + */ + public static String generateUniqueDynamicCodeWithPrefix(Function cacheProvider) { + final int MAX_ATTEMPTS = 20; // 最大尝试次数 + int attempts = 0; + + while (attempts < MAX_ATTEMPTS) { + // 生成6位数字动态码 + String dynamicCode = generateDynamicCode(); + + try { + // 构建带前缀的缓存键 + String cacheKey = buildCacheKey(dynamicCode); + + // 检查缓存中是否已存在此动态码 + String result = cacheProvider.apply(cacheKey); + + // 如果缓存中没有此动态码,说明是唯一的,可以使用 + if (result == null) { + return dynamicCode; + } + } catch (Exception e) { + // 检查异常,记录日志但继续尝试 + System.err.println("检查动态码是否存在时发生错误: " + e.getMessage()); + } + + attempts++; + System.err.println("动态码 " + dynamicCode + " 已存在,尝试第 " + attempts + " 次重新生成"); + } + + // 如果尝试多次后仍无法生成唯一动态码,抛出异常 + throw new RuntimeException("生成唯一动态码失败,已尝试 " + MAX_ATTEMPTS + " 次"); + } +} diff --git a/build-admin.bat b/build-admin.bat new file mode 100644 index 0000000..713f5f7 --- /dev/null +++ b/build-admin.bat @@ -0,0 +1 @@ +mvn clean package -pl agileboot-admin -am \ No newline at end of file diff --git a/build-api.bat b/build-api.bat new file mode 100644 index 0000000..1eb23a8 --- /dev/null +++ b/build-api.bat @@ -0,0 +1 @@ +mvn clean package -pl agileboot-api -am \ No newline at end of file diff --git a/build.bat b/build.bat deleted file mode 100644 index 5791d84..0000000 --- a/build.bat +++ /dev/null @@ -1,2 +0,0 @@ -mvn clean package -pl agileboot-api -am -mvn clean package -pl agileboot-admin -am \ No newline at end of file