feat(微信): 添加动态码生成功能及相关缓存支持
新增动态码生成工具类DynamicCodeGenerator,提供唯一6位数字动态码生成及验证功能 在CacheCenter和CaffeineCacheService中添加dynamicCodeCache支持动态码缓存 在WxController中添加生成动态码接口 新增build脚本用于构建不同模块
This commit is contained in:
parent
509d57596f
commit
09fce0754d
|
|
@ -5,9 +5,11 @@ import cn.hutool.json.JSONUtil;
|
||||||
import com.agileboot.common.core.dto.ResponseDTO;
|
import com.agileboot.common.core.dto.ResponseDTO;
|
||||||
import com.agileboot.common.exception.ApiException;
|
import com.agileboot.common.exception.ApiException;
|
||||||
import com.agileboot.common.exception.error.ErrorCode;
|
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.WxService;
|
||||||
import com.agileboot.domain.wx.user.WxUserApplicationService;
|
import com.agileboot.domain.wx.user.WxUserApplicationService;
|
||||||
import com.agileboot.domain.wx.user.dto.WxUserDTO;
|
import com.agileboot.domain.wx.user.dto.WxUserDTO;
|
||||||
|
import com.agileboot.domain.wx.utils.DynamicCodeGenerator;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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()));
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ public class CacheCenter {
|
||||||
|
|
||||||
public static AbstractCaffeineCacheTemplate<String> qyUseridCache;
|
public static AbstractCaffeineCacheTemplate<String> qyUseridCache;
|
||||||
|
|
||||||
|
public static AbstractCaffeineCacheTemplate<String> dynamicCodeCache;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
|
GuavaCacheService guavaCache = SpringUtil.getBean(GuavaCacheService.class);
|
||||||
|
|
@ -50,6 +52,7 @@ public class CacheCenter {
|
||||||
roleCache = caffeineCache.roleCache;
|
roleCache = caffeineCache.roleCache;
|
||||||
postCache = caffeineCache.postCache;
|
postCache = caffeineCache.postCache;
|
||||||
qyUseridCache = caffeineCache.qyUseridCache;
|
qyUseridCache = caffeineCache.qyUseridCache;
|
||||||
|
dynamicCodeCache = caffeineCache.dynamicCodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ public class CaffeineCacheService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 企业微信用户ID缓存:6小时过期和刷新(通过API获取,有一定成本)
|
// 企业微信用户ID缓存:12小时过期和刷新(通过API获取,有一定成本)
|
||||||
public AbstractCaffeineCacheTemplate<String> qyUseridCache = new AbstractCaffeineCacheTemplate<String>(
|
public AbstractCaffeineCacheTemplate<String> qyUseridCache = new AbstractCaffeineCacheTemplate<String>(
|
||||||
12, java.util.concurrent.TimeUnit.HOURS,
|
12, java.util.concurrent.TimeUnit.HOURS,
|
||||||
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 统计信息字符串
|
* @return 统计信息字符串
|
||||||
|
|
@ -100,6 +111,7 @@ public class CaffeineCacheService {
|
||||||
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
|
stats.append("Role Cache: ").append(roleCache.getStats()).append("\n");
|
||||||
stats.append("Post Cache: ").append(postCache.getStats()).append("\n");
|
stats.append("Post Cache: ").append(postCache.getStats()).append("\n");
|
||||||
stats.append("QyUserid Cache: ").append(qyUseridCache.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();
|
return stats.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 + " 次");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
mvn clean package -pl agileboot-admin -am
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
mvn clean package -pl agileboot-api -am
|
||||||
Loading…
Reference in New Issue