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 a891af8d..67c8c351 100644 Binary files a/src/main/resources/files/sub_account_import_template.xlsx and b/src/main/resources/files/sub_account_import_template.xlsx differ