feat(微信): 添加动态码生成功能及相关缓存支持

新增动态码生成工具类DynamicCodeGenerator,提供唯一6位数字动态码生成及验证功能
在CacheCenter和CaffeineCacheService中添加dynamicCodeCache支持动态码缓存
在WxController中添加生成动态码接口
新增build脚本用于构建不同模块
This commit is contained in:
dzq 2025-11-06 11:14:42 +08:00
parent 509d57596f
commit 09fce0754d
7 changed files with 265 additions and 3 deletions

View File

@ -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<Map<String, String>> 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<String, String> 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()));
}
}
}

View File

@ -36,6 +36,8 @@ public class CacheCenter {
public static AbstractCaffeineCacheTemplate<String> qyUseridCache;
public static AbstractCaffeineCacheTemplate<String> 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;
}
}

View File

@ -76,7 +76,7 @@ public class CaffeineCacheService {
}
};
// 企业微信用户ID缓存6小时过期和刷新通过API获取有一定成本
// 企业微信用户ID缓存12小时过期和刷新通过API获取有一定成本
public AbstractCaffeineCacheTemplate<String> qyUseridCache = new AbstractCaffeineCacheTemplate<String>(
12, java.util.concurrent.TimeUnit.HOURS,
12, java.util.concurrent.TimeUnit.HOURS) {
@ -87,6 +87,17 @@ public class CaffeineCacheService {
}
};
// 动态码缓存30分钟过期和刷新动态码通常短时间内有效
public AbstractCaffeineCacheTemplate<String> dynamicCodeCache = new AbstractCaffeineCacheTemplate<String>(
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();
}
}

View File

@ -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<String, String> 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<String, String> 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 + "");
}
}

1
build-admin.bat Normal file
View File

@ -0,0 +1 @@
mvn clean package -pl agileboot-admin -am

1
build-api.bat Normal file
View File

@ -0,0 +1 @@
mvn clean package -pl agileboot-api -am

View File

@ -1,2 +0,0 @@
mvn clean package -pl agileboot-api -am
mvn clean package -pl agileboot-admin -am