This commit is contained in:
luke 2025-06-20 01:05:21 +08:00
parent f1d54c107e
commit b0ea0f5ffa
17 changed files with 254 additions and 106 deletions

View File

@ -0,0 +1,45 @@
package com.knowledge.base.domain.doc.service;
import com.knowledge.base.domain.doc.service.impl.AbstractBaseFileImporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.file.Path;
import java.util.*;
/**
* 文件导入分派器可根据文件后缀名的不同执行不同的导入逻辑
*/
@Component
public class FileImporterDispatcher {
@Autowired
private List<AbstractBaseFileImporter> fileImporters;
private final Map<String, AbstractBaseFileImporter> suffixToImporter = new HashMap<>();
@PostConstruct
public void init() {
for (AbstractBaseFileImporter importer : fileImporters) {
for (String suffix : importer.getFileSuffixes()) {
suffixToImporter.put(suffix.toLowerCase(), importer);
}
}
}
public boolean importSingleFile(Path filePath, Path excludePrefix, Map<String, Object> extInfo) {
String fileName = filePath.getFileName().toString().toLowerCase();
Optional<String> matchedSuffix = suffixToImporter.keySet().stream()
.filter(fileName::endsWith)
.findFirst();
if (matchedSuffix.isEmpty()) {
throw new IllegalArgumentException("不支持的文件后缀: " + fileName);
}
AbstractBaseFileImporter importer = suffixToImporter.get(matchedSuffix.get());
return importer.insertOrUpdateOneFileIntoES(filePath, excludePrefix, extInfo);
}
}

View File

@ -2,6 +2,9 @@ package com.knowledge.base.domain.doc.service.iface;
import com.knowledge.base.domain.common.enums.DocTypeEnum; import com.knowledge.base.domain.common.enums.DocTypeEnum;
import java.nio.file.Path;
import java.util.Map;
/** /**
* @author Luke.ye * @author Luke.ye
* @date 2025/5/20 09:02 * @date 2025/5/20 09:02
@ -14,4 +17,12 @@ public interface DocumentImporter {
* @return * @return
*/ */
String getType(); String getType();
/**
* 将文件插入/更新到ES中
* @param absoluteFilePath
* @param excludeFilePrefix
* @return
*/
default boolean insertOrUpdateOneFileIntoES(Path absoluteFilePath, Path excludeFilePrefix, Map<String, Object> extInfo) { return true; }
} }

View File

@ -1,8 +1,11 @@
package com.knowledge.base.domain.doc.service.impl; package com.knowledge.base.domain.doc.service.impl;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON; import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.knowledge.base.domain.common.enums.DocMetaPropEnum; import com.knowledge.base.domain.common.enums.DocMetaPropEnum;
import com.knowledge.base.domain.doc.model.OSRecordDO; import com.knowledge.base.domain.doc.model.OSRecordDO;
import com.knowledge.base.domain.doc.service.iface.DocumentImporter; import com.knowledge.base.domain.doc.service.iface.DocumentImporter;
@ -10,10 +13,7 @@ import com.knowledge.base.domain.doc.service.iface.FileDomainService;
import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.cache.iface.FileCacheService;
import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.config.ConstantConfig;
import com.knowledge.base.infrastructure.config.ThreadPoolConfig; import com.knowledge.base.infrastructure.config.ThreadPoolConfig;
import com.knowledge.base.infrastructure.util.CacheUtil; import com.knowledge.base.infrastructure.util.*;
import com.knowledge.base.infrastructure.util.DateUtil;
import com.knowledge.base.infrastructure.util.SafeIdUtil;
import com.knowledge.base.infrastructure.util.ThreadPoolUtil;
import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.get.GetResponse;
@ -29,6 +29,7 @@ import java.nio.file.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
public abstract class AbstractBaseFileImporter implements DocumentImporter { public abstract class AbstractBaseFileImporter implements DocumentImporter {
@ -44,9 +45,12 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
@Autowired @Autowired
private FileDomainService fileDomainService;; private FileDomainService fileDomainService;;
@Autowired
private RateLimiterManager rateLimiterManager;
protected abstract String getDirectoryPath(); protected abstract String getDirectoryPath();
protected abstract String getFileSuffix(); public abstract Set<String> getFileSuffixes();
protected abstract String getDocTypeCode(); protected abstract String getDocTypeCode();
@ -58,50 +62,72 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
Path excludeBase = Paths.get(getExcludePrefix()).toAbsolutePath().normalize(); Path excludeBase = Paths.get(getExcludePrefix()).toAbsolutePath().normalize();
Files.walk(basePath) Files.walk(basePath)
.filter(p -> p.toString().toLowerCase().endsWith(getFileSuffix())) .filter(Files::isRegularFile)
.filter(path -> {
String fileName = path.getFileName().toString().toLowerCase();
return getFileSuffixes().stream().anyMatch(fileName::endsWith);
})
.forEach(path -> { .forEach(path -> {
ThreadPoolUtil.execute(() -> processFile(path, excludeBase), ThreadPoolConfig.IMPORT_DOC_POOL); rateLimiterManager.getRateLimiter().acquire();
try { ThreadPoolUtil.execute(() -> insertOrUpdateOneFileIntoES(path, excludeBase, Maps.newHashMap()), ThreadPoolConfig.IMPORT_DOC_POOL);
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}); });
ThreadPoolUtil.shutdownAndAwait(); ThreadPoolUtil.shutdownAndAwait();
} }
private void processFile(Path path, Path excludeBase) { /**
* 将单个文件导入或更新到 Elasticsearch 并缓存元信息至 Redis
*
* 方法逻辑流程如下
* 1. 获取文件相对路径及最后修改时间
* 2. 检查 Redis 中的缓存元信息是否存在且未过期
* 3. 检查 ES 中是否已有相同文档且未变动
* 4. 若有变化或首次导入则提取文件内容构建文档
* 5. 将文档写入 ES并缓存元信息
* 6. 支持通过 extInfo 参数手动提供 uploaderurlexpireTime 信息用于无缓存情况
*
* @param absoluteFilePath 文件绝对路径
* @param excludeFilePrefix 排除前缀用于计算相对路径
* @param extInfo 可选附加元信息当缓存未命中时使用
* @return 导入是否成功
*/
@Override
public boolean insertOrUpdateOneFileIntoES(Path absoluteFilePath, Path excludeFilePrefix, Map<String, Object> extInfo) {
try { try {
Path absPath = path.toAbsolutePath().normalize(); // === Step 1: 计算相对路径 & 获取文件信息 ===
Path relativePathObj; Path absPath = absoluteFilePath.toAbsolutePath().normalize();
if (absPath.startsWith(excludeBase)) { Path relativePathObj = absPath.startsWith(excludeFilePrefix)
relativePathObj = excludeBase.relativize(absPath); ? excludeFilePrefix.relativize(absPath)
} else { : absPath;
if (!absPath.startsWith(excludeFilePrefix)) {
logger.warn("路径未匹配 exclude.prefix使用全路径: {}", absPath); logger.warn("路径未匹配 exclude.prefix使用全路径: {}", absPath);
relativePathObj = absPath;
} }
Long localMTime = Files.getLastModifiedTime(path).toMillis(); Long localMTime = Files.getLastModifiedTime(absoluteFilePath).toMillis();
String localRelaFilePath = relativePathObj.toString().replace("\\", "/"); String localRelaFilePath = relativePathObj.toString().replace("\\", "/");
String fileNameWithSuffix = path.getFileName().toString(); String fileNameWithSuffix = absoluteFilePath.getFileName().toString();
// === Step 2: 检查 Redis 缓存是否已是最新 ===
Optional<String> metaJsonOpt = fileCacheService.getMeta(localRelaFilePath); Optional<String> metaJsonOpt = fileCacheService.getMeta(localRelaFilePath);
if(metaJsonOpt.isPresent()) { if (metaJsonOpt.isPresent()) {
// 有文件信息
JSON metaJson = JSONUtil.parse(metaJsonOpt.get()); JSON metaJson = JSONUtil.parse(metaJsonOpt.get());
Long uploadTime = metaJson.getByPath(DocMetaPropEnum.UPLOAD_TIME.code, Long.class); Long uploadTime = metaJson.getByPath(DocMetaPropEnum.UPLOAD_TIME.code, Long.class);
if(localMTime.equals(uploadTime)) { if (localMTime.equals(uploadTime)) {
logger.info("文件未变动,跳过导入: {}", localRelaFilePath); logger.info("文件未变动,跳过导入: {}", localRelaFilePath);
return; return true;
} }
fileCacheService.removeMetaCache(localRelaFilePath);
logger.info("[Redis] 已移除旧版本文件缓存Meta信息: {}", localRelaFilePath);
} }
String content = extractContent(path); // === Step 3: 提取文件内容 ===
String content = extractContent(absoluteFilePath);
if (StrUtil.isBlank(content)) { if (StrUtil.isBlank(content)) {
logger.warn("跳过空内容文件: {}", path); logger.warn("跳过空内容文件: {}", absoluteFilePath);
return; return true;
} }
// === Step 4: 检查 ES 是否已有未变动版本 ===
String docId = SafeIdUtil.encode(localRelaFilePath); String docId = SafeIdUtil.encode(localRelaFilePath);
GetRequest getRequest = new GetRequest(INDEX_NAME, docId); GetRequest getRequest = new GetRequest(INDEX_NAME, docId);
if (esClient.exists(getRequest, RequestOptions.DEFAULT)) { if (esClient.exists(getRequest, RequestOptions.DEFAULT)) {
@ -110,73 +136,93 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
Object esMtime = existingSource.get("mtime"); Object esMtime = existingSource.get("mtime");
if (esMtime != null && Long.parseLong(esMtime.toString()) == localMTime) { if (esMtime != null && Long.parseLong(esMtime.toString()) == localMTime) {
logger.info("文件未变动,跳过导入: {}", localRelaFilePath); logger.info("文件未变动,跳过导入: {}", localRelaFilePath);
String metaJsonStr = transToMetaJson(existingSource); fileCacheService.cacheMeta(localRelaFilePath, transToMetaJson(existingSource), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES);
fileCacheService.cacheMeta(localRelaFilePath, metaJsonStr, ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES); return true;
return;
} }
DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, docId); esClient.delete(new DeleteRequest(INDEX_NAME, docId), RequestOptions.DEFAULT);
esClient.delete(deleteRequest, RequestOptions.DEFAULT); logger.info("[ES] 已删除旧版本文件: {}", localRelaFilePath);
logger.info("已删除旧版本文件: {}", localRelaFilePath);
} }
logger.info("[开始进行文件导入......] localRelaFilePath: {} localMTime {}", localRelaFilePath, localMTime); logger.info("[开始进行文件导入......] localRelaFilePath: {} localMTime {}", localRelaFilePath, localMTime);
Map<String, Object> doc = new HashMap<>(); // === Step 5: 构建待写入文档 ===
doc.put("filename", fileNameWithSuffix); Map<String, Object> doc = buildDocument(fileNameWithSuffix, localRelaFilePath, content, localMTime, extInfo);
doc.put("filepath", localRelaFilePath);
doc.put("content", content);
doc.put("mtime", localMTime);
if(metaJsonOpt.isPresent()) {
Map<String, Object> metaMap = JSONUtil.toBean(metaJsonOpt.get(), Map.class);
doc.put("uploader", CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.UPLOADER.code, String.class, ConstantConfig.DEFAULT_UPLOADER));
String cacheFileUrl = CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.ACCESS_URL.code, String.class, StrUtil.EMPTY);
doc.put("url", buildAccessUrl(cacheFileUrl, localRelaFilePath, fileNameWithSuffix));
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);
String url = buildAccessUrl(StrUtil.EMPTY, localRelaFilePath, fileNameWithSuffix);
doc.put("url", url);
doc.put("expireTime", DateUtil.toMillis(ConstantConfig.LONG_TERM_EXPIRE_TIME));
}
IndexRequest request = new IndexRequest(INDEX_NAME)
.id(docId)
.source(doc);
// === Step 6: 写入 Elasticsearch 并更新缓存 ===
IndexRequest request = new IndexRequest(INDEX_NAME).id(docId).source(doc);
esClient.index(request, RequestOptions.DEFAULT); esClient.index(request, RequestOptions.DEFAULT);
logger.info("导入成功: {}", localRelaFilePath); logger.info("导入成功: {}", localRelaFilePath);
// 信息入缓存
fileCacheService.cacheMeta(localRelaFilePath, transToMetaJson(doc), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES); fileCacheService.cacheMeta(localRelaFilePath, transToMetaJson(doc), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES);
return true;
} catch (Exception e) { } catch (Exception e) {
logger.error("导入失败: {}", path, e); logger.error("导入失败: {}", absoluteFilePath, e);
return false;
} }
} }
private String buildAccessUrl(String originUrl, String localRelaFilePath, String fileNameWithSuffix) { private Map<String, Object> buildDocument(String filename, String filepath, String content, Long mtime, Map<String, Object> extInfo) {
// 优先使用原始链接 Map<String, Object> doc = new HashMap<>();
if(StrUtil.isNotBlank(originUrl)) { doc.put("filename", filename);
return originUrl; doc.put("filepath", filepath);
doc.put("content", content);
doc.put("mtime", mtime);
Optional<String> metaJsonOpt = fileCacheService.getMeta(filepath);
if (metaJsonOpt.isPresent()) {
Map<String, Object> 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, StrUtil.EMPTY));
doc.put("expireTime", CacheUtil.getFileMetaProp(metaMap, DocMetaPropEnum.EXPIRE_TIME.code, Long.class, DateUtil.toMillis(ConstantConfig.LONG_TERM_EXPIRE_TIME)));
} else {
extInfo = MapUtil.isEmpty(extInfo) ? MapUtil.empty() : extInfo;
boolean hasAllMeta = extInfo.keySet().containsAll(Lists.newArrayList(
DocMetaPropEnum.UPLOADER.code, DocMetaPropEnum.ACCESS_URL.code, DocMetaPropEnum.EXPIRE_TIME.code));
if (hasAllMeta) {
doc.put("uploader", extInfo.get(DocMetaPropEnum.UPLOADER.code));
doc.put("url", extInfo.get(DocMetaPropEnum.ACCESS_URL.code));
doc.put("expireTime", extInfo.get(DocMetaPropEnum.EXPIRE_TIME.code));
} else {
Map<String, Object> props = buildDocMetaProps(filepath);
doc.put("uploader", props.get(DocMetaPropEnum.UPLOADER.code));
doc.put("url", props.get(DocMetaPropEnum.ACCESS_URL.code));
doc.put("expireTime", props.get(DocMetaPropEnum.EXPIRE_TIME.code));
}
} }
return doc;
}
private Map<String, Object> buildDocMetaProps(String localRelaFilePath) {
Map<String, Object> metaMap = new HashMap<>(Map.of(
DocMetaPropEnum.UPLOADER.code, ConstantConfig.DEFAULT_UPLOADER,
DocMetaPropEnum.ACCESS_URL.code, StrUtil.EMPTY,
DocMetaPropEnum.EXPIRE_TIME.code, DateUtil.toMillis(ConstantConfig.LONG_TERM_EXPIRE_TIME)
));
if(StrUtil.isBlank(localRelaFilePath)) { if(StrUtil.isBlank(localRelaFilePath)) {
return StrUtil.EMPTY; return metaMap;
} }
String accessUrl = originUrl; String accessUrl = localRelaFilePath;
if(localRelaFilePath.endsWith(".md")) { if(localRelaFilePath.endsWith(".md")) {
// markdown直接拼接http链接 // markdown直接拼接http链接
accessUrl = String.format("%s/%s", ConstantConfig.SHARE_BASE_URL, localRelaFilePath); accessUrl = String.format("%s/%s", ConstantConfig.SHARE_BASE_URL, localRelaFilePath);
accessUrl = accessUrl.substring(0, accessUrl.length() - 3) + ".html"; accessUrl = accessUrl.substring(0, accessUrl.length() - 3) + ".html";
metaMap.put(DocMetaPropEnum.ACCESS_URL.code, accessUrl);
} else { } else {
// 其它文件从对象存储的DB中获取 // 其它文件从对象存储的DB中获取
Optional<OSRecordDO> latestRecordOpt = fileDomainService.getLatestRecordByRelaPath(localRelaFilePath); Optional<OSRecordDO> latestRecordOpt = fileDomainService.getLatestRecordByRelaPath(localRelaFilePath);
if(latestRecordOpt.isPresent()) { if(latestRecordOpt.isPresent()) {
OSRecordDO latestRecord = latestRecordOpt.get(); OSRecordDO latestRecord = latestRecordOpt.get();
accessUrl = latestRecord.getUrl(); accessUrl = latestRecord.getUrl();
metaMap.put(DocMetaPropEnum.ACCESS_URL.code, accessUrl);
metaMap.put(DocMetaPropEnum.UPLOADER.code, latestRecord.getUploader());
metaMap.put(DocMetaPropEnum.EXPIRE_TIME.code, latestRecord.getExpireTime());
} }
} }
return accessUrl; return metaMap;
} }
private String transToMetaJson(Map<String, Object> esExistingSource) { private String transToMetaJson(Map<String, Object> esExistingSource) {

View File

@ -13,6 +13,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set;
@Component @Component
public class ExcelImporter extends AbstractBaseFileImporter { public class ExcelImporter extends AbstractBaseFileImporter {
@ -26,8 +27,8 @@ public class ExcelImporter extends AbstractBaseFileImporter {
} }
@Override @Override
protected String getFileSuffix() { public Set<String> getFileSuffixes() {
return ".xlsx"; return Set.of(".xlsx", ".xls");
} }
@Override @Override

View File

@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
import java.util.Set;
/** /**
* @author Luke.ye * @author Luke.ye
@ -27,8 +28,8 @@ public class MarkdownImporter extends AbstractBaseFileImporter {
} }
@Override @Override
protected String getFileSuffix() { public Set<String> getFileSuffixes() {
return ".md"; return Set.of(".md");
} }
@Override @Override

View File

@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.util.Set;
/** /**
* @author Luke.ye * @author Luke.ye
@ -26,8 +27,8 @@ public class PdfImporter extends AbstractBaseFileImporter {
} }
@Override @Override
protected String getFileSuffix() { public Set<String> getFileSuffixes() {
return ".pdf"; return Set.of(".pdf");
} }
@Override @Override

View File

@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.util.Set;
/** /**
@ -27,8 +28,8 @@ public class WordImporter extends AbstractBaseFileImporter {
} }
@Override @Override
protected String getFileSuffix() { public Set<String> getFileSuffixes() {
return ".docx"; return Set.of(".doc", ".docx");
} }
@Override @Override

View File

@ -25,6 +25,12 @@ public interface FileCacheService {
*/ */
default Optional<String> getMeta(String fnWithRelativePath) { return null; }; default Optional<String> getMeta(String fnWithRelativePath) { return null; };
/**
* 移除指定key对应对的缓存
* @param fnWithRelativePath
*/
default void removeMetaCache(String fnWithRelativePath) {};
/** /**
* 清空所有缓存注意某些实现可能未实现 * 清空所有缓存注意某些实现可能未实现
*/ */

View File

@ -43,6 +43,11 @@ public class RedisFileCacheServiceImpl implements FileCacheService {
return Optional.ofNullable(value); return Optional.ofNullable(value);
} }
@Override
public void removeMetaCache(String fnWithRelativePath) {
redisTemplate.delete(key("meta", fnWithRelativePath));
}
@Override @Override
public void clearAll() { public void clearAll() {
log.warn("正在清空 Redis 文件缓存,前缀: {}", FILE_CACHE_PREFIX); log.warn("正在清空 Redis 文件缓存,前缀: {}", FILE_CACHE_PREFIX);

View File

@ -19,7 +19,7 @@ public class ConstantConfig {
public static final LocalDateTime LONG_TERM_EXPIRE_TIME = LocalDateTime.of(9999, 12, 31, 23, 59, 59); public static final LocalDateTime LONG_TERM_EXPIRE_TIME = LocalDateTime.of(9999, 12, 31, 23, 59, 59);
public static final long FILE_META_CACHE_EXPIRED_MINUTES = Duration.ofDays(2).toMinutes(); public static final long FILE_META_CACHE_EXPIRED_MINUTES = Duration.ofDays(7).toMinutes();
public static final long USER_CACHE_EXPIRED_MINUTES = Duration.ofDays(10).toMinutes(); public static final long USER_CACHE_EXPIRED_MINUTES = Duration.ofDays(10).toMinutes();

View File

@ -1,15 +1,19 @@
package com.knowledge.base.infrastructure.config; package com.knowledge.base.infrastructure.config;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* @author Luke.ye * @author Luke.ye
* @date 2025/5/8 09:59 * @date 2025/5/8 09:59
*/ */
@Component @Component
@RefreshScope @RefreshScope
@Getter
public class DynamicConfig { public class DynamicConfig {
//是否记录请求和响应信息 Y-记录 N-不记录 默认记录 //是否记录请求和响应信息 Y-记录 N-不记录 默认记录
@Value("${micro.saas.doc.parser.recordMsgBody:Y}") @Value("${micro.saas.doc.parser.recordMsgBody:Y}")
@ -25,19 +29,9 @@ public class DynamicConfig {
@Value("${token.expire.time:86400000}") @Value("${token.expire.time:86400000}")
private long tokenExpireTime; private long tokenExpireTime;
public String getRecordMsgBody() { @Value("${os.supported.searchable.file.suffix: pdf,doc,docx,xls,xlsx}")
return recordMsgBody; private String supportedSearchFileSuffix;
}
public String getImportScheduleCron() { @Value("${file.import.rate.limit: 10}")
return importScheduleCron; private String fileImportRateLimit;
}
public String getCookieDomainName() {
return cookieDomainName;
}
public long getTokenExpireTime() {
return tokenExpireTime;
}
} }

View File

@ -20,5 +20,5 @@ public class ObjectStorageProperties {
/** /**
* 可被检索文件的本地路径 * 可被检索文件的本地路径
*/ */
private String localSearchablePath; private String localSearchablePathPrefix;
} }

View File

@ -7,13 +7,14 @@ import cn.hutool.json.JSONUtil;
import com.knowledge.base.application.service.DocAppService; import com.knowledge.base.application.service.DocAppService;
import com.knowledge.base.application.service.UserAppService; import com.knowledge.base.application.service.UserAppService;
import com.knowledge.base.domain.common.enums.DocMetaPropEnum; import com.knowledge.base.domain.common.enums.DocMetaPropEnum;
import com.knowledge.base.domain.doc.service.FileImporterDispatcher;
import com.knowledge.base.domain.doc.service.iface.DocumentImporter; import com.knowledge.base.domain.doc.service.iface.DocumentImporter;
import com.knowledge.base.infrastructure.cache.iface.FileCacheService; import com.knowledge.base.infrastructure.cache.iface.FileCacheService;
import com.knowledge.base.infrastructure.config.ConstantConfig; import com.knowledge.base.infrastructure.config.ConstantConfig;
import com.knowledge.base.infrastructure.config.DynamicConfig;
import com.knowledge.base.infrastructure.config.ObjectStorageProperties; import com.knowledge.base.infrastructure.config.ObjectStorageProperties;
import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO; 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.UserDTO;
import com.knowledge.base.infrastructure.north.dto.user.UserTokenDTO;
import com.knowledge.base.infrastructure.south.ObjectStorageGateway; import com.knowledge.base.infrastructure.south.ObjectStorageGateway;
import com.knowledge.base.infrastructure.util.DateUtil; import com.knowledge.base.infrastructure.util.DateUtil;
import com.knowledge.base.infrastructure.util.ThreadPoolUtil; import com.knowledge.base.infrastructure.util.ThreadPoolUtil;
@ -35,6 +36,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/v1/doc") @RequestMapping("/api/v1/doc")
@ -47,12 +49,16 @@ public class FileWriteController {
private final FileCacheService fileCacheService; private final FileCacheService fileCacheService;
private static final String INDEX_NAME = "documents"; private static final String INDEX_NAME = "documents";
private final DynamicConfig dynamicConfig;
private final UserAppService userAppService; private final UserAppService userAppService;
private final DocAppService docAppService; private final DocAppService docAppService;
private final ObjectStorageGateway objectStorageGateway; private final ObjectStorageGateway objectStorageGateway;
private final ObjectStorageProperties objectStorageProperties; private final ObjectStorageProperties objectStorageProperties;
private final FileImporterDispatcher dispatcher;
@Autowired @Autowired
private List<DocumentImporter> importers; private List<DocumentImporter> importers;
@ -173,7 +179,8 @@ public class FileWriteController {
String uploader = userDTO.get().getUsername(); String uploader = userDTO.get().getUsername();
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
String dateFolder = now.toLocalDate().toString(); String dateFolder = now.toLocalDate().toString();
Set<String> supportedTypes = Set.of("pdf", "doc", "docx", "xls", "xlsx"); String[] supportedSuffix = dynamicConfig.getSupportedSearchFileSuffix().split(",");
Set<String> supportedTypes = Arrays.stream(supportedSuffix).map(String::trim).collect(Collectors.toSet());
boolean allowSaveToLocal = searchable && supportedTypes.contains(suffix); boolean allowSaveToLocal = searchable && supportedTypes.contains(suffix);
String localRelaFilePath = String.format("%s/%s", dateFolder, originFileNameWithSuffix); String localRelaFilePath = String.format("%s/%s", dateFolder, originFileNameWithSuffix);
@ -198,7 +205,8 @@ public class FileWriteController {
// 允许保存到本地 // 允许保存到本地
if (allowSaveToLocal) { if (allowSaveToLocal) {
String localDir = Paths.get(objectStorageProperties.getLocalSearchablePath(), dateFolder).toString(); String excludePrefix = objectStorageProperties.getLocalSearchablePathPrefix();
String localDir = Paths.get(excludePrefix, dateFolder).toString();
File localTargetDir = new File(localDir); File localTargetDir = new File(localDir);
if (!localTargetDir.exists()) { if (!localTargetDir.exists()) {
localTargetDir.mkdirs(); localTargetDir.mkdirs();
@ -207,18 +215,15 @@ public class FileWriteController {
Path targetPath = Paths.get(localDir, originFileNameWithSuffix); Path targetPath = Paths.get(localDir, originFileNameWithSuffix);
Files.write(targetPath, fileBytes); Files.write(targetPath, fileBytes);
long mtime = Files.getLastModifiedTime(targetPath).toMillis(); // 直接导入ES
Map<String, Object> extInfo = Map.of(
DocMetaPropEnum.UPLOADER.code, uploader,
DocMetaPropEnum.ACCESS_URL.code, url,
DocMetaPropEnum.EXPIRE_TIME.code, DateUtil.toMillis(expireTime)
);
boolean res = dispatcher.importSingleFile(targetPath.toAbsolutePath(), Paths.get(excludePrefix), extInfo);
Map<String, Object> cacheMeta = new LinkedHashMap<>(); logger.info("本地文件信息已保存并导入ES: localRelaPath = {}, ESImportRes = {}", localRelaFilePath, res);
cacheMeta.put(DocMetaPropEnum.UPLOADER.code, uploader);
cacheMeta.put(DocMetaPropEnum.ACCESS_URL.code, url);
cacheMeta.put(DocMetaPropEnum.UPLOAD_TIME.code, mtime);
cacheMeta.put(DocMetaPropEnum.EXPIRE_TIME.code, DateUtil.toMillis(expireTime));
String metaJson = JSONUtil.toJsonStr(cacheMeta);
fileCacheService.cacheMeta(localRelaFilePath, metaJson, ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES);
logger.info("本地文件信息已保存并写入缓存: key = {}, value = {}", localRelaFilePath, metaJson);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("异步写入失败: {}", originFileNameWithSuffix, e); logger.error("异步写入失败: {}", originFileNameWithSuffix, e);

View File

@ -0,0 +1,32 @@
package com.knowledge.base.infrastructure.util;
import com.google.common.util.concurrent.RateLimiter;
import com.knowledge.base.infrastructure.config.DynamicConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RateLimiterManager {
@Autowired
private DynamicConfig dynamicConfig;
private volatile RateLimiter rateLimiter;
private volatile double lastRate = -1;
public RateLimiter getRateLimiter() {
double currentRate = Double.valueOf(dynamicConfig.getFileImportRateLimit());
// 如果速率发生变化则更新限速器
if (rateLimiter == null || currentRate != lastRate) {
synchronized (this) {
if (rateLimiter == null || currentRate != lastRate) {
rateLimiter = RateLimiter.create(currentRate);
lastRate = currentRate;
}
}
}
return rateLimiter;
}
}

View File

@ -27,7 +27,7 @@ markdown.path=/Users/admin/Desktop/Archived/micro-saas
pdf.path=/Users/admin/Desktop/Archived/micro-saas pdf.path=/Users/admin/Desktop/Archived/micro-saas
word.path=/Users/admin/Desktop/Archived/micro-saas word.path=/Users/admin/Desktop/Archived/micro-saas
excel.path=/Users/admin/Desktop/Archived/micro-saas excel.path=/Users/admin/Desktop/Archived/micro-saas
object.storage.local-searchable-path=/Users/admin/Desktop/Archived/micro-saas object.storage.local-searchable-path-prefix=/Users/admin/Desktop/Archived/micro-saas
# mysql # mysql
spring.datasource.url=jdbc:mysql://localhost:3306/kbase?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.url=jdbc:mysql://localhost:3306/kbase?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

View File

@ -27,7 +27,7 @@ markdown.path=D:/02-documents/01-ahnx-share-src-public/public
pdf.path=D:/02-documents/01-os-uploaded-searchable-files pdf.path=D:/02-documents/01-os-uploaded-searchable-files
word.path=D:/02-documents/01-os-uploaded-searchable-files word.path=D:/02-documents/01-os-uploaded-searchable-files
excel.path=D:/02-documents/01-os-uploaded-searchable-files excel.path=D:/02-documents/01-os-uploaded-searchable-files
object.storage.local-searchable-path=D:/02-documents/01-os-uploaded-searchable-files object.storage.local-searchable-path-prefix=D:/02-documents/01-os-uploaded-searchable-files
# mysql # mysql

View File

@ -26,7 +26,7 @@ markdown.path=/app/import-data
pdf.path=/app/os-uploaded-searchable-files pdf.path=/app/os-uploaded-searchable-files
word.path=/app/os-uploaded-searchable-files word.path=/app/os-uploaded-searchable-files
excel.path=/app/os-uploaded-searchable-files excel.path=/app/os-uploaded-searchable-files
object.storage.local-searchable-path=/app/os-uploaded-searchable-files object.storage.local-searchable-path-prefix=/app/os-uploaded-searchable-files
# 相对路径裁剪 # 相对路径裁剪
exclude.file.path.prefix=/app/import-data exclude.file.path.prefix=/app/import-data