From 81c417ed76889aca83f98e6ff0d3ff08bf93aa90 Mon Sep 17 00:00:00 2001 From: xupei Date: Wed, 20 Aug 2025 16:40:44 +0800 Subject: [PATCH] =?UTF-8?q?BUGFIX:1=E3=80=81=E5=AD=90=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=EF=BC=8C=E6=97=A0=E6=B3=95=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E7=94=A8=E6=88=B7=E5=90=8D=E6=88=96=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E6=9F=A5=202=E3=80=81=E6=95=99=E8=82=B2=E7=89=88?= =?UTF-8?q?=E5=AD=90=E8=B4=A6=E5=8F=B7=E5=88=9B=E5=BB=BA=E5=8F=8A=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=B6=89=E5=8F=8A=E7=9A=84=E7=A7=AF=E5=88=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/da/common/utils/RedisUtil.java | 11 +- .../ai/da/common/utils/RedisUtilEnhance.java | 743 ++++++++++++++++++ .../ai/da/controller/AccountController.java | 3 - .../ai/da/model/dto/SubAccountPageDTO.java | 6 +- .../da/service/impl/AccountServiceImpl.java | 182 +++-- .../da/service/impl/AffiliateServiceImpl.java | 6 +- .../files/sub_account_import_template.xlsx | Bin 10752 -> 11044 bytes 7 files changed, 872 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/ai/da/common/utils/RedisUtilEnhance.java diff --git a/src/main/java/com/ai/da/common/utils/RedisUtil.java b/src/main/java/com/ai/da/common/utils/RedisUtil.java index 8a925732..39d7e3ad 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -380,15 +380,6 @@ public class RedisUtil { return redisTemplate.opsForValue().increment(key, 0); } - public Long getAndSetKey(String key, Long count) { - try { - String val = redisTemplate.opsForValue().getAndSet(key, count.toString()); - return StringUtil.isNullOrEmpty(val) ? 0L : Long.parseLong(val); - } catch (NumberFormatException e) { - return 0L; // 或 throw new BusinessException("非法的数值格式"); - } - } - /** * 记录任务的耗时到Redis * @param taskKey 任务标识,如 "taskA" @@ -620,4 +611,4 @@ public class RedisUtil { return count <= 3; } -} +} \ No newline at end of file diff --git a/src/main/java/com/ai/da/common/utils/RedisUtilEnhance.java b/src/main/java/com/ai/da/common/utils/RedisUtilEnhance.java new file mode 100644 index 00000000..ed1fd938 --- /dev/null +++ b/src/main/java/com/ai/da/common/utils/RedisUtilEnhance.java @@ -0,0 +1,743 @@ +package com.ai.da.common.utils; + +import com.ai.da.model.dto.ProgressDTO; +import com.ai.da.python.vo.DesignPythonObject; +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class RedisUtilEnhance { + + @Resource + private RedisTemplate redisTemplate; + + private ValueOperations valueOperations; + private SetOperations setOperations; + private HashOperations hashOperations; + private ZSetOperations zSetOperations; + + @PostConstruct + public void init() { + this.valueOperations = redisTemplate.opsForValue(); + this.setOperations = redisTemplate.opsForSet(); + this.hashOperations = redisTemplate.opsForHash(); + this.zSetOperations = redisTemplate.opsForZSet(); + } + + public final static String FLUX_POLLING_URL = "Flux:"; + + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + //- - - - - - - - - - - - - - - - - - - - - ZSet类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 向ZSet中添加元素 + */ + public void addToZSet(String key, Object value, Double score) { + zSetOperations.add(key, value, score); + } + + /** + * 从ZSet中删除元素 + */ + public void removeFromZSet(String key, Object value) { + zSetOperations.remove(key, value); + } + + /** + * 获取指定元素的当前排列顺序 + */ + public Long getRank(String key, Object value) { + return zSetOperations.rank(key, value); + } + + /** + * 获取当前ZSet中的最大score + */ + public Double getMaxScore(String key) { + Set> set = zSetOperations.reverseRangeWithScores(key, 0, 0); + + if (!CollectionUtils.isEmpty(set)) { + Iterator> iterator = set.iterator(); + if (iterator.hasNext()) { + Double score = iterator.next().getScore(); + return score != null ? score + 1.0 : 1.0; + } + } + return 1.0; + } + + /** + * 判断元素是否存在 + */ + public Boolean isElementExistsInZSet(String key, Object value) { + return zSetOperations.score(key, value) != null; + } + + /** + * 获取当前ZSet中数据量的总和 + */ + public Long getZSetTotalCount(String key) { + return zSetOperations.zCard(key); + } + + public Set getZSetTotalData(String key) { + return zSetOperations.range(key, 0, -1); + } + + //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 将数据放入set缓存 + */ + public void addToSet(String key, Object value) { + setOperations.add(key, value); + } + + /** + * 弹出变量中的元素 + */ + public void removeFromSet(String key, Object value) { + setOperations.remove(key, value); + } + + /** + * 检查给定的元素是否在变量中。 + */ + public Boolean isElementExistsInSet(String key, Object obj) { + return setOperations.isMember(key, obj); + } + + //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 加入缓存 + */ + public void addToMap(String key, Map map) { + hashOperations.putAll(key, map); + } + + /** + * 验证指定 key 下 有没有指定的 hashkey + */ + public Boolean isElementExistsInMap(String key, Object hashKey) { + return hashOperations.hasKey(key, hashKey); + } + + /** + * 获取指定key的值string + */ + public String getMapValue(String key1, Object key2) { + Object value = hashOperations.get(key1, key2); + return value != null ? value.toString() : null; + } + + /** + * 删除指定 hash 的 HashKey + * + * @return 删除成功的 数量 + */ + public Long removeFromMap(String key, Object hashKeys) { + return hashOperations.delete(key, hashKeys); + } + + //- - - - - - - - - - - - - - - - - - - - - String/Long/Integer类型支持 - - - - - - - - - - - - - - - - - - - - + + /** + * 设置字符串值 + */ + public void setString(String key, String value) { + valueOperations.set(key, value); + } + + /** + * 设置字符串值并设置过期时间 + */ + public void setString(String key, String value, long timeout, TimeUnit unit) { + valueOperations.set(key, value, timeout, unit); + } + + /** + * 设置Long值 + */ + public void setLong(String key, Long value) { + valueOperations.set(key, value); + } + + /** + * 设置Long值并设置过期时间 + */ + public void setLong(String key, Long value, long timeout, TimeUnit unit) { + valueOperations.set(key, value, timeout, unit); + } + + /** + * 设置Integer值 + */ + public void setInteger(String key, Integer value) { + valueOperations.set(key, value); + } + + /** + * 设置Integer值并设置过期时间 + */ + public void setInteger(String key, Integer value, long timeout, TimeUnit unit) { + valueOperations.set(key, value, timeout, unit); + } + + /** + * 设置对象值 + */ + public void setObject(String key, Object value, long timeout, TimeUnit unit) { + valueOperations.set(key, value, timeout, unit); + } + + /** + * 获取字符串值 + */ + public String getString(String key) { + Object value = valueOperations.get(key); + return value != null ? value.toString() : null; + } + + /** + * 获取Long值 + */ + public Long getLong(String key) { + Object value = valueOperations.get(key); + if (value instanceof Long) { + return (Long) value; + } else if (value instanceof Integer) { + return ((Integer) value).longValue(); + } else if (value instanceof String) { + try { + return Long.parseLong((String) value); + } catch (NumberFormatException e) { + log.warn("无法将字符串转换为Long: key={}, value={}", key, value); + return null; + } + } else if (value != null) { + log.warn("不支持的类型转换到Long: key={}, type={}", key, value.getClass().getName()); + } + return null; + } + + /** + * 获取Long值,带默认值 + */ + public Long getLong(String key, Long defaultValue) { + try { + Long value = getLong(key); + return value != null ? value : defaultValue; + } catch (Exception e) { + log.warn("获取Long值失败: key={}", key, e); + return defaultValue; + } + } + + /** + * 获取Integer值 + */ + public Integer getInteger(String key) { + Object value = valueOperations.get(key); + if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Long) { + return ((Long) value).intValue(); + } else if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + log.warn("无法将字符串转换为Integer: key={}, value={}", key, value); + return null; + } + } else if (value != null) { + log.warn("不支持的类型转换到Integer: key={}, type={}", key, value.getClass().getName()); + } + return null; + } + + /** + * 获取Integer值,带默认值 + */ + public Integer getInteger(String key, Integer defaultValue) { + try { + Integer value = getInteger(key); + return value != null ? value : defaultValue; + } catch (Exception e) { + log.warn("获取Integer值失败: key={}", key, e); + return defaultValue; + } + } + + /** + * 递增操作 + */ + public Long increment(String key, long delta) { + return valueOperations.increment(key, delta); + } + + /** + * 递增操作(double) + */ + public Double increment(String key, double delta) { + return valueOperations.increment(key, delta); + } + + //- - - - - - - - - - - - - - - - - - - - - 原有方法适配 - - - - - - - - - - - - - - - - - - - - + + public void addToString(String key, String value) { + setString(key, value); + } + + public void addToString(String key, String value, Long expiresIn) { + setString(key, value, expiresIn, TimeUnit.SECONDS); + } + + public String getFromString(String key) { + return getString(key); + } + + public Set getKeysFromString(String pattern) { + Set keys = redisTemplate.keys(pattern); + return keys != null ? keys : Collections.emptySet(); + } + + public Long getSize(String key) { + return setOperations.size(key); + } + + public List getMultiValue(Set keys) { + return valueOperations.multiGet(keys); + } + + public Long getExpire(String key) { + return redisTemplate.getExpire(key); + } + + public void removeFromString(String key) { + redisTemplate.delete(key); + } + + public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:"; + + public void likePost(Long portfolioId, Long userId) { + setOperations.add(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); + } + + public Long getLikeCount(Long portfolioId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + return setOperations.size(key); + } + + public List getLikedPortfolios(Long userId) { + // 获取所有包含PORTFOLIO_LIKE_KEY的键 + Set likedPortfolios = redisTemplate.keys(PORTFOLIO_LIKE_KEY + "*"); + + // 如果没有喜欢的,返回空列表 + if (likedPortfolios == null || likedPortfolios.isEmpty()) { + return new ArrayList<>(); + } + + // 过滤出包含指定用户ID的键,并提取投资组合ID + return likedPortfolios.stream() + .filter(key -> setOperations.isMember(key, userId.toString())) + .map(key -> Long.valueOf(key.replace(PORTFOLIO_LIKE_KEY, ""))) + .collect(Collectors.toList()); + } + + public void unLikePost(Long portfolioId, Long userId) { + setOperations.remove(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); + } + + // 检查用户是否喜欢某个作品 + public boolean isPostLikedByUser(Long portfolioId, Long userId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + Boolean isMember = setOperations.isMember(key, userId.toString()); + return isMember != null && isMember; + } + + public final static String PORTFOLIO_VIEW_KEY = "portfolio:view:"; + + public void increaseViewCount(Long portfolioId) { + String key = PORTFOLIO_VIEW_KEY + portfolioId; + increment(key, 1); + } + + public Long getViewCount(Long portfolioId) { + String key = PORTFOLIO_VIEW_KEY + portfolioId; + return getLong(key, 0L); + } + + public Long getViewCount(String key) { + return getLong(key, 0L); + } + + public final static String PERSONAL_HOMEPAGE_VIEW_KEY = "PersonalHomepage:view:"; + + public void increasePersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + increment(key, 1); + } + + public Long getPersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + return getLong(key, 0L); + } + + public final static String MOODBOARD_POSITION_KEY = "moodboard:position:"; + + public void saveMoodboardPosition(Long id, String moodboardPosition) { + setString(MOODBOARD_POSITION_KEY + id, moodboardPosition); + } + + public String getMoodboardPosition(Long id) { + return getString(MOODBOARD_POSITION_KEY + id); + } + + public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; + + public void increaseCount(String key) { + increment(key, 1); + } + + public Long getIncrementCount(String key) { + return getLong(key, 0L); + } + + public void setKeyExpire(String key, Long expire) { + redisTemplate.expire(key, expire, TimeUnit.DAYS); + } + + public final static String CHANGE_MAILBOX = "ChangeMailbox:"; + + // 每天允许通知3次 + public final static String UPLOAD_TIMEOUT_REMINDER_COUNTER = "UploadTimeoutReminderCounter"; + + public void addProcessId(String processId, int progress) { + // Redis 中的键,可以通过 processId 来唯一标识 + String redisKey = "process:progress:" + processId; + setInteger(redisKey, progress); + redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); + } + + public void addPathToCache(Long collectionId, Long userId, String path) { + // Redis 中的键,唯一标识由 collectionId 和 userId 组成 + String redisKey = "path:cache:" + collectionId + ":" + userId; + // 增加路径的计数 + hashOperations.increment(redisKey, path, 1); + // 设置过期时间为 2 小时(7200 秒) + redisTemplate.expire(redisKey, 2, TimeUnit.HOURS); + } + + public int getPathUsageCount(Long collectionId, Long userId, String path) { + String redisKey = "path:cache:" + collectionId + ":" + userId; + // 获取路径的使用次数 + Object count = hashOperations.get(redisKey, path); + return count != null ? ((Number) count).intValue() : 0; + } + + public void addAssembledObjects(Long collectionId, Set assembledObjects) { + // Redis 中的键,使用 collectionId 来唯一标识 + String redisKey = "collection:assembledObjects:" + collectionId; + + // 将 assembledObjects 转换为 JSON 格式存储,避免直接存储对象 + String assembledObjectsJson = convertToJson(assembledObjects); + if (assembledObjectsJson != null) { + // 使用 Redis 的 set 操作更新集合 + setString(redisKey, assembledObjectsJson); + // 设置过期时间为 5 分钟(300 秒) + redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES); + } + } + + private String convertToJson(Set assembledObjects) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(assembledObjects); + } catch (JsonProcessingException e) { + log.error("JSON转换失败", e); + return null; + } + } + + public Set getAssembledObjects(Long collectionId) { + String redisKey = "collection:assembledObjects:" + collectionId; + String assembledObjectsJson = getString(redisKey); + if (assembledObjectsJson == null) { + return new HashSet<>(); + } + return convertFromJson(assembledObjectsJson); + } + + private Set convertFromJson(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("JSON解析失败", e); + return new HashSet<>(); + } + } + + public final static String PAYMENT_INFO_LAST_SCAN_TIME = "PaymentInfoLastScanTime"; + public final static String AFFILIATE_LINK_VIEW_KEY = "AffiliateLink:view:"; + + public void increaseAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + increment(key, 1); + } + + public Long getAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + return getLong(key, 0L); + } + + public Long getAndSetKey(String key, Long count) { + Object oldValue = valueOperations.getAndSet(key, count); + if (oldValue instanceof Long) { + return (Long) oldValue; + } else if (oldValue instanceof Integer) { + return ((Integer) oldValue).longValue(); + } else if (oldValue instanceof String) { + try { + return Long.parseLong((String) oldValue); + } catch (NumberFormatException e) { + log.warn("无法将字符串转换为Long: key={}, value={}", key, oldValue); + return 0L; + } + } + return 0L; + } + + /** + * 记录任务的耗时到Redis + */ + public void recordTaskElapsedTime(String taskKey, long elapsedTime) { + String hashKey = "task:stats"; + hashOperations.increment(hashKey, taskKey + ":totalTime", elapsedTime); + hashOperations.increment(hashKey, taskKey + ":count", 1); + } + + /** + * 获取任务的平均耗时 + */ + public double getTaskAverageTime(String taskKey) { + String hashKey = "task:stats"; + Object totalTime = hashOperations.get(hashKey, taskKey + ":totalTime"); + Object count = hashOperations.get(hashKey, taskKey + ":count"); + + if (totalTime == null || count == null) { + return 0; + } + try { + double total = ((Number) totalTime).doubleValue(); + long cnt = ((Number) count).longValue(); + return cnt > 0 ? total / cnt : 0; + } catch (Exception e) { + log.warn("计算平均耗时失败: taskKey={}", taskKey, e); + return 0; + } + } + + /** + * 清除指定任务的统计数据 + * @param taskKey 任务标识,如 "taskA" + */ + public void clearTaskStats(String taskKey) { + String hashKey = "task:stats"; + // 删除总耗时和计数器 + hashOperations.delete(hashKey, taskKey + ":totalTime", taskKey + ":count"); + } + + public void recordTaskElapsedTime(String taskKey, double elapsedTimeInSeconds) { + // 将耗时转换为 BigDecimal,并四舍五入保留四位小数 + BigDecimal elapsedTime = new BigDecimal(elapsedTimeInSeconds).setScale(4, RoundingMode.HALF_UP); + hashOperations.increment("task:stats", taskKey + ":totalTime", elapsedTime.doubleValue()); + hashOperations.increment("task:stats", taskKey + ":count", 1); + } + + // 获取第一部分(Sketch)耗时 + public double getFirstSketchTime() { + Object time = hashOperations.get("task:stats", "firstSketchTime:totalTime"); + return time != null ? ((Number) time).doubleValue() : 0.0; + } + + // 获取第二部分(获取特征值)耗时 + public double getGetAttributeRecognitionTime() { + Object time = hashOperations.get("task:stats", "getAttributeRecognitionTime:totalTime"); + return time != null ? ((Number) time).doubleValue() : 0.0; + } + + // 获取第三部分(搭配 Sketch)耗时 + public double getOtherSketchTime() { + Object time = hashOperations.get("task:stats", "otherSketchTime:totalTime"); + return time != null ? ((Number) time).doubleValue() : 0.0; + } + + // 清理三部分的缓存 + public void clearTaskElapsedTimeCache() { + String hashKey = "task:stats"; + Object[] keysToDelete = { + "firstSketchTime:totalTime", "firstSketchTime:count", + "getAttributeRecognitionTime:totalTime", "getAttributeRecognitionTime:count", + "otherSketchTime:totalTime", "otherSketchTime:count" + }; + hashOperations.delete(hashKey, keysToDelete); + } + + public boolean incrementLikeCount(Long userId, String sketchPath) { + String redisKey = "user_like_count:" + userId; + try { + hashOperations.increment(redisKey, sketchPath, 1); + return true; + } catch (Exception e) { + log.error("Error incrementing like count for userId {} and sketchPath {}: {}", userId, sketchPath, e.getMessage()); + return false; + } + } + + public int getLikeCount(Long userId, String sketchPath) { + String redisKey = "user_like_count:" + userId; + Object count = hashOperations.get(redisKey, sketchPath); + return count != null ? ((Number) count).intValue() : 0; + } + + public void storeMaxLikeCount(Long userId, int maxLikeCount) { + String redisKey = "user_max_like_count:" + userId; + setInteger(redisKey, maxLikeCount); + } + + public int getMaxLikeCount(Long userId) { + String redisKey = "user_max_like_count:" + userId; + return getInteger(redisKey, 0); + } + + public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; + public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; + + public void batchDeleteKeysWithSamePrefix(String prefix) { + Set keys = redisTemplate.keys(prefix + "*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } + + public void setTaskProgressDTO(String taskId, ProgressDTO dto) { + String key = "task:progress:" + taskId; + setString(key, JSON.toJSONString(dto)); + redisTemplate.expire(key, 1, TimeUnit.DAYS); + } + + public ProgressDTO getTaskProgressDTO(String taskId) { + String key = "task:progress:" + taskId; + String json = getString(key); + if (StringUtils.isBlank(json)) { + return null; + } + try { + return JSON.parseObject(json, ProgressDTO.class); + } catch (Exception e) { + log.warn("任务进度解析失败 key={}, json={}", key, json); + return new ProgressDTO(0, 0, false); + } + } + + // Lua脚本 + private static final String RATE_LIMIT_SCRIPT = + "local current = redis.call('INCR', KEYS[1])\n" + + "local ttl = redis.call('TTL', KEYS[1])\n" + + "if tonumber(current) == 1 or tonumber(ttl) == -1 then\n" + + " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + + "end\n" + + "return tonumber(current) <= tonumber(ARGV[2])"; + + /** + * 检查是否允许发送 + * @param userId 用户ID + * @return true-允许发送,false-已超限 + */ + public boolean allowSend(Long userId) { + String hourKey = getCurrentHourKey(userId); + + // 执行Lua脚本 + List keys = Collections.singletonList(hourKey); + // 1小时过期,限制数量 一小时只能向普通用户发10封 + String[] args = new String[]{"3600", "10"}; + + Boolean result = redisTemplate.execute( + new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Boolean.class), + keys, + args + ); + + return Boolean.TRUE.equals(result); + } + + /** + * 获取当前小时的Key + */ + private String getCurrentHourKey(Long userId) { + String hour = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHH")); + return String.format("email_limit:%s:%s", userId, hour); + } + + /** + * 获取当前已发送数量 + */ + public int getCurrentCount(Long userId) { + String key = getCurrentHourKey(userId); + return getInteger(key, 0); + } + + public boolean allowRequest(String apiKey) { + String key = "rate_limit:" + apiKey; + // 使用Redis的INCR命令 + Long count = increment(key, 1); + if (count == 1) { + redisTemplate.expire(key, 1, TimeUnit.MINUTES); + } + return count <= 3; + } + + // 新增方法:安全删除key + public boolean safeDelete(String key) { + try { + return Boolean.TRUE.equals(redisTemplate.delete(key)); + } catch (Exception e) { + log.error("删除Redis key失败: {}", key, e); + return false; + } + } + + // 新增方法:批量设置过期时间 + public void batchExpire(Set keys, long timeout, TimeUnit unit) { + if (keys != null && !keys.isEmpty()) { + for (String key : keys) { + redisTemplate.expire(key, timeout, unit); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/da/controller/AccountController.java b/src/main/java/com/ai/da/controller/AccountController.java index 5dceae65..b6c434f8 100644 --- a/src/main/java/com/ai/da/controller/AccountController.java +++ b/src/main/java/com/ai/da/controller/AccountController.java @@ -12,8 +12,6 @@ import com.ai.da.model.vo.AccountPreLoginVO; import com.ai.da.model.vo.BindEmailVO; import com.ai.da.model.vo.PersonalHomepageVO; import com.ai.da.service.AccountService; -import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; -import io.minio.errors.MinioException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -26,7 +24,6 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; -import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/ai/da/model/dto/SubAccountPageDTO.java b/src/main/java/com/ai/da/model/dto/SubAccountPageDTO.java index 7a57d7b5..8a0f5762 100644 --- a/src/main/java/com/ai/da/model/dto/SubAccountPageDTO.java +++ b/src/main/java/com/ai/da/model/dto/SubAccountPageDTO.java @@ -3,13 +3,15 @@ package com.ai.da.model.dto; import com.ai.da.model.vo.PageQueryBaseVo; import lombok.Data; +import java.util.List; + @Data public class SubAccountPageDTO extends PageQueryBaseVo { private String startTime; private String endTime; - private String email; + private List email; - private String userName; + private List userName; } diff --git a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java index 691fa8ec..662557e3 100644 --- a/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AccountServiceImpl.java @@ -2160,13 +2160,13 @@ public class AccountServiceImpl extends ServiceImpl impl @Override public Boolean addSubAccount(AddSubAccountDTO addSubAccountDTO) { AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder(); - Account account = accountMapper.selectById(authPrincipalVo.getId()); - int subUserRole = getSubUserRole(account.getSystemUser()); + Account adminAcc = accountMapper.selectById(authPrincipalVo.getId()); + int subUserRole = getSubUserRole(adminAcc.getSystemUser()); if (addSubAccountDTO.getId() == null) { - return createSubAccount(addSubAccountDTO, account, subUserRole, addSubAccountDTO.getCreditsUsageLimit(), null); + return createSubAccount(addSubAccountDTO, adminAcc, subUserRole); } else { - return updateSubAccount(addSubAccountDTO, account, subUserRole); + return updateSubAccount(addSubAccountDTO, adminAcc, subUserRole); } } @@ -2182,135 +2182,184 @@ public class AccountServiceImpl extends ServiceImpl impl } @Transactional(rollbackFor = Exception.class) - public Boolean createSubAccount(AddSubAccountDTO addSubAccountDTO, Account account, int subUserRole, - BigDecimal creditsLimit, BigDecimal creditsUsage) { + public Boolean createSubAccount(AddSubAccountDTO addSubAccountDTO, Account adminAcc, int subUserRole) { QueryWrapper qw = new QueryWrapper<>(); - qw.lambda().eq(Account::getOrganizationName, account.getOrganizationName()); + qw.lambda().eq(Account::getOrganizationName, adminAcc.getOrganizationName()); List accounts = accountMapper.selectList(qw); // 校验子账号总数是否达上限 - if (account.getSubAccountNum() == null || account.getSubAccountNum() <= 0){ + if (adminAcc.getSubAccountNum() == null || adminAcc.getSubAccountNum() <= 0){ throw new BusinessException("Error: Sub-account quota reached (Max: 0). Upgrade to create more."); } - if (accounts.size() >= account.getSubAccountNum()) { - throw new BusinessException("Error: Sub-account quota reached (Max: " + account.getSubAccountNum() + "). Upgrade to create more."); + if (accounts.size() >= adminAcc.getSubAccountNum()) { + throw new BusinessException("Error: Sub-account quota reached (Max: " + adminAcc.getSubAccountNum() + "). Upgrade to create more."); + } + + if (StringUtil.isNullOrEmpty(addSubAccountDTO.getUserEmail())){ + throw new BusinessException("email.cannot.be.empty"); + } + + if (StringUtil.isNullOrEmpty(addSubAccountDTO.getUserPassword())){ + throw new BusinessException("password.cannot.be.empty"); } // 校验邮箱是否已加入组织 - if (isUserEmailExists(account.getOrganizationName(), addSubAccountDTO.getUserEmail())) { + if (isUserEmailExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserEmail())) { throw new BusinessException("This organization already has an account with the same email.", ResultEnum.PROMPT.getCode()); } // 校验用户名是否同名 - if (isUsernameExists(account.getOrganizationName(), addSubAccountDTO.getUserName())) { + if (isUsernameExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserName())) { throw new BusinessException("This organization already has an account with the same username."); } - // 之后是否需要检验密码不能设置为空 // 校验当前账号邮箱是否有个人账号 Account subAccount = accountMapper.selectOne(new QueryWrapper().eq("user_email", addSubAccountDTO.getUserEmail())); List personAccRole = Arrays.asList(0, 1, 2, 3); List orgAccRole = Arrays.asList(5, 6, 7, 8); + BigDecimal remainingCredits = adminRemainingCredits(adminAcc); + // 将个人账号加入组织 if (Objects.nonNull(subAccount) && personAccRole.contains(subAccount.getSystemUser())) { - log.info("将用户{} 加入组织{}", addSubAccountDTO.getUserEmail(), account.getOrganizationName()); + log.info("将用户{} 加入组织{}", addSubAccountDTO.getUserEmail(), adminAcc.getOrganizationName()); subAccount.setUserName(addSubAccountDTO.getUserName()); subAccount.setUserPassword(addSubAccountDTO.getUserPassword()); subAccount.setSystemUser(subUserRole); - subAccount.setOrganizationName(account.getOrganizationName()); - subAccount.setParentId(account.getId()); - if (Objects.nonNull(creditsLimit)){ - subAccount.setCreditsUsageLimit(creditsLimit); - subAccount.setCreditsUsage(creditsUsage); - if (Objects.nonNull(subAccount.getCredits())) { - subAccount.setCredits(subAccount.getCreditsUsageLimit().add(subAccount.getCredits())); + subAccount.setOrganizationName(adminAcc.getOrganizationName()); + subAccount.setParentId(adminAcc.getId()); + if (Objects.nonNull(subAccount.getCreditsUsageLimit())){ + if (remainingCredits.compareTo(subAccount.getCreditsUsageLimit()) < 0) { + throw new BusinessException("Insufficient credits (Balance: " + remainingCredits + ").", ResultEnum.PROMPT.getCode()); } - }else { - handleSubAccCredits(subAccount, account); - } - subAccount.setUpdateDate(new Date()); - updateById(subAccount); - updateById(account); - } else if (Objects.nonNull(subAccount) && orgAccRole.contains(subAccount.getSystemUser())) { - throw new BusinessException("邮箱 " + addSubAccountDTO.getUserEmail() + " 已加入其他组织", ResultEnum.PROMPT.getCode()); - } else { - subAccount = new Account(); - subAccount.setUserName(addSubAccountDTO.getUserName()); - subAccount.setUserEmail(addSubAccountDTO.getUserEmail()); - subAccount.setUserPassword(addSubAccountDTO.getUserPassword()); - if (Objects.nonNull(creditsLimit)){ - subAccount.setCreditsUsageLimit(creditsLimit); - subAccount.setCreditsUsage(creditsUsage); + subAccount.setCreditsUsageLimit(subAccount.getCreditsUsageLimit()); + subAccount.setCreditsUsage(subAccount.getCreditsUsageLimit()); if (Objects.nonNull(subAccount.getCredits())) { subAccount.setCredits(subAccount.getCreditsUsageLimit().add(subAccount.getCredits())); }else { subAccount.setCredits(subAccount.getCreditsUsageLimit()); } - } else { - handleSubAccCredits(subAccount, account); - updateById(account); + adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().add(subAccount.getCreditsUsageLimit())); + adminAcc.setCredits(adminAcc.getCreditsUsageLimit().subtract(adminAcc.getCreditsUsage())); + adminAcc.setUpdateDate(new Date()); + }else { + handleSubAccCredits(subAccount, adminAcc); + } + subAccount.setUpdateDate(new Date()); + updateById(subAccount); + updateById(adminAcc); + } + // 输入的账号已存在于其他组织 + else if (Objects.nonNull(subAccount) && orgAccRole.contains(subAccount.getSystemUser())) { + throw new BusinessException("邮箱 " + addSubAccountDTO.getUserEmail() + " 已加入其他组织", ResultEnum.PROMPT.getCode()); + } + // 完全新建一个账号 + else { + subAccount = new Account(); + + subAccount.setUserName(addSubAccountDTO.getUserName()); + subAccount.setUserEmail(addSubAccountDTO.getUserEmail()); + subAccount.setUserPassword(addSubAccountDTO.getUserPassword()); + + // 指定积分上限 + if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit())){ + if (remainingCredits.compareTo(addSubAccountDTO.getCreditsUsageLimit()) < 0) { + throw new BusinessException("Insufficient credits (Balance: " + remainingCredits + ").", ResultEnum.PROMPT.getCode()); + } + subAccount.setCreditsUsageLimit(addSubAccountDTO.getCreditsUsageLimit()); + subAccount.setCreditsUsage(Objects.isNull(addSubAccountDTO.getCreditsUsage()) ? BigDecimal.ZERO : addSubAccountDTO.getCreditsUsage()); + if (Objects.nonNull(subAccount.getCredits())) { + subAccount.setCredits(subAccount.getCreditsUsageLimit().add(subAccount.getCredits())); + }else { + subAccount.setCredits(subAccount.getCreditsUsageLimit()); + } + adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().add(subAccount.getCreditsUsageLimit())); + adminAcc.setCredits(adminAcc.getCreditsUsageLimit().subtract(adminAcc.getCreditsUsage())); + adminAcc.setUpdateDate(new Date()); + } + // 未指定积分使用上限 + else { + handleSubAccCredits(subAccount, adminAcc); + updateById(adminAcc); } subAccount.setSystemUser(subUserRole); subAccount.setLanguage(Language.ENGLISH.name()); subAccount.setCreateDate(new Date()); subAccount.setIsTrial(0); subAccount.setIsBeginner(1); - subAccount.setParentId(account.getId()); - subAccount.setOrganizationName(account.getOrganizationName()); + subAccount.setParentId(adminAcc.getId()); + subAccount.setOrganizationName(adminAcc.getOrganizationName()); accountMapper.insert(subAccount); } return Boolean.TRUE; } - private Boolean updateSubAccount(AddSubAccountDTO addSubAccountDTO, Account account, int subUserRole) { + private Boolean updateSubAccount(AddSubAccountDTO addSubAccountDTO, Account adminAcc, int subUserRole) { Account exAccountInfo = baseMapper.selectById(addSubAccountDTO.getId()); // 校验用户名是否同名 - if (!exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName()) && isUsernameExists(account.getOrganizationName(), addSubAccountDTO.getUserName())) { + if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserName()) + && !exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName()) + && isUsernameExists(adminAcc.getOrganizationName(), addSubAccountDTO.getUserName())) { throw new BusinessException("This organization already has an account with the same username."); - }else if (!exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName())){ + }else if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserName()) + && !exAccountInfo.getUserName().equals(addSubAccountDTO.getUserName())){ exAccountInfo.setUserName(addSubAccountDTO.getUserName()); } - // 判断积分变更是增加还是减少还是没变化 - if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit()) && exAccountInfo.getCreditsUsageLimit().compareTo(addSubAccountDTO.getCreditsUsageLimit()) < 0) { - BigDecimal remainingCredits = adminRemainingCredits(account); + // 判断积分变更是增加还是减少还是没变化,需修改子账号的credits\creditsUsageLimit,管理员账号的creditUsage + if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit()) + && exAccountInfo.getCreditsUsageLimit().compareTo(addSubAccountDTO.getCreditsUsageLimit()) < 0) { + BigDecimal remainingCredits = adminRemainingCredits(adminAcc); // 新增加的积分 BigDecimal addedCredits = addSubAccountDTO.getCreditsUsageLimit().subtract(exAccountInfo.getCreditsUsageLimit()); if (remainingCredits.compareTo(addedCredits) >= 0) { // 更新管理员已分配的积分 - account.setCreditsUsage(account.getCreditsUsage().add(addedCredits)); - // 更新子账号的积分上限 + adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().add(addedCredits)); + adminAcc.setCredits(adminAcc.getCreditsUsageLimit().subtract(adminAcc.getCreditsUsage())); + // 更新子账号的积分上限和目前所有积分总数 exAccountInfo.setCreditsUsageLimit(addSubAccountDTO.getCreditsUsageLimit()); + exAccountInfo.setCredits(exAccountInfo.getCredits().add(addedCredits)); } else { throw new BusinessException("Insufficient credits (Balance: " + remainingCredits + ").", ResultEnum.PROMPT.getCode()); } - } else if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit()) && exAccountInfo.getCreditsUsageLimit().compareTo(addSubAccountDTO.getCreditsUsageLimit()) > 0) { + } else if (Objects.nonNull(addSubAccountDTO.getCreditsUsageLimit()) + && exAccountInfo.getCreditsUsageLimit().compareTo(addSubAccountDTO.getCreditsUsageLimit()) > 0) { if (exAccountInfo.getCreditsUsage().compareTo(addSubAccountDTO.getCreditsUsageLimit()) > 0) { throw new BusinessException("Usage alert: " + exAccountInfo.getCreditsUsage() + " credits consumed this month. New limit must be ≥ " + exAccountInfo.getCreditsUsage() + " ."); } else { // 减少的积分 BigDecimal subtractedCredits = exAccountInfo.getCreditsUsageLimit().subtract(addSubAccountDTO.getCreditsUsageLimit()); // 更新管理员已分配的积分(积分回流) - account.setCreditsUsage(account.getCreditsUsage().subtract(subtractedCredits)); - // 更新子账号的积分上限 + adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().subtract(subtractedCredits)); + adminAcc.setCredits(adminAcc.getCreditsUsageLimit().subtract(adminAcc.getCreditsUsage())); + // 更新子账号的积分上限和目前所有积分总数 exAccountInfo.setCreditsUsageLimit(addSubAccountDTO.getCreditsUsageLimit()); + exAccountInfo.setCredits(exAccountInfo.getCredits().subtract(subtractedCredits)); } } + // 校验密码是否变更 + if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserPassword()) + && !exAccountInfo.getUserPassword().equals(addSubAccountDTO.getUserPassword())){ + exAccountInfo.setUserPassword(addSubAccountDTO.getUserPassword()); + } + // 校验邮箱是否变更 - if (!exAccountInfo.getUserEmail().equals(addSubAccountDTO.getUserEmail())) { + if (!StringUtil.isNullOrEmpty(addSubAccountDTO.getUserEmail()) + && !exAccountInfo.getUserEmail().equals(addSubAccountDTO.getUserEmail())) { // 原账号的积分使用上限 - BigDecimal creditsLimit = exAccountInfo.getCreditsUsageLimit(); +// BigDecimal creditsLimit = exAccountInfo.getCreditsUsageLimit(); + addSubAccountDTO.setCreditsUsageLimit(exAccountInfo.getCreditsUsageLimit()); // 原账号已使用的积分 - BigDecimal creditsUsage = exAccountInfo.getCreditsUsage(); +// BigDecimal creditsUsage = exAccountInfo.getCreditsUsage(); + addSubAccountDTO.setCreditsUsage(exAccountInfo.getCreditsUsage()); // 这里移除原账号,但是积分不回流,机构分配的积分会由下一个账号继续持有(包括积分上限和已使用的积分都保持不变) removeSubAccount(new AddSubAccountDTO(Collections.singletonList(addSubAccountDTO.getId())), false); // 移入新子账号(可能是移入,也可能是新增) - createSubAccount(addSubAccountDTO, account, subUserRole, creditsLimit, creditsUsage); + createSubAccount(addSubAccountDTO, adminAcc, subUserRole); } else { baseMapper.updateById(exAccountInfo); - baseMapper.updateById(account); + baseMapper.updateById(adminAcc); } return Boolean.TRUE; } @@ -2364,6 +2413,7 @@ public class AccountServiceImpl extends ServiceImpl impl subAcc.setCreditsUsageLimit(BigDecimal.ZERO); } adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().add(subAcc.getCreditsUsageLimit())); + adminAcc.setCredits(adminAcc.getCreditsUsageLimit().subtract(adminAcc.getCreditsUsage())); adminAcc.setUpdateDate(new Date()); log.debug("分配积分: subAccId={}, defaultCredits={}, remainingCredits={}", subAcc.getId(), defaultCredits, remainingCredits); @@ -2426,7 +2476,8 @@ public class AccountServiceImpl extends ServiceImpl impl } // 是否需要将积分回流 if (returnCredits && unusedCreditsTotal.compareTo(BigDecimal.ZERO) != 0){ - adminAcc.setCreditsUsage(adminAcc.getCreditsUsage().subtract(unusedCreditsTotal)); + BigDecimal subtracted = adminAcc.getCreditsUsage().subtract(unusedCreditsTotal); + adminAcc.setCreditsUsage(subtracted.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : subtracted); adminAcc.setUpdateDate(new Date()); baseMapper.updateById(adminAcc); } @@ -2462,12 +2513,17 @@ public class AccountServiceImpl extends ServiceImpl impl if (StringUtils.isNotBlank(subAccountPageDTO.getEndTime())) { qw.lambda().le(Account::getCreateDate, subAccountPageDTO.getEndTime()); } - if (StringUtils.isNotBlank(subAccountPageDTO.getEmail())) { - qw.lambda().like(Account::getUserEmail, subAccountPageDTO.getEmail()); + if (subAccountPageDTO.getEmail() != null && subAccountPageDTO.getEmail().size() == 1){ + qw.lambda().like(Account::getUserEmail, subAccountPageDTO.getEmail().get(0)); + }else if (subAccountPageDTO.getEmail() != null && subAccountPageDTO.getEmail().size() > 1){ + qw.lambda().in(Account::getUserEmail, subAccountPageDTO.getEmail()); } - if (StringUtils.isNotBlank(subAccountPageDTO.getUserName())) { - qw.lambda().like(Account::getUserName, subAccountPageDTO.getUserName()); + if (subAccountPageDTO.getUserName() != null && subAccountPageDTO.getUserName().size() == 1){ + qw.lambda().like(Account::getUserName, subAccountPageDTO.getUserName().get(0)); + }else if (subAccountPageDTO.getUserName() != null && subAccountPageDTO.getUserName().size() > 1){ + qw.lambda().in(Account::getUserName, subAccountPageDTO.getUserName()); } + // 执行分页查询 IPage page = accountMapper.selectPage(new Page<>(subAccountPageDTO.getPage(), subAccountPageDTO.getSize()), qw); diff --git a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java index 5c8fabe0..97feb56f 100644 --- a/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/AffiliateServiceImpl.java @@ -7,6 +7,7 @@ import com.ai.da.common.response.PageBaseResponse; import com.ai.da.common.response.ResultEnum; import com.ai.da.common.utils.CopyUtil; import com.ai.da.common.utils.RedisUtil; +import com.ai.da.common.utils.RedisUtilEnhance; import com.ai.da.common.utils.SendEmailUtil; import com.ai.da.mapper.primary.*; import com.ai.da.mapper.primary.entity.*; @@ -298,6 +299,9 @@ public class AffiliateServiceImpl extends ServiceImpl affiliateList = baseMapper.selectList(new QueryWrapper().lambda().eq(Affiliate::getStatus, "Active")); @@ -307,7 +311,7 @@ public class AffiliateServiceImpl extends ServiceImpl 0) { // 累加到数据库 diff --git a/src/main/resources/files/sub_account_import_template.xlsx b/src/main/resources/files/sub_account_import_template.xlsx index a891af8d348c294d196930b24d35be02eae5acfc..67c8c351b53abf3dd54f14fbdc27c72922f4a219 100644 GIT binary patch literal 11044 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 iYXIMe-=O^=