diff --git a/src/main/java/com/ai/da/common/constant/CommonConstant.java b/src/main/java/com/ai/da/common/constant/CommonConstant.java index 90d4144b..dcd2c51f 100644 --- a/src/main/java/com/ai/da/common/constant/CommonConstant.java +++ b/src/main/java/com/ai/da/common/constant/CommonConstant.java @@ -22,7 +22,8 @@ public class CommonConstant { public static final Integer NUMBER_10080 = 10080; } - public static final String GENERATE_PATH = "/api/generate_image_flux2_klein"; + public static final String GENERATE_PATH = "api/generate_image"; + public static final String GENERATE_PATH_FLUX2_KLEIN = "/api/generate_image_flux2_klein"; public static final String GENERATE_SINGLE_LOGO = "/api/generate_single_logo"; 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 1264a1e4..c91411fa 100644 --- a/src/main/java/com/ai/da/common/utils/RedisUtil.java +++ b/src/main/java/com/ai/da/common/utils/RedisUtil.java @@ -1,659 +1,659 @@ -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 io.netty.util.internal.StringUtil; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.data.redis.core.ZSetOperations; -import org.springframework.data.redis.core.script.DefaultRedisScript; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - -import jakarta.annotation.Resource; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.Duration; -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 RedisUtil { - - @Resource - private RedisTemplate redisTemplate; - - public final static String FLUX_POLLING_URL = "Flux:"; - /** - * 登录 token 在 Redis 中的前缀: - * 最终 key 结构为 login:token:{userId} - */ - public final static String LOGIN_TOKEN_KEY = "login:token:"; - - public Boolean hasKey(String key){ - return redisTemplate.hasKey(key); - } - - //- - - - - - - - - - - - - - - - - - - - - ZSet类型 - - - - - - - - - - - - - - - - - - - - - - /** - * 向ZSet中添加元素 - */ - public void addToZSet(String key, String value, Double score) { - redisTemplate.opsForZSet().add(key, value, score); - } - - /** - * 从ZSet中删除元素 - */ - public void removeFromZSet(String key, String value) { - redisTemplate.opsForZSet().remove(key, value); - } - - /** - * 获取指定元素的当前排列顺序 - */ - public Long getRank(String key, String value) { - return redisTemplate.opsForZSet().rank(key, value); - } - - /** - * 获取当前ZSet中的最大score - */ - public Double getMaxScore(String key) { - Set> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); - - if (!CollectionUtils.isEmpty(set)) { - Double score = set.iterator().next().getScore(); - return score + 1.0; - } else { - return 1.0; - } - } - - /** - * 判断元素是否存在 - */ - public Boolean isElementExistsInZSet(String key, String value) { - return redisTemplate.opsForZSet().score(key, value) != null; - } - - /** - * 获取当前ZSet中数据量的总和 - */ - public Long getZSetTotalCount(String key) { - return redisTemplate.opsForZSet().zCard(key); - } - - - public Set getZSetTotalData(String key){ - return redisTemplate.opsForZSet().range(key, 0, -1); - } - - //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - - - public final static String VIDEO_FINISHED_TASKS = "VideoFinishedTasks"; - - /** - * 将数据放入set缓存 - */ - public void addToSet(String key, String value, Long expiresIn) { - redisTemplate.opsForSet().add(key, value); - // 设置过期时间 - redisTemplate.expire(key, expiresIn, TimeUnit.SECONDS); - } - - /** - * 弹出变量中的元素 - */ - public void removeFromSet(String key, String value) { - redisTemplate.opsForSet().remove(key, value); - } - - /** - * 检查给定的元素是否在变量中。 - */ - public Boolean isElementExistsInSet(String key, String obj) { - return redisTemplate.opsForSet().isMember(key, obj); - } - - - //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - - - /** - * 加入缓存 - */ - public void addToMap(String key, Map map) { - redisTemplate.opsForHash().putAll(key, map); - } - - /** - * 验证指定 key 下 有没有指定的 hashkey - */ - public Boolean isElementExistsInMap(String key, String hashKey) { - return redisTemplate.opsForHash().hasKey(key, hashKey); - } - - /** - * 获取指定key的值string - */ - public String getMapValue(String key1, String key2) { - return String.valueOf(redisTemplate.opsForHash().get(key1, key2)); - } - - /** - * 删除指定 hash 的 HashKey - * - * @return 删除成功的 数量 - */ - public Long removeFromMap(String key, String hashKeys) { - return redisTemplate.opsForHash().delete(key, hashKeys); - } - - //- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - - - public void addToString(String key, String value){ - redisTemplate.opsForValue().set(key,value); - } - - public void addToString(String key, String value, Long expiresIn){ - redisTemplate.opsForValue().set(key,value,expiresIn, TimeUnit.SECONDS); - } - - public String getFromString(String key){ - return redisTemplate.opsForValue().get(key); - } - - public Set getKeysFromString(String key){ - return redisTemplate.keys(key); - } - - public Long getSize(String key){return redisTemplate.opsForSet().size(key);} - - public List getMultiValue(Set keys){ - return redisTemplate.opsForValue().multiGet(keys); - } - - public Long getExpire(String key){ - return redisTemplate.getExpire(key); - } - - public void removeFromString(String key){ - redisTemplate.delete(key); - } - - /** - * 保存登录 token - * - * @param userId 用户 ID - * @param token token 字符串 - * @param expireMillis 过期时间(毫秒,通常与 JWT 保持一致) - */ - public void setLoginToken(Long userId, String token, long expireMillis) { - if (expireMillis <= 0) { - // 不设置过期时间,直到手动删除(不推荐) - addToString(LOGIN_TOKEN_KEY + userId, token); - return; - } - long expireSeconds = expireMillis / 1000; - if (expireSeconds <= 0) { - expireSeconds = 1; - } - addToString(LOGIN_TOKEN_KEY + userId, token, expireSeconds); - } - - /** - * 获取登录 token - */ - public String getLoginToken(Long userId) { - return getFromString(LOGIN_TOKEN_KEY + userId); - } - - /** - * 删除登录 token - */ - public void deleteLoginToken(Long userId) { - removeFromString(LOGIN_TOKEN_KEY + userId); - } - - public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:"; - - public void likePost(Long portfolioId, Long userId) { - redisTemplate.opsForSet().add(PORTFOLIO_LIKE_KEY + portfolioId, String.valueOf(userId)); - } - - public Long getLikeCount(Long portfolioId) { - String key = PORTFOLIO_LIKE_KEY + portfolioId; - return redisTemplate.opsForSet().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 -> redisTemplate.opsForSet().isMember(key, String.valueOf(userId))) - .map(key -> Long.valueOf(key.replace(PORTFOLIO_LIKE_KEY, ""))) - .collect(Collectors.toList()); - } - - public void unLikePost(Long portfolioId, Long userId) { - redisTemplate.opsForSet().remove(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); - } - - // 检查用户是否喜欢某个作品 - public boolean isPostLikedByUser(Long portfolioId, Long userId) { - String key = PORTFOLIO_LIKE_KEY + portfolioId; - Boolean isMember = redisTemplate.opsForSet().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; - redisTemplate.opsForValue().increment(key); - } - - public Long getViewCount(Long portfolioId) { - String key = PORTFOLIO_VIEW_KEY + portfolioId; - return redisTemplate.opsForValue().increment(key, 0); - } - - public Long getViewCount(String key) { - Object value = redisTemplate.opsForValue().get(key); - if (value instanceof Integer) { - return Long.valueOf((Integer) value); - } else if (value instanceof Long) { - return (Long) value; - } else if (value instanceof String) { - return Long.valueOf((String) value); - } else { - throw new IllegalArgumentException("Unexpected value type"); - } - } - - public final static String PERSONAL_HOMEPAGE_VIEW_KEY = "PersonalHomepage:view:"; - - public void increasePersonalHomepageViewCount(Long accountId) { - String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; - redisTemplate.opsForValue().increment(key); - } - - public Long getPersonalHomepageViewCount(Long accountId) { - String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; - return redisTemplate.opsForValue().increment(key, 0); - } - - public final static String MOODBOARD_POSITION_KEY = "moodboard:position:"; - - public void saveMoodboardPosition(Long id, String moodboardPosition) { - addToString(MOODBOARD_POSITION_KEY + id, moodboardPosition); - } - - public String getMoodboardPosition(Long id) { - return getFromString(MOODBOARD_POSITION_KEY + id); - } - public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; - public final static String UNNAMED_PROJECT_SEQ = "Project:UnnamedProjectSeq:"; - public Long increaseCount(String key) { - return redisTemplate.opsForValue().increment(key); - } - - public Long getIncrementCount(String key) { - return redisTemplate.opsForValue().increment(key, 0); - } - - 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; - - // 将当前进度存储到 Redis - redisTemplate.opsForValue().set(redisKey, String.valueOf(progress)); - - // 设置过期时间为 5 分钟(300 秒) - redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); - } - - public void addPathToCache(Long collectionId, Long userId, String path) { - // Redis 中的键,唯一标识由 collectionId 和 userId 组成 - String redisKey = "path:cache:" + collectionId + ":" + userId; - - // 增加路径的计数 - redisTemplate.opsForHash().increment(redisKey, path, 1); - - // 设置过期时间为 2 小时(7200 秒) - redisTemplate.expire(redisKey, 8, TimeUnit.HOURS); - } - - public int getPathUsageCount(Long collectionId, Long userId, String path) { - String redisKey = "path:cache:" + collectionId + ":" + userId; - - // 获取路径的使用次数 - Object count = redisTemplate.opsForHash().get(redisKey, path); - return count != null ? Integer.parseInt(count.toString()) : 0; - } - - public void addAssembledObjects(Long collectionId, Set assembledObjects) { - // Redis 中的键,使用 collectionId 来唯一标识 - String redisKey = "collection:assembledObjects:" + collectionId; - - // 将 assembledObjects 转换为 JSON 格式存储,避免直接存储对象 - String assembledObjectsJson = convertToJson(assembledObjects); - - // 使用 Redis 的 set 操作更新集合 - redisTemplate.opsForValue().set(redisKey, assembledObjectsJson); - - // 设置过期时间为 5 分钟(300 秒) - redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES); - } - - // 将 Set 转换为 JSON 格式 - private String convertToJson(Set assembledObjects) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writeValueAsString(assembledObjects); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return null; - } - } - - public Set getAssembledObjects(Long collectionId) { - // Redis 中的键,使用 collectionId 来唯一标识 - String redisKey = "collection:assembledObjects:" + collectionId; - - // 从 Redis 获取存储的 JSON 字符串 - String assembledObjectsJson = (String) redisTemplate.opsForValue().get(redisKey); - - if (assembledObjectsJson == null) { - return new HashSet<>(); // 如果没有找到数据,返回一个空的 Set - } - - // 将 JSON 字符串转换为 Set - return convertFromJson(assembledObjectsJson); - } - - // 将 JSON 字符串转换为 Set - private Set convertFromJson(String json) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - // 使用 TypeReference 来指定目标类型是 Set - return objectMapper.readValue(json, new TypeReference>() {}); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return new HashSet<>(); // 如果转换失败,返回空的 Set - } - } - - 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; - redisTemplate.opsForValue().increment(key); - } - - public Long getAffiliateLinkViewCount(Long accountId) { - String key = AFFILIATE_LINK_VIEW_KEY + accountId; - return redisTemplate.opsForValue().increment(key, 0); - } - - /** - * 记录任务的耗时到Redis - * @param taskKey 任务标识,如 "taskA" - * @param elapsedTime 本次耗时,单位为毫秒 - */ - public void recordTaskElapsedTime(String taskKey, long elapsedTime) { - String hashKey = "task:stats"; - - // 累加总耗时 - redisTemplate.opsForHash().increment(hashKey, taskKey + ":totalTime", elapsedTime); - - // 增加计数器 - redisTemplate.opsForHash().increment(hashKey, taskKey + ":count", 1); - } - - /** - * 获取任务的平均耗时 - * @param taskKey 任务标识,如 "taskA" - * @return 平均耗时(毫秒) - */ - public double getTaskAverageTime(String taskKey) { - String hashKey = "task:stats"; - - // 获取总耗时和计数 - Object totalTime = redisTemplate.opsForHash().get(hashKey, taskKey + ":totalTime"); - Object count = redisTemplate.opsForHash().get(hashKey, taskKey + ":count"); - - // 计算平均值 - if (totalTime == null || count == null) { - return 0; - } - return Double.parseDouble(totalTime.toString()) / Long.parseLong(count.toString()); - } - - /** - * 清除指定任务的统计数据 - * @param taskKey 任务标识,如 "taskA" - */ - public void clearTaskStats(String taskKey) { - String hashKey = "task:stats"; - - // 删除总耗时和计数器 - redisTemplate.opsForHash().delete(hashKey, taskKey + ":totalTime", taskKey + ":count"); - } - - public void recordTaskElapsedTime(String taskKey, double elapsedTimeInSeconds) { - // 将耗时转换为 BigDecimal,并四舍五入保留四位小数 - BigDecimal elapsedTime = new BigDecimal(elapsedTimeInSeconds).setScale(4, RoundingMode.HALF_UP); - - // 累加总耗时(以毫秒为单位) - redisTemplate.opsForHash().increment("task:stats", taskKey + ":totalTime", elapsedTime.doubleValue()); - - // 增加计数器 - redisTemplate.opsForHash().increment("task:stats", taskKey + ":count", 1); - } - - // 获取第一部分(Sketch)耗时 - public double getFirstSketchTime() { - // 获取 "firstSketchTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "firstSketchTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 获取第二部分(获取特征值)耗时 - public double getGetAttributeRecognitionTime() { - // 获取 "getAttributeRecognitionTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "getAttributeRecognitionTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 获取第三部分(搭配 Sketch)耗时 - public double getOtherSketchTime() { - // 获取 "otherSketchTime:totalTime" 对应的值,并返回(单位为秒) - Object time = redisTemplate.opsForHash().get("task:stats", "otherSketchTime:totalTime"); - return time != null ? (double) time : 0.0; - } - - // 清理三部分的缓存 - public void clearTaskElapsedTimeCache() { - // 删除第一部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:count"); - - // 删除第二部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:count"); - - // 删除第三部分的缓存 - redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:totalTime"); - redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:count"); - } - - public boolean incrementLikeCount(Long userId, String sketchPath) { - String redisKey = "user_like_count:" + userId; - try { - redisTemplate.opsForHash().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 = redisTemplate.opsForHash().get(redisKey, sketchPath); - return count != null ? Integer.parseInt(count.toString()) : 0; - } - - public void storeMaxLikeCount(Long userId, int maxLikeCount) { - String redisKey = "user_max_like_count:" + userId; - redisTemplate.opsForValue().set(redisKey, String.valueOf(maxLikeCount)); - } - - public int getMaxLikeCount(Long userId) { - String redisKey = "user_max_like_count:" + userId; - String maxLikeCount = redisTemplate.opsForValue().get(redisKey); - return maxLikeCount != null ? Integer.parseInt(maxLikeCount) : 0; - } - - public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; - - public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; - public final static String SUBSCRIPTION_SENT_EMAIL_TYPE = "SubscriptionEmailSentType:"; - - public void batchDeleteKeysWithSamePrefix(String prefix){ - Set keys = redisTemplate.keys(prefix + "*"); - assert keys != null; - if (!keys.isEmpty()){ - redisTemplate.delete(keys); - } - } - - public void setTaskProgressDTO(String taskId, ProgressDTO dto) { - String key = "task:progress:" + taskId; - redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), Duration.ofDays(1)); - } - - public ProgressDTO getTaskProgressDTO(String taskId) { - String key = "task:progress:" + taskId; - String json = redisTemplate.opsForValue().get(key); - if (StringUtils.isBlank(json)) { -// return new ProgressDTO(0, 0, false); - return null; - } - try { - return JSON.parseObject(json, ProgressDTO.class); - } catch (Exception e) { - log.warn("任务进度解析失败 key={}, json={}", key, json); - return new ProgressDTO(0, 0, false, null); - } - } - - // Lua脚本(原子化操作) - /*private static final String RATE_LIMIT_SCRIPT = - "local current = redis.call('INCR', KEYS[1])\n" + - "if tonumber(current) == 1 then\n" + - " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + - "end\n" + - "return tonumber(current) <= tonumber(ARGV[2])";*/ - 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); - List args = Arrays.asList( - 3600L, // 1小时过期 - 10L // 限制数量 一小时只能向普通用户发10封 - ); - - Boolean result = redisTemplate.execute( - new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Boolean.class), - keys, - args.toArray() - ); - - return Boolean.TRUE.equals(result); - } - - /** - * 获取当前小时的Key - * 格式:email_limit:{userId}:{yyyyMMddHH} - */ - 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); - String val = redisTemplate.opsForValue().get(key); - int count; - if (StringUtils.isBlank(val)){ - count = 0; - }else { - count = Integer.parseInt(val); - } - return count; - } - - public boolean allowRequest(String apiKey) { - String key = "rate_limit:" + apiKey; - ValueOperations ops = redisTemplate.opsForValue(); - - // 使用Redis的INCR命令 - Long count = ops.increment(key, 1); - - if (count == 1) { - // 第一次调用,设置过期时间 - redisTemplate.expire(key, 1, TimeUnit.MINUTES); - } - - return count <= 3; - } - +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 io.netty.util.internal.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Duration; +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 RedisUtil { + + @Resource + private RedisTemplate redisTemplate; + + public final static String FLUX_POLLING_URL = "Flux:"; + /** + * 登录 token 在 Redis 中的前缀: + * 最终 key 结构为 login:token:{userId} + */ + public final static String LOGIN_TOKEN_KEY = "login:token:"; + + public Boolean hasKey(String key){ + return redisTemplate.hasKey(key); + } + + //- - - - - - - - - - - - - - - - - - - - - ZSet类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 向ZSet中添加元素 + */ + public void addToZSet(String key, String value, Double score) { + redisTemplate.opsForZSet().add(key, value, score); + } + + /** + * 从ZSet中删除元素 + */ + public void removeFromZSet(String key, String value) { + redisTemplate.opsForZSet().remove(key, value); + } + + /** + * 获取指定元素的当前排列顺序 + */ + public Long getRank(String key, String value) { + return redisTemplate.opsForZSet().rank(key, value); + } + + /** + * 获取当前ZSet中的最大score + */ + public Double getMaxScore(String key) { + Set> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); + + if (!CollectionUtils.isEmpty(set)) { + Double score = set.iterator().next().getScore(); + return score + 1.0; + } else { + return 1.0; + } + } + + /** + * 判断元素是否存在 + */ + public Boolean isElementExistsInZSet(String key, String value) { + return redisTemplate.opsForZSet().score(key, value) != null; + } + + /** + * 获取当前ZSet中数据量的总和 + */ + public Long getZSetTotalCount(String key) { + return redisTemplate.opsForZSet().zCard(key); + } + + + public Set getZSetTotalData(String key){ + return redisTemplate.opsForZSet().range(key, 0, -1); + } + + //- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - - + + public final static String VIDEO_FINISHED_TASKS = "VideoFinishedTasks"; + + /** + * 将数据放入set缓存 + */ + public void addToSet(String key, String value, Long expiresIn) { + redisTemplate.opsForSet().add(key, value); + // 设置过期时间 + redisTemplate.expire(key, expiresIn, TimeUnit.SECONDS); + } + + /** + * 弹出变量中的元素 + */ + public void removeFromSet(String key, String value) { + redisTemplate.opsForSet().remove(key, value); + } + + /** + * 检查给定的元素是否在变量中。 + */ + public Boolean isElementExistsInSet(String key, String obj) { + return redisTemplate.opsForSet().isMember(key, obj); + } + + + //- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - - + + /** + * 加入缓存 + */ + public void addToMap(String key, Map map) { + redisTemplate.opsForHash().putAll(key, map); + } + + /** + * 验证指定 key 下 有没有指定的 hashkey + */ + public Boolean isElementExistsInMap(String key, String hashKey) { + return redisTemplate.opsForHash().hasKey(key, hashKey); + } + + /** + * 获取指定key的值string + */ + public String getMapValue(String key1, String key2) { + return String.valueOf(redisTemplate.opsForHash().get(key1, key2)); + } + + /** + * 删除指定 hash 的 HashKey + * + * @return 删除成功的 数量 + */ + public Long removeFromMap(String key, String hashKeys) { + return redisTemplate.opsForHash().delete(key, hashKeys); + } + + //- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - - + public void addToString(String key, String value){ + redisTemplate.opsForValue().set(key,value); + } + + public void addToString(String key, String value, Long expiresIn){ + redisTemplate.opsForValue().set(key,value,expiresIn, TimeUnit.SECONDS); + } + + public String getFromString(String key){ + return redisTemplate.opsForValue().get(key); + } + + public Set getKeysFromString(String key){ + return redisTemplate.keys(key); + } + + public Long getSize(String key){return redisTemplate.opsForSet().size(key);} + + public List getMultiValue(Set keys){ + return redisTemplate.opsForValue().multiGet(keys); + } + + public Long getExpire(String key){ + return redisTemplate.getExpire(key); + } + + public void removeFromString(String key){ + redisTemplate.delete(key); + } + + /** + * 保存登录 token + * + * @param userId 用户 ID + * @param token token 字符串 + * @param expireMillis 过期时间(毫秒,通常与 JWT 保持一致) + */ + public void setLoginToken(Long userId, String token, long expireMillis) { + if (expireMillis <= 0) { + // 不设置过期时间,直到手动删除(不推荐) + addToString(LOGIN_TOKEN_KEY + userId, token); + return; + } + long expireSeconds = expireMillis / 1000; + if (expireSeconds <= 0) { + expireSeconds = 1; + } + addToString(LOGIN_TOKEN_KEY + userId, token, expireSeconds); + } + + /** + * 获取登录 token + */ + public String getLoginToken(Long userId) { + return getFromString(LOGIN_TOKEN_KEY + userId); + } + + /** + * 删除登录 token + */ + public void deleteLoginToken(Long userId) { + removeFromString(LOGIN_TOKEN_KEY + userId); + } + + public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:"; + + public void likePost(Long portfolioId, Long userId) { + redisTemplate.opsForSet().add(PORTFOLIO_LIKE_KEY + portfolioId, String.valueOf(userId)); + } + + public Long getLikeCount(Long portfolioId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + return redisTemplate.opsForSet().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 -> redisTemplate.opsForSet().isMember(key, String.valueOf(userId))) + .map(key -> Long.valueOf(key.replace(PORTFOLIO_LIKE_KEY, ""))) + .collect(Collectors.toList()); + } + + public void unLikePost(Long portfolioId, Long userId) { + redisTemplate.opsForSet().remove(PORTFOLIO_LIKE_KEY + portfolioId, userId.toString()); + } + + // 检查用户是否喜欢某个作品 + public boolean isPostLikedByUser(Long portfolioId, Long userId) { + String key = PORTFOLIO_LIKE_KEY + portfolioId; + Boolean isMember = redisTemplate.opsForSet().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; + redisTemplate.opsForValue().increment(key); + } + + public Long getViewCount(Long portfolioId) { + String key = PORTFOLIO_VIEW_KEY + portfolioId; + return redisTemplate.opsForValue().increment(key, 0); + } + + public Long getViewCount(String key) { + Object value = redisTemplate.opsForValue().get(key); + if (value instanceof Integer) { + return Long.valueOf((Integer) value); + } else if (value instanceof Long) { + return (Long) value; + } else if (value instanceof String) { + return Long.valueOf((String) value); + } else { + throw new IllegalArgumentException("Unexpected value type"); + } + } + + public final static String PERSONAL_HOMEPAGE_VIEW_KEY = "PersonalHomepage:view:"; + + public void increasePersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + redisTemplate.opsForValue().increment(key); + } + + public Long getPersonalHomepageViewCount(Long accountId) { + String key = PERSONAL_HOMEPAGE_VIEW_KEY + accountId; + return redisTemplate.opsForValue().increment(key, 0); + } + + public final static String MOODBOARD_POSITION_KEY = "moodboard:position:"; + + public void saveMoodboardPosition(Long id, String moodboardPosition) { + addToString(MOODBOARD_POSITION_KEY + id, moodboardPosition); + } + + public String getMoodboardPosition(Long id) { + return getFromString(MOODBOARD_POSITION_KEY + id); + } + public final static String NICKNAME_MODIFY_TIMES = "NicknameModifyTimes:"; + public final static String UNNAMED_PROJECT_SEQ = "Project:UnnamedProjectSeq:"; + public Long increaseCount(String key) { + return redisTemplate.opsForValue().increment(key); + } + + public Long getIncrementCount(String key) { + return redisTemplate.opsForValue().increment(key, 0); + } + + 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; + + // 将当前进度存储到 Redis + redisTemplate.opsForValue().set(redisKey, String.valueOf(progress)); + + // 设置过期时间为 5 分钟(300 秒) + redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); + } + + public void addPathToCache(Long collectionId, Long userId, String path) { + // Redis 中的键,唯一标识由 collectionId 和 userId 组成 + String redisKey = "path:cache:" + collectionId + ":" + userId; + + // 增加路径的计数 + redisTemplate.opsForHash().increment(redisKey, path, 1); + + // 设置过期时间为 2 小时(7200 秒) + redisTemplate.expire(redisKey, 8, TimeUnit.HOURS); + } + + public int getPathUsageCount(Long collectionId, Long userId, String path) { + String redisKey = "path:cache:" + collectionId + ":" + userId; + + // 获取路径的使用次数 + Object count = redisTemplate.opsForHash().get(redisKey, path); + return count != null ? Integer.parseInt(count.toString()) : 0; + } + + public void addAssembledObjects(Long collectionId, Set assembledObjects) { + // Redis 中的键,使用 collectionId 来唯一标识 + String redisKey = "collection:assembledObjects:" + collectionId; + + // 将 assembledObjects 转换为 JSON 格式存储,避免直接存储对象 + String assembledObjectsJson = convertToJson(assembledObjects); + + // 使用 Redis 的 set 操作更新集合 + redisTemplate.opsForValue().set(redisKey, assembledObjectsJson); + + // 设置过期时间为 5 分钟(300 秒) + redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES); + } + + // 将 Set 转换为 JSON 格式 + private String convertToJson(Set assembledObjects) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(assembledObjects); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } + + public Set getAssembledObjects(Long collectionId) { + // Redis 中的键,使用 collectionId 来唯一标识 + String redisKey = "collection:assembledObjects:" + collectionId; + + // 从 Redis 获取存储的 JSON 字符串 + String assembledObjectsJson = (String) redisTemplate.opsForValue().get(redisKey); + + if (assembledObjectsJson == null) { + return new HashSet<>(); // 如果没有找到数据,返回一个空的 Set + } + + // 将 JSON 字符串转换为 Set + return convertFromJson(assembledObjectsJson); + } + + // 将 JSON 字符串转换为 Set + private Set convertFromJson(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + // 使用 TypeReference 来指定目标类型是 Set + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return new HashSet<>(); // 如果转换失败,返回空的 Set + } + } + + 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; + redisTemplate.opsForValue().increment(key); + } + + public Long getAffiliateLinkViewCount(Long accountId) { + String key = AFFILIATE_LINK_VIEW_KEY + accountId; + return redisTemplate.opsForValue().increment(key, 0); + } + + /** + * 记录任务的耗时到Redis + * @param taskKey 任务标识,如 "taskA" + * @param elapsedTime 本次耗时,单位为毫秒 + */ + public void recordTaskElapsedTime(String taskKey, long elapsedTime) { + String hashKey = "task:stats"; + + // 累加总耗时 + redisTemplate.opsForHash().increment(hashKey, taskKey + ":totalTime", elapsedTime); + + // 增加计数器 + redisTemplate.opsForHash().increment(hashKey, taskKey + ":count", 1); + } + + /** + * 获取任务的平均耗时 + * @param taskKey 任务标识,如 "taskA" + * @return 平均耗时(毫秒) + */ + public double getTaskAverageTime(String taskKey) { + String hashKey = "task:stats"; + + // 获取总耗时和计数 + Object totalTime = redisTemplate.opsForHash().get(hashKey, taskKey + ":totalTime"); + Object count = redisTemplate.opsForHash().get(hashKey, taskKey + ":count"); + + // 计算平均值 + if (totalTime == null || count == null) { + return 0; + } + return Double.parseDouble(totalTime.toString()) / Long.parseLong(count.toString()); + } + + /** + * 清除指定任务的统计数据 + * @param taskKey 任务标识,如 "taskA" + */ + public void clearTaskStats(String taskKey) { + String hashKey = "task:stats"; + + // 删除总耗时和计数器 + redisTemplate.opsForHash().delete(hashKey, taskKey + ":totalTime", taskKey + ":count"); + } + + public void recordTaskElapsedTime(String taskKey, double elapsedTimeInSeconds) { + // 将耗时转换为 BigDecimal,并四舍五入保留四位小数 + BigDecimal elapsedTime = new BigDecimal(elapsedTimeInSeconds).setScale(4, RoundingMode.HALF_UP); + + // 累加总耗时(以毫秒为单位) + redisTemplate.opsForHash().increment("task:stats", taskKey + ":totalTime", elapsedTime.doubleValue()); + + // 增加计数器 + redisTemplate.opsForHash().increment("task:stats", taskKey + ":count", 1); + } + + // 获取第一部分(Sketch)耗时 + public double getFirstSketchTime() { + // 获取 "firstSketchTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "firstSketchTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 获取第二部分(获取特征值)耗时 + public double getGetAttributeRecognitionTime() { + // 获取 "getAttributeRecognitionTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "getAttributeRecognitionTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 获取第三部分(搭配 Sketch)耗时 + public double getOtherSketchTime() { + // 获取 "otherSketchTime:totalTime" 对应的值,并返回(单位为秒) + Object time = redisTemplate.opsForHash().get("task:stats", "otherSketchTime:totalTime"); + return time != null ? (double) time : 0.0; + } + + // 清理三部分的缓存 + public void clearTaskElapsedTimeCache() { + // 删除第一部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "firstSketchTime:count"); + + // 删除第二部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "getAttributeRecognitionTime:count"); + + // 删除第三部分的缓存 + redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:totalTime"); + redisTemplate.opsForHash().delete("task:stats", "otherSketchTime:count"); + } + + public boolean incrementLikeCount(Long userId, String sketchPath) { + String redisKey = "user_like_count:" + userId; + try { + redisTemplate.opsForHash().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 = redisTemplate.opsForHash().get(redisKey, sketchPath); + return count != null ? Integer.parseInt(count.toString()) : 0; + } + + public void storeMaxLikeCount(Long userId, int maxLikeCount) { + String redisKey = "user_max_like_count:" + userId; + redisTemplate.opsForValue().set(redisKey, String.valueOf(maxLikeCount)); + } + + public int getMaxLikeCount(Long userId) { + String redisKey = "user_max_like_count:" + userId; + String maxLikeCount = redisTemplate.opsForValue().get(redisKey); + return maxLikeCount != null ? Integer.parseInt(maxLikeCount) : 0; + } + + public final static String IMAGE_SEGMENTATION = "ImageSegmentation:"; + + public final static String STRIPE_EXCEPTION_LOG = "StripeException:"; + public final static String SUBSCRIPTION_SENT_EMAIL_TYPE = "SubscriptionEmailSentType:"; + + public void batchDeleteKeysWithSamePrefix(String prefix){ + Set keys = redisTemplate.keys(prefix + "*"); + assert keys != null; + if (!keys.isEmpty()){ + redisTemplate.delete(keys); + } + } + + public void setTaskProgressDTO(String taskId, ProgressDTO dto) { + String key = "task:progress:" + taskId; + redisTemplate.opsForValue().set(key, JSON.toJSONString(dto), Duration.ofDays(1)); + } + + public ProgressDTO getTaskProgressDTO(String taskId) { + String key = "task:progress:" + taskId; + String json = redisTemplate.opsForValue().get(key); + if (StringUtils.isBlank(json)) { +// return new ProgressDTO(0, 0, false); + return null; + } + try { + return JSON.parseObject(json, ProgressDTO.class); + } catch (Exception e) { + log.warn("任务进度解析失败 key={}, json={}", key, json); + return new ProgressDTO(0, 0, false, null); + } + } + + // Lua脚本(原子化操作) + /*private static final String RATE_LIMIT_SCRIPT = + "local current = redis.call('INCR', KEYS[1])\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + + "end\n" + + "return tonumber(current) <= tonumber(ARGV[2])";*/ + 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); + List args = Arrays.asList( + 3600L, // 1小时过期 + 10L // 限制数量 一小时只能向普通用户发10封 + ); + + Boolean result = redisTemplate.execute( + new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Boolean.class), + keys, + args.toArray() + ); + + return Boolean.TRUE.equals(result); + } + + /** + * 获取当前小时的Key + * 格式:email_limit:{userId}:{yyyyMMddHH} + */ + 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); + String val = redisTemplate.opsForValue().get(key); + int count; + if (StringUtils.isBlank(val)){ + count = 0; + }else { + count = Integer.parseInt(val); + } + return count; + } + + public boolean allowRequest(String apiKey) { + String key = "rate_limit:" + apiKey; + ValueOperations ops = redisTemplate.opsForValue(); + + // 使用Redis的INCR命令 + Long count = ops.increment(key, 1); + + if (count == 1) { + // 第一次调用,设置过期时间 + redisTemplate.expire(key, 1, TimeUnit.MINUTES); + } + + return count <= 3; + } + } \ No newline at end of file diff --git a/src/main/java/com/ai/da/python/PythonService.java b/src/main/java/com/ai/da/python/PythonService.java index f20b0ae2..3ffad287 100644 --- a/src/main/java/com/ai/da/python/PythonService.java +++ b/src/main/java/com/ai/da/python/PythonService.java @@ -3405,7 +3405,7 @@ public class PythonService { if (result && jsonObject.get("code").equals(200)) { log.info("Generate##responseObject###{}", jsonObject); // return setGenerateImageList(jsonObject.getJSONObject("data")); - if (servicePath== CommonConstant.GENERATE_PATH){ + if (servicePath== CommonConstant.GENERATE_PATH_FLUX2_KLEIN){ //放入结果到mq JSONObject data = jsonObject.getJSONObject("data"); String outputPath = data.getString("output_path"); diff --git a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java index 3d443151..42f7a5c7 100644 --- a/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java +++ b/src/main/java/com/ai/da/service/impl/GenerateServiceImpl.java @@ -48,6 +48,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.apache.commons.io.IOUtils; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; @@ -241,16 +243,12 @@ public class GenerateServiceImpl extends ServiceImpl i jsonString = JSON.toJSONString(params, SerializerFeature.WriteMapNullValue); break; case "Pattern": - // 构建object_name: {userId}/{category}/{uuid}.png - String objectName = generateThroughImageTextDTO.getUserId() + "/" + category + "/" + UUID.randomUUID() + ".png"; - - ImageProcessRequest imageProcessRequest = ImageProcessRequest.builder() - .object_name(objectName) - .bucket_name(userBucket) - .prompt(text).build(); - jsonString = JSON.toJSONString(imageProcessRequest); + GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(), + mode, category, generateThroughImageTextDTO.getGender(), version); + jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue); } } else { + path = CommonConstant.GENERATE_PATH_FLUX2_KLEIN; // 构建object_name: {userId}/{category}/{uuid}.png String objectName = generateThroughImageTextDTO.getUserId() + "/" + category + "/" + UUID.randomUUID() + ".png"; @@ -278,9 +276,16 @@ public class GenerateServiceImpl extends ServiceImpl i } public void saveGenerateImmediately(Generate generate) { save(generate); - // 写入完成后设锁,通知 MQ 消费者可以安全读取 - String lockKey = "generate:lock:" + generate.getUniqueId(); - redisUtil.addToString(lockKey, "1", 60L); + // 使用 TransactionSynchronizationManager 在事务真正提交后再设锁 + // 否则 save() 完成后事务尚未 commit,MQ 消费者立即读到 null + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + String lockKey = "generate:lock:" + generate.getUniqueId(); + redisUtil.addToString(lockKey, "1", 60L); + log.debug("Save lock set after commit for uniqueId: {}", generate.getUniqueId()); + } + }); } private void waitForSaveLock(String uniqueId) {