diff --git a/Readme.md b/Readme.md index 6d49d05..d006cf8 100644 --- a/Readme.md +++ b/Readme.md @@ -8,6 +8,7 @@ | 1.0.2 | 2025-06-09 | Luke.Ye | 添加用户角色相关逻辑,重构代码 | | 1.0.3 | 2025-06-16 | Luke.Ye | 新增文档上传至对象存储接口 | | 1.0.4 | 2025-06-17 | Luke.Ye | 上传文档记录入库 | +| 1.0.5 | 2025-06-19 | Luke.Ye | 文档导入ES逻辑优化 | diff --git a/src/main/java/com/knowledge/base/application/service/UserAppService.java b/src/main/java/com/knowledge/base/application/service/UserAppService.java index 150d52e..d3774fc 100644 --- a/src/main/java/com/knowledge/base/application/service/UserAppService.java +++ b/src/main/java/com/knowledge/base/application/service/UserAppService.java @@ -29,6 +29,8 @@ public interface UserAppService { boolean isValidToken(String token); Optional findToken(String token); + Optional findUserByToken(String token); + // 用户角色 List listUserRoles(Long userId); boolean addUserRole(Long userId, Long roleId); diff --git a/src/main/java/com/knowledge/base/application/service/UserAppServiceImpl.java b/src/main/java/com/knowledge/base/application/service/UserAppServiceImpl.java index 2ddde5f..dae497d 100644 --- a/src/main/java/com/knowledge/base/application/service/UserAppServiceImpl.java +++ b/src/main/java/com/knowledge/base/application/service/UserAppServiceImpl.java @@ -1,11 +1,14 @@ package com.knowledge.base.application.service; +import cn.hutool.json.JSONUtil; import com.google.common.collect.Lists; import com.knowledge.base.application.exceptions.AppException; import com.knowledge.base.domain.user.model.RoleDO; import com.knowledge.base.domain.user.model.UserDO; import com.knowledge.base.domain.user.model.UserRoleDO; +import com.knowledge.base.domain.user.model.UserTokenDO; import com.knowledge.base.domain.user.service.iface.UserDomainService; +import com.knowledge.base.infrastructure.cache.iface.UserCacheService; import com.knowledge.base.infrastructure.converter.*; import com.knowledge.base.infrastructure.north.dto.role.RoleDTO; import com.knowledge.base.infrastructure.north.dto.role.RoleFileRuleDTO; @@ -14,6 +17,8 @@ import com.knowledge.base.infrastructure.north.dto.user.UserDTO; import com.knowledge.base.infrastructure.north.dto.user.UserFileDTO; import com.knowledge.base.infrastructure.north.dto.user.UserTokenDTO; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.util.List; @@ -23,10 +28,13 @@ import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Slf4j public class UserAppServiceImpl implements UserAppService { private final UserDomainService userDomainService; + private final UserCacheService userCacheService; + @Override public boolean registerUser(String username, String password, List roleIds) { boolean ok = userDomainService.registerUser(username, password); @@ -119,6 +127,25 @@ public class UserAppServiceImpl implements UserAppService { return Optional.ofNullable(UserTokenDtoConverter.toDTO(userDomainService.findToken(token).get())); } + @Override + public Optional findUserByToken(String token) { + Optional cachedUserJson = userCacheService.getUserJsonByToken(token); + if(cachedUserJson.isPresent()) { + return Optional.ofNullable(JSONUtil.toBean(cachedUserJson.get(), UserDTO.class)); + } + + Optional userTokenOpt = userDomainService.findToken(token); + if (userTokenOpt.isEmpty()) { + log.warn("[findUserByToken] token无效: {}", token); + return Optional.empty(); + } + Optional userDO = userDomainService.findById(userTokenOpt.get().getUserId()); + UserDTO dto = UserDtoConverter.toDTO(userDO.get()); + // 数据进缓存 + userCacheService.cacheUserJsonByToken(token, JSONUtil.toJsonStr(dto)); + return Optional.ofNullable(dto); + } + @Override public List listUserRoles(Long userId) { return Optional.ofNullable(userDomainService.findRolesByUserId(userId)).orElse(Lists.newArrayList()).stream() diff --git a/src/main/java/com/knowledge/base/domain/doc/repository/iface/OSRecordRepository.java b/src/main/java/com/knowledge/base/domain/doc/repository/iface/OSRecordRepository.java index 4def7ab..3afc892 100644 --- a/src/main/java/com/knowledge/base/domain/doc/repository/iface/OSRecordRepository.java +++ b/src/main/java/com/knowledge/base/domain/doc/repository/iface/OSRecordRepository.java @@ -3,6 +3,8 @@ package com.knowledge.base.domain.doc.repository.iface; import com.knowledge.base.domain.common.model.PageResult; import com.knowledge.base.domain.doc.repository.po.OSRecord; +import java.util.Optional; + public interface OSRecordRepository { /** @@ -14,4 +16,11 @@ public interface OSRecordRepository { * 保存上传记录 */ void save(OSRecord po); + + /** + * 获取文档最新版本 + * @param + * @return + */ + Optional getLatestRecordByFileName(String relativeFilePath); } diff --git a/src/main/java/com/knowledge/base/domain/doc/service/iface/FileDomainService.java b/src/main/java/com/knowledge/base/domain/doc/service/iface/FileDomainService.java index 06606bb..3d11f24 100644 --- a/src/main/java/com/knowledge/base/domain/doc/service/iface/FileDomainService.java +++ b/src/main/java/com/knowledge/base/domain/doc/service/iface/FileDomainService.java @@ -3,7 +3,6 @@ package com.knowledge.base.domain.doc.service.iface; import com.knowledge.base.domain.common.model.PageResult; import com.knowledge.base.domain.doc.model.OSRecordDO; import com.knowledge.base.domain.doc.repository.po.File; -import com.knowledge.base.domain.doc.repository.po.OSRecord; import java.util.List; import java.util.Optional; @@ -25,4 +24,11 @@ public interface FileDomainService { * 保存上传记录 */ void save(OSRecordDO osRecord); + + /** + * 获取文档最新版本 + * @param + * @return + */ + Optional getLatestRecordByFileName(String relativeFilePath); } diff --git a/src/main/java/com/knowledge/base/domain/doc/service/impl/AbstractBaseFileImporter.java b/src/main/java/com/knowledge/base/domain/doc/service/impl/AbstractBaseFileImporter.java index 3477276..0c5e9d5 100644 --- a/src/main/java/com/knowledge/base/domain/doc/service/impl/AbstractBaseFileImporter.java +++ b/src/main/java/com/knowledge/base/domain/doc/service/impl/AbstractBaseFileImporter.java @@ -4,8 +4,10 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSON; import cn.hutool.json.JSONUtil; import com.knowledge.base.domain.common.enums.DocMetaPropEnum; +import com.knowledge.base.domain.doc.model.OSRecordDO; import com.knowledge.base.domain.doc.service.iface.DocumentImporter; -import com.knowledge.base.infrastructure.cache.FileCacheService; +import com.knowledge.base.domain.doc.service.iface.FileDomainService; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.config.ThreadPoolConfig; import com.knowledge.base.infrastructure.util.CacheUtil; @@ -21,15 +23,12 @@ import org.elasticsearch.client.RestHighLevelClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.nio.file.*; -import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.*; public abstract class AbstractBaseFileImporter implements DocumentImporter { @@ -42,6 +41,9 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter { @Autowired private FileCacheService fileCacheService; + @Autowired + private FileDomainService fileDomainService;; + protected abstract String getDirectoryPath(); protected abstract String getFileSuffix(); @@ -109,7 +111,7 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter { if (esMtime != null && Long.parseLong(esMtime.toString()) == localMTime) { logger.info("文件未变动,跳过导入: {}", relativePath); String metaJsonStr = transToMetaJson(existingSource); - fileCacheService.cacheMeta(relativePath, metaJsonStr, ConstantConfig.CACHE_EXPIRED_MINUTES); + fileCacheService.cacheMeta(relativePath, metaJsonStr, ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES); return; } DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, docId); @@ -127,12 +129,14 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter { if(metaJsonOpt.isPresent()) { Map metaMap = JSONUtil.toBean(metaJsonOpt.get(), Map.class); doc.put("uploader", CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.UPLOADER.code, String.class, ConstantConfig.DEFAULT_UPLOADER)); - doc.put("url", CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.ACCESS_URL.code, String.class, "")); + String cacheFileUrl = CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.ACCESS_URL.code, String.class, StrUtil.EMPTY); + doc.put("url", buildAccessUrl(cacheFileUrl, relativePath)); doc.put("expireTime", CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.EXPIRE_TIME.code, Long.class, DateUtil.toMillis(ConstantConfig.LONG_TERM_EXPIRE_TIME))); } else { doc.put("uploader",ConstantConfig.DEFAULT_UPLOADER); - doc.put("url", String.format("%s/%s", ConstantConfig.SHARE_BASE_URL, relativePath) ); + String url = buildAccessUrl(StrUtil.EMPTY, relativePath); + doc.put("url", url); doc.put("expireTime", DateUtil.toMillis(ConstantConfig.LONG_TERM_EXPIRE_TIME)); } @@ -144,12 +148,37 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter { logger.info("导入成功: {}", relativePath); // 信息入缓存 - fileCacheService.cacheMeta(relativePath, transToMetaJson(doc), ConstantConfig.CACHE_EXPIRED_MINUTES); + fileCacheService.cacheMeta(relativePath, transToMetaJson(doc), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES); } catch (Exception e) { logger.error("导入失败: {}", path, e); } } + private String buildAccessUrl(String originUrl, String relativePath) { + // 优先使用原始链接 + if(StrUtil.isNotBlank(originUrl)) { + return originUrl; + } + + if(StrUtil.isBlank(relativePath)) { + return StrUtil.EMPTY; + } + String accessUrl = originUrl; + if(relativePath.endsWith(".md")) { + // markdown直接拼接http链接 + accessUrl = String.format("%s/%s", ConstantConfig.SHARE_BASE_URL, relativePath); + accessUrl = accessUrl.substring(0, accessUrl.length() - 3) + ".html"; + } else { + // 其它文件从对象存储的DB中获取 + Optional latestRecordOpt = fileDomainService.getLatestRecordByFileName(relativePath); + if(latestRecordOpt.isPresent()) { + OSRecordDO latestRecord = latestRecordOpt.get(); + accessUrl = latestRecord.getUrl(); + } + } + return accessUrl; + } + private String transToMetaJson(Map esExistingSource) { return JSONUtil.toJsonStr(Map.of( DocMetaPropEnum.UPLOADER.code, Optional.ofNullable((String)esExistingSource.get("uploader")).orElse(ConstantConfig.DEFAULT_UPLOADER), diff --git a/src/main/java/com/knowledge/base/domain/doc/service/impl/FileDomainServiceImpl.java b/src/main/java/com/knowledge/base/domain/doc/service/impl/FileDomainServiceImpl.java index 66e04d8..60645d8 100644 --- a/src/main/java/com/knowledge/base/domain/doc/service/impl/FileDomainServiceImpl.java +++ b/src/main/java/com/knowledge/base/domain/doc/service/impl/FileDomainServiceImpl.java @@ -1,5 +1,6 @@ package com.knowledge.base.domain.doc.service.impl; +import cn.hutool.core.util.StrUtil; import com.knowledge.base.domain.common.model.PageResult; import com.knowledge.base.domain.doc.model.OSRecordDO; import com.knowledge.base.domain.doc.repository.iface.FileRepository; @@ -64,4 +65,16 @@ public class FileDomainServiceImpl implements FileDomainService { public void save(OSRecordDO osRecordDO) { osRecordRepository.save(BeanConvertUtil.convert(osRecordDO, OSRecord.class)); } + + @Override + public Optional getLatestRecordByFileName(String relativeFilePath) { + if(StrUtil.isBlank(relativeFilePath)) { + return Optional.empty(); + } + Optional osRecordOpt = osRecordRepository.getLatestRecordByFileName(relativeFilePath); + if(osRecordOpt.isEmpty()) { + return Optional.empty(); + } + return Optional.of(BeanConvertUtil.convert(osRecordOpt.get(), OSRecordDO.class)); + } } diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/FileCacheService.java b/src/main/java/com/knowledge/base/infrastructure/cache/FileCacheService.java deleted file mode 100644 index 7d4719f..0000000 --- a/src/main/java/com/knowledge/base/infrastructure/cache/FileCacheService.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.knowledge.base.infrastructure.cache; - -import java.util.Optional; - -/** - * 文件缓存服务接口(支持 Redis 或本地实现) - * - * @author Luke.ye - * @date 2025/6/7 - */ -public interface FileCacheService { - - /** - * 获取文件名对应的相对路径 - * - * @param filename 文件名 - * @return 相对路径(可能为 null) - */ - Optional getPath(String filename); - - /** - * 缓存文件名与相对路径的映射 - * - * @param filename 文件名 - * @param relativePath 相对路径 - * @param expireMinutes 过期时间(分钟) - */ - void cachePath(String filename, String relativePath, long expireMinutes); - - /** - * 缓存文件名与文件相关的元信息 - * @param filename 文件名,带后缀 - * @param jsonMeta 文件元信息,包含:上传人、可访问链接、可访问链接过期时间、文件写入本地时间等 - * @param expireMinutes 过期时间(分钟) - */ - default void cacheMeta(String filename, String jsonMeta, long expireMinutes) {}; - - /** - * 获取文件名对应的元信息 - * @param filename - * @return - */ - default Optional getMeta(String filename) { return null; }; - - /** - * 获取指定路径的上次修改时间(用于变更检测) - * - * @param relativePath 文件相对路径 - * @return 本地上次修改时间(可能为 null) - */ - Optional getMTime(String relativePath); - - /** - * 缓存指定路径的修改时间 - * - * @param relativePath 文件路径 - * @param mtime 修改时间 - * @param expireMinutes 过期时间(分钟) - */ - void cacheMTime(String relativePath, Long mtime, long expireMinutes); - - /** - * 清除文件名对应的路径缓存 - */ - void clearPath(String filename); - - /** - * 清除路径对应的修改时间缓存 - */ - void clearMTime(String relativePath); - - /** - * 清空所有缓存(注意:某些实现可能未实现) - */ - void clearAll(); -} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/LocalFileCacheService.java b/src/main/java/com/knowledge/base/infrastructure/cache/LocalFileCacheService.java deleted file mode 100644 index f5c34c9..0000000 --- a/src/main/java/com/knowledge/base/infrastructure/cache/LocalFileCacheService.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.knowledge.base.infrastructure.cache; - -import com.knowledge.base.infrastructure.util.LocalCacheUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -/** - * 使用本地缓存实现的文件缓存服务(非 Redis) - * - * @author Luke.ye - * @date 2025/6/7 - */ -@Slf4j -@Service -@ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "false") -public class LocalFileCacheService implements FileCacheService { - - @Value("${knowledge.base.local-cache.expire-minutes:1440}") - private long expireMinutes; - - private String key(String prefix, String key) { - return String.format("kb:file:%s:%s", prefix, key); - } - - @Override - public Optional getPath(String filename) { - Object val = LocalCacheUtil.get(key("path", filename)); - return val instanceof String ? Optional.of((String) val) : Optional.empty(); - } - - @Override - public void cachePath(String filename, String relativePath, long expireMinutes) { - LocalCacheUtil.put(key("path", filename), relativePath); - } - - @Override - public Optional getMTime(String relativePath) { - Object val = LocalCacheUtil.get(key("mtime", relativePath)); - if (val instanceof Long) return Optional.of((Long) val); - if (val instanceof Integer) return Optional.of(((Integer) val).longValue()); - return Optional.empty(); - } - - @Override - public void cacheMTime(String relativePath, Long mtime, long expireMinutes) { - LocalCacheUtil.put(key("mtime", relativePath), mtime); - } - - - @Override - public void cacheMeta(String filename, String jsonMeta, long expireMinutes) { - LocalCacheUtil.put(key("meta", filename), jsonMeta); - } - - @Override - public Optional getMeta(String filename) { - Object val = LocalCacheUtil.get(key("meta", filename)); - return val instanceof String ? Optional.of((String) val) : Optional.empty(); - } - - @Override - public void clearPath(String filename) { - LocalCacheUtil.remove(key("path", filename)); - } - - @Override - public void clearMTime(String relativePath) { - LocalCacheUtil.remove(key("mtime", relativePath)); - } - - @Override - public void clearAll() { - LocalCacheUtil.clearAll(); - } -} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/iface/FileCacheService.java b/src/main/java/com/knowledge/base/infrastructure/cache/iface/FileCacheService.java new file mode 100644 index 0000000..25c3d7b --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/cache/iface/FileCacheService.java @@ -0,0 +1,40 @@ +package com.knowledge.base.infrastructure.cache.iface; + +import java.util.Optional; + +/** + * 文件缓存服务接口(支持 Redis 或本地实现) + * + * @author Luke.ye + * @date 2025/6/7 + */ +public interface FileCacheService { + + /** + * 获取文件名对应的相对路径 + * + * @param filename 文件名 + * @return 相对路径(可能为 null) + */ + Optional getPath(String filename); + + /** + * 缓存文件名与文件相关的元信息 + * @param fnWithRelativePath 文件名,带相对路径和后缀 + * @param jsonMeta 文件元信息,包含:上传人、可访问链接、可访问链接过期时间、文件写入本地时间等 + * @param expireMinutes 过期时间(分钟) + */ + default void cacheMeta(String fnWithRelativePath, String jsonMeta, long expireMinutes) {}; + + /** + * 获取文件名对应的元信息 + * @param fnWithRelativePath + * @return + */ + default Optional getMeta(String fnWithRelativePath) { return null; }; + + /** + * 清空所有缓存(注意:某些实现可能未实现) + */ + void clearAll(); +} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/iface/UserCacheService.java b/src/main/java/com/knowledge/base/infrastructure/cache/iface/UserCacheService.java new file mode 100644 index 0000000..d314d47 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/cache/iface/UserCacheService.java @@ -0,0 +1,25 @@ +package com.knowledge.base.infrastructure.cache.iface; + +import java.util.Optional; + +/** + * @author Luke.ye + * @date 2025/6/19 09:59 + */ +public interface UserCacheService { + + /** + * 根据token获取User信息 + * + * @param token + * @return UserDTO的Json + */ + Optional getUserJsonByToken(String token); + + /** + * 将用户信息缓存 + * @param token + * @param userJsonStr + */ + void cacheUserJsonByToken(String token, String userJsonStr); +} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalFileCacheServiceImpl.java b/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalFileCacheServiceImpl.java new file mode 100644 index 0000000..1a47198 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalFileCacheServiceImpl.java @@ -0,0 +1,51 @@ +package com.knowledge.base.infrastructure.cache.impl; + +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; +import com.knowledge.base.infrastructure.util.LocalCacheUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * 使用本地缓存实现的文件缓存服务(非 Redis) + * + * @author Luke.ye + * @date 2025/6/7 + */ +@Slf4j +@Service +@ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "false") +public class LocalFileCacheServiceImpl implements FileCacheService { + + @Value("${knowledge.base.local-cache.expire-minutes:1440}") + private long expireMinutes; + + private String key(String prefix, String key) { + return String.format("kb:file:%s:%s", prefix, key); + } + + @Override + public Optional getPath(String filename) { + Object val = LocalCacheUtil.get(key("path", filename)); + return val instanceof String ? Optional.of((String) val) : Optional.empty(); + } + + @Override + public void cacheMeta(String fnWithRelativePath, String jsonMeta, long expireMinutes) { + LocalCacheUtil.put(key("meta", fnWithRelativePath), jsonMeta); + } + + @Override + public Optional getMeta(String fnWithRelativePath) { + Object val = LocalCacheUtil.get(key("meta", fnWithRelativePath)); + return val instanceof String ? Optional.of((String) val) : Optional.empty(); + } + + @Override + public void clearAll() { + LocalCacheUtil.clearAll(); + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalUserCacheServiceImpl.java b/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalUserCacheServiceImpl.java new file mode 100644 index 0000000..efec625 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/cache/impl/LocalUserCacheServiceImpl.java @@ -0,0 +1,27 @@ +package com.knowledge.base.infrastructure.cache.impl; + +import com.knowledge.base.infrastructure.cache.iface.UserCacheService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * @author Luke.ye + * @date 2025/6/19 10:01 + */ +@Slf4j +@Service +@ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "false") +public class LocalUserCacheServiceImpl implements UserCacheService { + @Override + public Optional getUserJsonByToken(String token) { + return Optional.empty(); + } + + @Override + public void cacheUserJsonByToken(String token, String userJsonStr) { + + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/RedisFileCacheService.java b/src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisFileCacheServiceImpl.java similarity index 61% rename from src/main/java/com/knowledge/base/infrastructure/cache/RedisFileCacheService.java rename to src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisFileCacheServiceImpl.java index 7c632e1..f1f29c8 100644 --- a/src/main/java/com/knowledge/base/infrastructure/cache/RedisFileCacheService.java +++ b/src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisFileCacheServiceImpl.java @@ -1,5 +1,6 @@ -package com.knowledge.base.infrastructure.cache; +package com.knowledge.base.infrastructure.cache.impl; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -21,7 +22,7 @@ import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "true", matchIfMissing = true) -public class RedisFileCacheService implements FileCacheService { +public class RedisFileCacheServiceImpl implements FileCacheService { private final StringRedisTemplate redisTemplate; @@ -38,47 +39,16 @@ public class RedisFileCacheService implements FileCacheService { } @Override - public void cachePath(String filename, String relativePath, long expireMinutes) { - redisTemplate.opsForValue().set(key("path", filename), relativePath, expireMinutes, TimeUnit.MINUTES); + public void cacheMeta(String fnWithRelativePath, String jsonMeta, long expireMinutes) { + redisTemplate.opsForValue().set(key("meta", fnWithRelativePath), jsonMeta, expireMinutes, TimeUnit.MINUTES); } @Override - public void cacheMeta(String filename, String jsonMeta, long expireMinutes) { - redisTemplate.opsForValue().set(key("meta", filename), jsonMeta, expireMinutes, TimeUnit.MINUTES); - } - - @Override - public Optional getMeta(String filename) { - String value = redisTemplate.opsForValue().get(key("meta", filename)); + public Optional getMeta(String fnWithRelativePath) { + String value = redisTemplate.opsForValue().get(key("meta", fnWithRelativePath)); return Optional.ofNullable(value); } - @Override - public Optional getMTime(String relativePath) { - String val = redisTemplate.opsForValue().get(key("mtime", relativePath)); - try { - return val != null ? Optional.of(Long.parseLong(val)) : Optional.empty(); - } catch (NumberFormatException e) { - log.warn("mtime 解析失败: {}", val, e); - return Optional.empty(); - } - } - - @Override - public void cacheMTime(String relativePath, Long mtime, long expireMinutes) { - redisTemplate.opsForValue().set(key("mtime", relativePath), String.valueOf(mtime), expireMinutes, TimeUnit.MINUTES); - } - - @Override - public void clearPath(String filename) { - redisTemplate.delete(key("path", filename)); - } - - @Override - public void clearMTime(String relativePath) { - redisTemplate.delete(key("mtime", relativePath)); - } - @Override public void clearAll() { log.warn("正在清空 Redis 文件缓存,前缀: {}", FILE_CACHE_PREFIX); diff --git a/src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisUserCacheServiceImpl.java b/src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisUserCacheServiceImpl.java new file mode 100644 index 0000000..e0def7f --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/cache/impl/RedisUserCacheServiceImpl.java @@ -0,0 +1,42 @@ +package com.knowledge.base.infrastructure.cache.impl; + +import com.knowledge.base.infrastructure.cache.iface.UserCacheService; +import com.knowledge.base.infrastructure.config.ConstantConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * @author Luke.ye + * @date 2025/6/19 10:01 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "knowledge.base.redis", name = "enable", havingValue = "true") +public class RedisUserCacheServiceImpl implements UserCacheService { + + private final StringRedisTemplate redisTemplate; + + private static final String USER_CACHE_PREFIX = "kb-user"; + + private String key(String prefix, String key) { + return String.format(USER_CACHE_PREFIX + ":%s:%s", prefix, key); + } + + @Override + public Optional getUserJsonByToken(String token) { + String value = redisTemplate.opsForValue().get(key("token", token)); + return Optional.ofNullable(value); + } + + @Override + public void cacheUserJsonByToken(String token, String userJsonStr) { + redisTemplate.opsForValue().set(key("token", token), userJsonStr, ConstantConfig.USER_CACHE_EXPIRED_MINUTES, TimeUnit.MINUTES); + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/config/ConstantConfig.java b/src/main/java/com/knowledge/base/infrastructure/config/ConstantConfig.java index a47bfc3..239dce6 100644 --- a/src/main/java/com/knowledge/base/infrastructure/config/ConstantConfig.java +++ b/src/main/java/com/knowledge/base/infrastructure/config/ConstantConfig.java @@ -19,7 +19,9 @@ public class ConstantConfig { public static final LocalDateTime LONG_TERM_EXPIRE_TIME = LocalDateTime.of(9999, 12, 31, 23, 59, 59); - public static final long CACHE_EXPIRED_MINUTES = Duration.ofDays(2).toMinutes(); + public static final long FILE_META_CACHE_EXPIRED_MINUTES = Duration.ofDays(2).toMinutes(); + + public static final long USER_CACHE_EXPIRED_MINUTES = Duration.ofDays(10).toMinutes(); public static final String SHARE_BASE_URL = "http://share.wisdompulse.cn/public"; diff --git a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileQueryController.java b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileQueryController.java index 7ea93b7..53f4f03 100644 --- a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileQueryController.java +++ b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileQueryController.java @@ -6,14 +6,16 @@ import cn.hutool.json.JSONUtil; import com.google.common.collect.Maps; import com.knowledge.base.application.service.DocAppService; import com.knowledge.base.application.service.UserAppService; +import com.knowledge.base.domain.common.enums.DocMetaPropEnum; import com.knowledge.base.domain.common.model.PageResult; -import com.knowledge.base.infrastructure.cache.FileCacheService; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.north.dto.SearchReq; import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO; import com.knowledge.base.infrastructure.north.dto.user.UserDTO; import com.knowledge.base.infrastructure.north.dto.user.UserFileDTO; import com.knowledge.base.infrastructure.north.dto.user.UserTokenDTO; +import com.knowledge.base.infrastructure.util.CacheUtil; import lombok.RequiredArgsConstructor; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -35,7 +37,6 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.time.Duration; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.regex.Pattern; @@ -89,11 +90,11 @@ public class FileQueryController { LOGGER.error("SearchController#resolveFilePath Error. Invalid Input: " + JSONUtil.toJsonStr(req)); return Maps.newHashMap(); } - List fileNames = (List)req.get("fileNames"); + List namesWithRelativePath = (List)req.get("fileNames"); Map filePaths = Maps.newHashMap(); - fileNames.forEach(name -> { - if(fileCacheService.getPath(name).isPresent()) { - filePaths.put(name, fileCacheService.getPath(name)); + namesWithRelativePath.forEach(pathFile -> { + if(fileCacheService.getMeta(pathFile).isPresent()) { + filePaths.put(pathFile, CacheUtil.loadFileMetaProp(pathFile, DocMetaPropEnum.ACCESS_URL.code, String.class)); } }); Map result = Maps.newHashMap(); @@ -121,7 +122,6 @@ public class FileQueryController { PageResult result = docAppService.pageQueryOSRecord(page, size, userDTO.get().getUsername()); List> data = new ArrayList<>(); - LocalDate today = LocalDate.now(); for (OSRecordDTO dto : result.getList()) { Map item = new LinkedHashMap<>(); @@ -183,9 +183,14 @@ public class FileQueryController { } outerQuery.minimumShouldMatch(1); + // 东八区时间点 + outerQuery.filter(QueryBuilders.rangeQuery("expireTime") + .gte("now") + .timeZone("+08:00")); return outerQuery; } + private Map buildSearchResponse(SearchResponse response, List keywords, int page, int size) { List> results = new ArrayList<>(); @@ -195,6 +200,8 @@ public class FileQueryController { result.put("filename", source.get("filename")); result.put("filepath", source.get("filepath")); result.put("mtime", source.get("mtime")); + result.put("uploader", source.getOrDefault("uploader", ConstantConfig.DEFAULT_UPLOADER)); + result.put("url", source.get("url")); String content = (String) source.get("content"); String summary = extractMultiSnippet(content, keywords, 50); diff --git a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java index 370251d..3b26be3 100644 --- a/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java +++ b/src/main/java/com/knowledge/base/infrastructure/north/controller/FileWriteController.java @@ -8,7 +8,7 @@ import com.knowledge.base.application.service.DocAppService; import com.knowledge.base.application.service.UserAppService; import com.knowledge.base.domain.common.enums.DocMetaPropEnum; import com.knowledge.base.domain.doc.service.iface.DocumentImporter; -import com.knowledge.base.infrastructure.cache.FileCacheService; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.config.ObjectStorageProperties; import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO; @@ -35,7 +35,6 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -84,42 +83,6 @@ public class FileWriteController { return ResponseEntity.badRequest().body("不支持的类型: " + type); } - - @DeleteMapping("/file/{fileName}") - public ResponseEntity deleteByFileName(@PathVariable String fileName) { - boolean esResult = false, cacheResult = false; - try { - // 1. 从缓存获取相对路径 - Optional relPathOpt = fileCacheService.getPath(fileName); - if (relPathOpt.isPresent()) { - String relativePath = relPathOpt.get(); - String docId = SafeIdUtil.encode(relativePath); - - // 2. 删除 ES - DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, docId); - // 强制刷新可选:deleteRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - esClient.delete(deleteRequest, RequestOptions.DEFAULT); - esResult = true; - - // 3. 删除缓存(文件名和mtime都清除) - fileCacheService.clearPath(fileName); - fileCacheService.clearMTime(relativePath); - cacheResult = true; - - logger.info("已删除文件 [{}] 的ES和缓存记录", fileName); - } else { - logger.warn("未找到缓存记录: {}", fileName); - } - return ResponseEntity.ok(Map.of( - "esDeleted", esResult, - "cacheDeleted", cacheResult - )); - } catch (Exception e) { - logger.error("删除失败: {}", fileName, e); - return ResponseEntity.status(500).body("删除失败: " + e.getMessage()); - } - } - @DeleteMapping("/files/clear-all") public ResponseEntity clearAllFiles() { try { @@ -217,6 +180,9 @@ public class FileWriteController { String uploader = userDTO.get().getUsername(); LocalDateTime now = LocalDateTime.now(); + String dateFolder = now.toLocalDate().toString(); + String relativeFilePath = String.format("%s/%s", dateFolder, originFileNameWithSuffix); + boolean isPublic = Optional.ofNullable(objectStorageProperties.getPublicBuckets()) .map(buckets -> buckets.contains(bucket)) .orElse(false); @@ -225,7 +191,7 @@ public class FileWriteController { : now.plusDays(7); OSRecordDTO dto = new OSRecordDTO(); - dto.setFileName(originFileNameWithSuffix); + dto.setFileName(relativeFilePath); dto.setUrl(url); dto.setUploader(uploader); dto.setBucketName(bucket); @@ -238,7 +204,6 @@ public class FileWriteController { // 可检索文件的类型 Set supportedTypes = Set.of("pdf", "doc", "docx", "xls", "xlsx"); if (searchable && supportedTypes.contains(suffix)) { - String dateFolder = now.toLocalDate().toString(); String localDir = Paths.get(objectStorageProperties.getLocalSearchablePath(), dateFolder).toString(); File localTargetDir = new File(localDir); if (!localTargetDir.exists()) { @@ -256,9 +221,8 @@ public class FileWriteController { cacheMeta.put(DocMetaPropEnum.UPLOAD_TIME.code, mtime); cacheMeta.put(DocMetaPropEnum.EXPIRE_TIME.code, DateUtil.toMillis(expireTime)); - String relativeFilePath = String.format("%s/%s", dateFolder, originFileNameWithSuffix); String metaJson = JSONUtil.toJsonStr(cacheMeta); - fileCacheService.cacheMeta(relativeFilePath, metaJson, ConstantConfig.CACHE_EXPIRED_MINUTES); + fileCacheService.cacheMeta(relativeFilePath, metaJson, ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES); logger.info("本地文件信息已保存并写入缓存: key = {}, value = {}", relativeFilePath, metaJson); } diff --git a/src/main/java/com/knowledge/base/infrastructure/north/controller/UserQueryController.java b/src/main/java/com/knowledge/base/infrastructure/north/controller/UserQueryController.java index 5745f03..bd4a966 100644 --- a/src/main/java/com/knowledge/base/infrastructure/north/controller/UserQueryController.java +++ b/src/main/java/com/knowledge/base/infrastructure/north/controller/UserQueryController.java @@ -3,6 +3,7 @@ package com.knowledge.base.infrastructure.north.controller; import cn.hutool.core.util.StrUtil; import com.google.common.collect.Lists; import com.knowledge.base.application.service.UserAppService; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.north.dto.role.RoleFileRuleDTO; import com.knowledge.base.infrastructure.north.dto.role.UserRoleDTO; @@ -29,6 +30,8 @@ public class UserQueryController { private final UserAppService userAppService; + private final FileCacheService fileCacheService; + @GetMapping("/check") public ResponseEntity check( @CookieValue(value = ConstantConfig.COOKIE_KEY, required = false) String cookieToken, diff --git a/src/main/java/com/knowledge/base/infrastructure/repository/persistence/doc/OSRecordRepositoryImpl.java b/src/main/java/com/knowledge/base/infrastructure/repository/persistence/doc/OSRecordRepositoryImpl.java index 84a5668..60c1212 100644 --- a/src/main/java/com/knowledge/base/infrastructure/repository/persistence/doc/OSRecordRepositoryImpl.java +++ b/src/main/java/com/knowledge/base/infrastructure/repository/persistence/doc/OSRecordRepositoryImpl.java @@ -2,6 +2,7 @@ package com.knowledge.base.infrastructure.repository.persistence.doc; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.knowledge.base.domain.common.model.PageResult; import com.knowledge.base.domain.doc.repository.iface.OSRecordRepository; @@ -10,6 +11,8 @@ import com.knowledge.base.infrastructure.repository.mapper.doc.OSRecordMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository @RequiredArgsConstructor public class OSRecordRepositoryImpl implements OSRecordRepository { @@ -34,4 +37,13 @@ public class OSRecordRepositoryImpl implements OSRecordRepository { public void save(OSRecord po) { osRecordMapper.insert(po); } + + @Override + public Optional getLatestRecordByFileName(String relativeFilePath) { + OSRecord fileLatestRecord = osRecordMapper.selectOne( + new QueryWrapper() + .eq("file_name", relativeFilePath) + .orderByDesc("upload_time")); + return Optional.ofNullable(fileLatestRecord); + } } diff --git a/src/main/java/com/knowledge/base/infrastructure/util/CacheUtil.java b/src/main/java/com/knowledge/base/infrastructure/util/CacheUtil.java index f1ea864..3832d07 100644 --- a/src/main/java/com/knowledge/base/infrastructure/util/CacheUtil.java +++ b/src/main/java/com/knowledge/base/infrastructure/util/CacheUtil.java @@ -4,7 +4,7 @@ import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; -import com.knowledge.base.infrastructure.cache.FileCacheService; +import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import lombok.extern.slf4j.Slf4j; import org.springframework.util.ObjectUtils;