feat(rag): add document parsing structures

This commit is contained in:
2026-05-21 23:20:09 +08:00
parent 2ab02fb574
commit 1de773405f
38 changed files with 1240 additions and 9 deletions

View File

@@ -0,0 +1,17 @@
package com.bruce.common.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@@ -0,0 +1,21 @@
package com.bruce.common.document.parse;
import lombok.Data;
import java.nio.file.Path;
@Data
public class DocumentParseContext {
private Long documentId;
private Long attachmentId;
private String originalName;
private String suffix;
private String contentType;
private Path filePath;
}

View File

@@ -0,0 +1,12 @@
package com.bruce.common.document.parse;
public class DocumentParseException extends RuntimeException {
public DocumentParseException(String message) {
super(message);
}
public DocumentParseException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,20 @@
package com.bruce.common.document.parse;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
public class DocumentParseResult {
private String text;
private Integer textLength;
private Integer pageCount;
private Integer sheetCount;
private Map<String, Object> metadata = new LinkedHashMap<>();
}

View File

@@ -0,0 +1,8 @@
package com.bruce.common.document.parse;
public interface DocumentParser {
boolean supports(DocumentParseContext context);
DocumentParseResult parse(DocumentParseContext context);
}

View File

@@ -0,0 +1,37 @@
package com.bruce.common.document.parse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Locale;
@Component
public class DocumentParserFactory {
private final List<DocumentParser> parsers;
public DocumentParserFactory(List<DocumentParser> parsers) {
this.parsers = parsers;
}
public DocumentParser resolve(DocumentParseContext context) {
return parsers.stream()
.filter(parser -> parser.supports(context))
.findFirst()
.orElseThrow(() -> new DocumentParseException("不支持的文档类型: " + resolveType(context)));
}
private String resolveType(DocumentParseContext context) {
if (context == null) {
return "unknown";
}
if (StringUtils.hasText(context.getSuffix())) {
return context.getSuffix().trim().toLowerCase(Locale.ROOT);
}
if (StringUtils.hasText(context.getContentType())) {
return context.getContentType().trim();
}
return "unknown";
}
}

View File

@@ -0,0 +1,64 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParseException;
import com.bruce.common.document.parse.DocumentParseResult;
import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Locale;
import java.util.Set;
abstract class AbstractTikaDocumentParser {
private static final int MAX_TEXT_LENGTH = -1;
private final Tika tika = new Tika();
boolean supportsSuffix(DocumentParseContext context, Set<String> suffixes) {
return context != null
&& StringUtils.hasText(context.getSuffix())
&& suffixes.contains(context.getSuffix().trim().toLowerCase(Locale.ROOT));
}
boolean supportsContentType(DocumentParseContext context, String prefix) {
return context != null
&& StringUtils.hasText(context.getContentType())
&& context.getContentType().trim().toLowerCase(Locale.ROOT).startsWith(prefix);
}
DocumentParseResult parseWithTika(DocumentParseContext context) {
if (context == null || context.getFilePath() == null) {
throw new DocumentParseException("解析文件不能为空");
}
try {
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, context.getOriginalName());
if (StringUtils.hasText(context.getContentType())) {
metadata.set(Metadata.CONTENT_TYPE, context.getContentType());
}
String text;
try (InputStream inputStream = Files.newInputStream(context.getFilePath())) {
text = tika.parseToString(inputStream, metadata, MAX_TEXT_LENGTH);
}
DocumentParseResult result = new DocumentParseResult();
result.setText(text == null ? "" : text.trim());
result.setTextLength(result.getText().length());
result.getMetadata().put("contentType", firstNonBlank(metadata.get(Metadata.CONTENT_TYPE), context.getContentType()));
result.getMetadata().put("resourceName", firstNonBlank(metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY), context.getOriginalName()));
return result;
} catch (IOException | TikaException e) {
throw new DocumentParseException("文档解析失败: " + e.getMessage(), e);
}
}
private String firstNonBlank(String first, String fallback) {
return StringUtils.hasText(first) ? first : fallback;
}
}

View File

@@ -0,0 +1,26 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class ExcelDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("xls", "xlsx");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES)
|| supportsContentType(context, "application/vnd.ms-excel")
|| supportsContentType(context, "application/vnd.openxmlformats-officedocument.spreadsheetml");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,24 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class PdfDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("pdf");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES) || supportsContentType(context, "application/pdf");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,24 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class TxtDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("txt", "md", "log");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES) || supportsContentType(context, "text/");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,26 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class WordDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("doc", "docx");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES)
|| supportsContentType(context, "application/msword")
|| supportsContentType(context, "application/vnd.openxmlformats-officedocument.wordprocessingml");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -2,9 +2,12 @@ package com.bruce.rag.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.rag.dto.request.RagDocumentBatchUploadRequest;
import com.bruce.rag.dto.request.RagDocumentParseRequest;
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
import com.bruce.rag.dto.response.RagDocumentParseResponse;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.service.IRagDocumentParseService;
import com.bruce.rag.service.IRagDocumentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -29,6 +32,9 @@ public class RagDocumentController {
@Autowired
private IRagDocumentService ragDocumentService;
@Autowired
private IRagDocumentParseService ragDocumentParseService;
@Operation(summary = "查询全部知识库文档")
@PostMapping("/list")
public RequestResult<List<RagDocumentResponse>> list() {
@@ -85,4 +91,13 @@ public class RagDocumentController {
request.getStoreId(), responses.size());
return RequestResult.success(responses);
}
@Operation(summary = "解析知识库文档")
@PostMapping("/parse")
public RequestResult<List<RagDocumentParseResponse>> parse(@RequestBody RagDocumentParseRequest request) {
log.info("RagDocumentController.parse start, request={}", request);
List<RagDocumentParseResponse> responses = ragDocumentParseService.parse(request);
log.info("RagDocumentController.parse success, count={}", responses.size());
return RequestResult.success(responses);
}
}

View File

@@ -0,0 +1,26 @@
package com.bruce.rag.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "RAG知识库文档解析请求")
public class RagDocumentParseRequest {
@Schema(description = "文档ID列表")
private List<Long> documentIds;
@Schema(description = "切片方式")
private String chunkStrategy;
@Schema(description = "切片长度")
private Integer chunkSize;
@Schema(description = "重叠长度")
private Integer chunkOverlap;
@Schema(description = "分隔符")
private String delimiter;
}

View File

@@ -0,0 +1,33 @@
package com.bruce.rag.dto.response;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
@Schema(description = "RAG知识库文档解析响应")
public class RagDocumentParseResponse {
@Schema(description = "文档ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long documentId;
@Schema(description = "解析状态")
private String parseStatus;
@Schema(description = "文本长度")
private Integer textLength;
@Schema(description = "页数")
private Integer pageCount;
@Schema(description = "工作表数量")
private Integer sheetCount;
@Schema(description = "解析元数据")
private Map<String, Object> metadata = new LinkedHashMap<>();
}

View File

@@ -0,0 +1,67 @@
package com.bruce.rag.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("rag_chunk")
@Schema(description = "RAG知识切片")
public class RagChunk extends BaseEntity {
@Schema(description = "知识库ID")
@TableField("store_id")
private Long storeId;
@Schema(description = "文档ID")
@TableField("document_id")
private Long documentId;
@Schema(description = "文档内切片序号")
@TableField("chunk_index")
private Integer chunkIndex;
@Schema(description = "切片内容")
@TableField("chunk_content")
private String chunkContent;
@Schema(description = "切片摘要")
@TableField("chunk_summary")
private String chunkSummary;
@Schema(description = "Token数量")
@TableField("token_count")
private Integer tokenCount;
@Schema(description = "页码")
@TableField("page_number")
private Integer pageNumber;
@Schema(description = "章节标题")
@TableField("section_title")
private String sectionTitle;
@Schema(description = "标题路径")
@TableField("heading_path")
private String headingPath;
@Schema(description = "向量ID")
@TableField("vector_id")
private String vectorId;
@Schema(description = "切片级扩展元数据JSON")
@TableField("metadata_json")
private String metadataJson;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,50 @@
package com.bruce.rag.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("rag_chunk_embedding")
@Schema(description = "RAG切片向量")
public class RagChunkEmbedding extends BaseEntity {
@Schema(description = "知识库ID")
@TableField("store_id")
private Long storeId;
@Schema(description = "文档ID")
@TableField("document_id")
private Long documentId;
@Schema(description = "切片ID")
@TableField("chunk_id")
private Long chunkId;
@Schema(description = "向量模型")
@TableField("embedding_model")
private String embeddingModel;
@Schema(description = "向量维度")
@TableField("embedding_dimension")
private Integer embeddingDimension;
@Schema(description = "向量内容")
private String embedding;
@Schema(description = "向量生成内容哈希")
@TableField("content_hash")
private String contentHash;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,20 @@
package com.bruce.rag.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RagChunkStrategyEnum {
FIXED_LENGTH(1, "固定长度切片"),
PARAGRAPH(2, "按段落切片"),
HEADING(3, "按标题层级切片"),
TABLE_ROW(4, "按表格行切片"),
DELIMITER(5, "按分隔符切片"),
SEMANTIC(6, "语义切片");
private final Integer value;
private final String label;
}

View File

@@ -0,0 +1,9 @@
package com.bruce.rag.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.rag.entity.RagChunkEmbedding;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RagChunkEmbeddingMapper extends BaseMapper<RagChunkEmbedding> {
}

View File

@@ -0,0 +1,9 @@
package com.bruce.rag.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.rag.entity.RagChunk;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RagChunkMapper extends BaseMapper<RagChunk> {
}

View File

@@ -0,0 +1,7 @@
package com.bruce.rag.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.rag.entity.RagChunkEmbedding;
public interface IRagChunkEmbeddingService extends IService<RagChunkEmbedding> {
}

View File

@@ -0,0 +1,7 @@
package com.bruce.rag.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.rag.entity.RagChunk;
public interface IRagChunkService extends IService<RagChunk> {
}

View File

@@ -0,0 +1,13 @@
package com.bruce.rag.service;
import com.bruce.rag.dto.response.RagDocumentParseResponse;
import com.bruce.rag.dto.request.RagDocumentParseRequest;
import java.util.List;
public interface IRagDocumentParseService {
RagDocumentParseResponse parse(Long documentId);
List<RagDocumentParseResponse> parse(RagDocumentParseRequest request);
}

View File

@@ -0,0 +1,12 @@
package com.bruce.rag.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.rag.entity.RagChunkEmbedding;
import com.bruce.rag.mapper.RagChunkEmbeddingMapper;
import com.bruce.rag.service.IRagChunkEmbeddingService;
import org.springframework.stereotype.Service;
@Service
public class RagChunkEmbeddingServiceImpl extends ServiceImpl<RagChunkEmbeddingMapper, RagChunkEmbedding>
implements IRagChunkEmbeddingService {
}

View File

@@ -0,0 +1,11 @@
package com.bruce.rag.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.rag.entity.RagChunk;
import com.bruce.rag.mapper.RagChunkMapper;
import com.bruce.rag.service.IRagChunkService;
import org.springframework.stereotype.Service;
@Service
public class RagChunkServiceImpl extends ServiceImpl<RagChunkMapper, RagChunk> implements IRagChunkService {
}

View File

@@ -0,0 +1,151 @@
package com.bruce.rag.service.impl;
import com.bruce.common.config.AttachmentProperties;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParseException;
import com.bruce.common.document.parse.DocumentParseResult;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParserFactory;
import com.bruce.common.domain.entity.SysAttachment;
import com.bruce.common.service.ISysAttachmentService;
import com.bruce.rag.dto.request.RagDocumentParseRequest;
import com.bruce.rag.dto.response.RagDocumentParseResponse;
import com.bruce.rag.entity.RagDocument;
import com.bruce.rag.enums.RagChunkStrategyEnum;
import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.service.IRagDocumentParseService;
import com.bruce.rag.service.IRagDocumentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class RagDocumentParseServiceImpl implements IRagDocumentParseService {
private final IRagDocumentService ragDocumentService;
private final ISysAttachmentService sysAttachmentService;
private final AttachmentProperties attachmentProperties;
private final DocumentParserFactory documentParserFactory;
@Override
public List<RagDocumentParseResponse> parse(RagDocumentParseRequest request) {
log.info("RagDocumentParseServiceImpl.parse batch start, request={}", request);
validateParseRequest(request);
List<RagDocumentParseResponse> responses = request.getDocumentIds().stream()
.map(this::parse)
.toList();
log.info("RagDocumentParseServiceImpl.parse batch success, count={}", responses.size());
return responses;
}
@Override
public RagDocumentParseResponse parse(Long documentId) {
log.info("RagDocumentParseServiceImpl.parse start, documentId={}", documentId);
if (documentId == null) {
throw new IllegalArgumentException("文档ID不能为空");
}
RagDocument document = ragDocumentService.getById(documentId);
if (document == null) {
throw new IllegalArgumentException("文档不存在ID: " + documentId);
}
if (document.getAttachmentId() == null) {
throw new IllegalArgumentException("文档附件ID不能为空");
}
SysAttachment attachment = sysAttachmentService.getById(document.getAttachmentId());
if (attachment == null) {
throw new IllegalArgumentException("附件不存在ID: " + document.getAttachmentId());
}
updateParseStatus(documentId, RagParseStatusEnum.PARSING, null);
try {
DocumentParseContext context = buildParseContext(document, attachment);
DocumentParser parser = documentParserFactory.resolve(context);
DocumentParseResult result = parser.parse(context);
updateParseStatus(documentId, RagParseStatusEnum.PARSED, null);
RagDocumentParseResponse response = toResponse(documentId, result);
log.info("RagDocumentParseServiceImpl.parse success, documentId={}, textLength={}",
documentId, response.getTextLength());
return response;
} catch (RuntimeException e) {
updateParseStatus(documentId, RagParseStatusEnum.FAILED, e.getMessage());
log.warn("RagDocumentParseServiceImpl.parse failed, documentId={}, message={}", documentId, e.getMessage());
throw e;
}
}
private void validateParseRequest(RagDocumentParseRequest request) {
if (request == null) {
throw new IllegalArgumentException("解析请求不能为空");
}
if (request.getDocumentIds() == null || request.getDocumentIds().isEmpty()) {
throw new IllegalArgumentException("文档ID列表不能为空");
}
Set<String> strategies = Arrays.stream(RagChunkStrategyEnum.values())
.map(Enum::name)
.collect(Collectors.toSet());
if (request.getChunkStrategy() == null || !strategies.contains(request.getChunkStrategy())) {
throw new IllegalArgumentException("不支持的切片方式: " + request.getChunkStrategy());
}
}
private DocumentParseContext buildParseContext(RagDocument document, SysAttachment attachment) {
Path filePath = resolveFilePath(attachment);
if (!Files.isRegularFile(filePath)) {
throw new DocumentParseException("解析文件不存在: " + filePath);
}
DocumentParseContext context = new DocumentParseContext();
context.setDocumentId(document.getId());
context.setAttachmentId(attachment.getId());
context.setOriginalName(attachment.getOriginalName());
context.setSuffix(attachment.getFileSuffix());
context.setContentType(attachment.getContentType());
context.setFilePath(filePath);
return context;
}
private Path resolveFilePath(SysAttachment attachment) {
if (!StringUtils.hasText(attachment.getFilePath())) {
throw new DocumentParseException("附件文件路径不能为空");
}
Path filePath = Path.of(attachment.getFilePath());
if (filePath.isAbsolute()) {
return filePath.normalize();
}
return Path.of(attachmentProperties.getBasePath()).resolve(filePath).normalize();
}
private void updateParseStatus(Long documentId, RagParseStatusEnum status, String errorMessage) {
RagDocument update = new RagDocument();
update.setId(documentId);
update.setParseStatus(status.name());
update.setErrorMessage(StringUtils.hasText(errorMessage) ? errorMessage : null);
ragDocumentService.updateById(update);
}
private RagDocumentParseResponse toResponse(Long documentId, DocumentParseResult result) {
RagDocumentParseResponse response = new RagDocumentParseResponse();
response.setDocumentId(documentId);
response.setParseStatus(RagParseStatusEnum.PARSED.name());
response.setTextLength(result.getTextLength());
response.setPageCount(result.getPageCount());
response.setSheetCount(result.getSheetCount());
response.setMetadata(result.getMetadata());
return response;
}
}

View File

@@ -82,10 +82,18 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
document.setIndexStatus(RagIndexStatusEnum.PENDING.name());
}
document.setStoreId(request.getStoreId());
document.setAttachmentId(request.getAttachmentId());
document.setDocumentTitle(request.getDocumentTitle().trim());
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
if (request.getStoreId() != null) {
document.setStoreId(request.getStoreId());
}
if (request.getAttachmentId() != null) {
document.setAttachmentId(request.getAttachmentId());
}
if (StringUtils.hasText(request.getDocumentTitle())) {
document.setDocumentTitle(request.getDocumentTitle().trim());
}
if (request.getDocumentSummary() != null) {
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
}
if (StringUtils.hasText(request.getParseStatus())) {
document.setParseStatus(request.getParseStatus().trim());
}
@@ -95,10 +103,14 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
if (request.getEnabled() != null) {
document.setEnabled(request.getEnabled());
}
document.setErrorMessage(trimToNull(request.getErrorMessage()));
document.setRemark(trimToNull(request.getRemark()));
if (request.getErrorMessage() != null) {
document.setErrorMessage(trimToNull(request.getErrorMessage()));
}
if (request.getRemark() != null) {
document.setRemark(trimToNull(request.getRemark()));
}
boolean result = saveOrUpdate(document);
boolean result = request.getId() == null ? save(document) : updateById(document);
log.info("RagDocumentServiceImpl.saveOrUpdate success, requestId={}, savedId={}, result={}",
request.getId(), document.getId(), result);
return result;