From 93bd565cb6ccaaa3cf4ba80920e8ebd2a90bba45 Mon Sep 17 00:00:00 2001 From: wenjinbo <599483010@qq.com> Date: Tue, 9 Sep 2025 14:04:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E6=96=87=E4=BB=B6=E5=88=97=E8=A1=A8=E8=A1=A8=E4=B8=BA?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=A0=91=E7=BB=93=E6=9E=84=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=9F=A5=E8=AF=86=E5=BA=93=E6=96=87=E4=BB=B6=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/chatweb/components/ChatBox.vue | 18 +- .../views/chatweb/components/MessageItem.vue | 10 +- .../chatweb/composables/useMessageHandlers.ts | 9 +- .../controller/DatasetDocController.java | 19 + .../controller/DatasetFilesController.java | 209 +++++++++++ .../brichat/entity/dataset/TDatasetFiles.java | 241 +++++++++++++ .../mapper/opengauss/TDatasetFilesMapper.java | 148 ++++++++ .../brichat/service/DatasetFilesService.java | 129 +++++++ .../service/impl/DatasetFilesServiceImpl.java | 331 ++++++++++++++++++ .../mapper/opengauss/TDatasetFilesMapper.xml | 188 ++++++++++ 10 files changed, 1296 insertions(+), 6 deletions(-) create mode 100644 chat-server/src/main/java/com/bjtds/brichat/controller/DatasetFilesController.java create mode 100644 chat-server/src/main/java/com/bjtds/brichat/entity/dataset/TDatasetFiles.java create mode 100644 chat-server/src/main/java/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.java create mode 100644 chat-server/src/main/java/com/bjtds/brichat/service/DatasetFilesService.java create mode 100644 chat-server/src/main/java/com/bjtds/brichat/service/impl/DatasetFilesServiceImpl.java create mode 100644 chat-server/src/main/resources/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.xml diff --git a/chat-client/src/views/chatweb/components/ChatBox.vue b/chat-client/src/views/chatweb/components/ChatBox.vue index e94f464..9e9d930 100644 --- a/chat-client/src/views/chatweb/components/ChatBox.vue +++ b/chat-client/src/views/chatweb/components/ChatBox.vue @@ -199,7 +199,7 @@ const scrollToBottom = async () => { await nextTick() setTimeout(() => { requestAnimationFrame(() => { - if (messagesContainer.value && !isUserScrolling) { + if (messagesContainer.value && (!isUserScrolling || isLoading.value)) { bottomAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'start' @@ -220,6 +220,14 @@ const throttledScrollToBottom = throttle(() => { scrollToBottom() }, 300, { leading: true, trailing: true }) +// 流式回答时的滚动函数,更积极的滚动策略 +const streamingScrollToBottom = throttle(() => { + if (messagesContainer.value && isLoading.value) { + // 在流式回答时,强制滚动到底部 + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight + } +}, 100, { leading: true, trailing: true }) + // 创建新会话 const handleCreateNewChat = async () => { // 保存当前会话消息 @@ -293,6 +301,10 @@ const handleSendMessage = async (messageText: string) => { // 更新会话ID updateSessionIds(data.conversationId, data.messageId) emit('streamComplete', data) + }, + () => { + // 流式更新时触发滚动 + streamingScrollToBottom() } ) @@ -345,6 +357,10 @@ const handleRegenerateMessage = async (index: number) => { (data) => { updateSessionIds(data.conversationId, data.messageId) emit('streamComplete', data) + }, + () => { + // 流式更新时触发滚动 + streamingScrollToBottom() } ) diff --git a/chat-client/src/views/chatweb/components/MessageItem.vue b/chat-client/src/views/chatweb/components/MessageItem.vue index 071ddc7..e1a9176 100644 --- a/chat-client/src/views/chatweb/components/MessageItem.vue +++ b/chat-client/src/views/chatweb/components/MessageItem.vue @@ -944,7 +944,7 @@ const hasAnySources = (sources: TraceData | null | undefined) => { overflow: auto; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); background: white; - font-size: 0.9rem; + font-size: 0.83rem; /* 比正文(0.95rem)小2px,约为0.83rem */ line-height: 1.4; border: 1px solid #e5e7eb; } @@ -979,6 +979,7 @@ const hasAnySources = (sources: TraceData | null | undefined) => { max-width: 200px; word-break: break-word; white-space: normal; + font-size: 0.83rem; /* 确保表格单元格字体也比正文小2px */ } .answer-content :deep(th) { @@ -986,7 +987,7 @@ const hasAnySources = (sources: TraceData | null | undefined) => { background: #f8fafc; font-weight: 600; color: #374151; - font-size: 0.85rem; + font-size: 0.83rem; /* 与表格整体字体大小保持一致,比正文小2px */ text-transform: uppercase; letter-spacing: 0.025em; position: sticky; @@ -1042,7 +1043,7 @@ const hasAnySources = (sources: TraceData | null | undefined) => { @media (max-width: 768px) { .answer-content :deep(table) { min-width: 600px; - font-size: 0.8rem; + font-size: 0.75rem; /* 移动端保持比正文小的比例 */ margin: 1rem 0; border: 1px solid #e5e7eb; } @@ -1052,10 +1053,11 @@ const hasAnySources = (sources: TraceData | null | undefined) => { padding: 0.5rem 0.75rem; max-width: 150px; border: 1px solid #e5e7eb; + font-size: 0.75rem; /* 移动端表格单元格字体 */ } .answer-content :deep(th) { - font-size: 0.75rem; + font-size: 0.75rem; /* 移动端表头字体 */ border-bottom: 2px solid #e5e7eb; } } diff --git a/chat-client/src/views/chatweb/composables/useMessageHandlers.ts b/chat-client/src/views/chatweb/composables/useMessageHandlers.ts index 5f844bd..0dcdaa7 100644 --- a/chat-client/src/views/chatweb/composables/useMessageHandlers.ts +++ b/chat-client/src/views/chatweb/composables/useMessageHandlers.ts @@ -142,7 +142,8 @@ export function useMessageHandlers() { chatType: string, conversationId?: string, onMessageUpdate?: (message: Message) => void, - onStreamComplete?: (data: { conversationId: string; messageId: string; content: string }) => void + onStreamComplete?: (data: { conversationId: string; messageId: string; content: string }) => void, + onStreamUpdate?: () => void ) => { try { isLoading.value = true @@ -217,6 +218,9 @@ export function useMessageHandlers() { if (data.taskId) { currentTaskId.value = data.taskId } + + // 触发流式更新回调,用于滚动 + onStreamUpdate?.() } } catch (e) { console.error('解析错误:', e) @@ -243,6 +247,9 @@ export function useMessageHandlers() { if (data.messageId) { newMessageId = data.messageId } + + // 触发流式更新回调,用于滚动 + onStreamUpdate?.() } } catch (e) { console.error('最终解析错误:', e) diff --git a/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetDocController.java b/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetDocController.java index 03f473b..b75b3b2 100644 --- a/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetDocController.java +++ b/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetDocController.java @@ -3,7 +3,9 @@ import cn.hutool.core.io.resource.InputStreamResource; import com.bjtds.brichat.entity.dataset.DatasetsDocRenameReq; import com.bjtds.brichat.entity.dataset.DocUploadReq; import com.bjtds.brichat.entity.dataset.DocumentUploadReq; +import com.bjtds.brichat.entity.dataset.TDatasetFiles; import com.bjtds.brichat.entity.dto.PdfTaskDto; +import com.bjtds.brichat.service.DatasetFilesService; import com.bjtds.brichat.service.DatasetsDocService; import com.bjtds.brichat.service.dify.DifyDatasetApiService; import com.bjtds.brichat.util.Constants; @@ -64,6 +66,10 @@ public class DatasetDocController { private String difyDatasetApiKey; + @Resource + private DatasetFilesService datasetFilesService; + + @Value("${dify.url}") private String difyUrl; @Autowired @@ -94,6 +100,19 @@ public class DatasetDocController { public ResultUtils upload(@RequestPart("file") MultipartFile file, @RequestPart("request") DocUploadReq req) throws Exception{ ResponseEntity documentByFile = difyDatasetApiService.createDocByFile(req, file,""); + Map document = (Map) documentByFile.getBody().get("document"); + String documentId = document.get("id"); + + TDatasetFiles datasetFiles = new TDatasetFiles(); + datasetFiles.setParentId(null); + datasetFiles.setType("file"); + datasetFiles.setName(file.getOriginalFilename()); + datasetFiles.setSize(file.getSize()); + datasetFiles.setDifyDatasetId(req.getDatasetId()); + datasetFiles.setOwnerId(1L); + datasetFiles.setDifyDocId(documentId); + datasetFilesService.createFile(datasetFiles); + return ResultUtils.success(documentByFile); } diff --git a/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetFilesController.java b/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetFilesController.java new file mode 100644 index 0000000..2c7704a --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/controller/DatasetFilesController.java @@ -0,0 +1,209 @@ +package com.bjtds.brichat.controller; + +import com.bjtds.brichat.entity.dataset.TDatasetFiles; +import com.bjtds.brichat.service.DatasetFilesService; +import com.bjtds.brichat.util.ResultUtils; +import com.bjtds.common.utils.Pagination; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 知识库文件/文件夹控制器 + * + * @author system + */ +@RestController +@RequestMapping("/api/dataset-files") +public class DatasetFilesController { + + @Autowired + private DatasetFilesService datasetFilesService; + + /** + * 根据知识库id和parent_id 分页查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param parentId 父目录ID + * @param orderBy 排序字段 + * @param orderDirection 排序方向 + * @param pageNo 页码 + * @param pageSize 页大小 + * @return 分页结果 + */ + @GetMapping("/list") + public Object getFilesByParent(@RequestParam String difyDatasetId, + @RequestParam(required = false) Long parentId, + @RequestParam(required = false, defaultValue = "name") String orderBy, + @RequestParam(required = false, defaultValue = "ASC") String orderDirection, + @RequestParam(required = false, defaultValue = "1") Integer pageNo, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + try { + Pagination result = datasetFilesService.getFilesByDatasetIdAndParentId( + difyDatasetId, parentId, orderBy, orderDirection, pageNo, pageSize); + return ResultUtils.success(result); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 根据知识库id和name查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param name 文件/文件夹名称 + * @param orderBy 排序字段 + * @param orderDirection 排序方向 + * @param pageNo 页码 + * @param pageSize 页大小 + * @return 分页结果 + */ + @GetMapping("/search") + public Object searchFilesByName(@RequestParam String difyDatasetId, + @RequestParam String name, + @RequestParam(required = false, defaultValue = "name") String orderBy, + @RequestParam(required = false, defaultValue = "ASC") String orderDirection, + @RequestParam(required = false, defaultValue = "1") Integer pageNo, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + try { + Pagination result = datasetFilesService.getFilesByDatasetIdAndName( + difyDatasetId, name, orderBy, orderDirection, pageNo, pageSize); + return ResultUtils.success(result); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 根据ID查询文件/文件夹详情 + * + * @param id 文件/文件夹ID + * @return 文件/文件夹信息 + */ + @GetMapping("/{id}") + public Object getFileById(@PathVariable Integer id) { + try { + TDatasetFiles file = datasetFilesService.getFileById(id); + if (file == null) { + return ResultUtils.error("文件/文件夹不存在"); + } + return ResultUtils.success(file); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 创建文件/文件夹 + * + * @param datasetFiles 文件/文件夹信息 + * @return 创建结果 + */ + @PostMapping("/create") + public Object createFile(@RequestBody TDatasetFiles datasetFiles) { + try { + TDatasetFiles file = datasetFilesService.createFile(datasetFiles); + return ResultUtils.success(file); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 删除文件/文件夹 + * + * @param id 文件/文件夹ID + * @return 删除结果 + */ + @DeleteMapping("/{id}") + public Object deleteFile(@PathVariable Integer id) { + try { + boolean success = datasetFilesService.deleteFile(id); + if (success) { + return ResultUtils.success("删除成功"); + } else { + return ResultUtils.error("删除失败"); + } + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 重命名文件/文件夹 + * + * @param id 文件/文件夹ID + * @param request 重命名请求 + * @return 重命名结果 + */ + @PutMapping("/{id}/rename") + public Object renameFile(@PathVariable Integer id, @RequestBody RenameFileRequest request) { + try { + boolean success = datasetFilesService.renameFile(id, request.getNewName()); + if (success) { + return ResultUtils.success("重命名成功"); + } else { + return ResultUtils.error("重命名失败"); + } + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 检查文件/文件夹名称是否存在 + * + * @param difyDatasetId 知识库ID + * @param parentId 父目录ID + * @param name 文件/文件夹名称 + * @param excludeId 排除的ID + * @return 检查结果 + */ + @GetMapping("/check-name") + public Object checkNameExists(@RequestParam String difyDatasetId, + @RequestParam(required = false) Long parentId, + @RequestParam String name, + @RequestParam(required = false) Integer excludeId) { + try { + boolean exists = datasetFilesService.checkNameExists(parentId, name, difyDatasetId, excludeId); + return ResultUtils.success(exists); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + /** + * 根据路径查询文件/文件夹 + * + * @param path 路径 + * @param difyDatasetId 知识库ID + * @return 文件/文件夹信息 + */ + @GetMapping("/by-path") + public Object getFileByPath(@RequestParam String path, @RequestParam String difyDatasetId) { + try { + TDatasetFiles file = datasetFilesService.getFileByPath(path, difyDatasetId); + if (file == null) { + return ResultUtils.error("文件/文件夹不存在"); + } + return ResultUtils.success(file); + } catch (Exception e) { + return ResultUtils.error(e.getMessage()); + } + } + + + /** + * 重命名文件/文件夹请求类 + */ + public static class RenameFileRequest { + private String newName; + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + } +} \ No newline at end of file diff --git a/chat-server/src/main/java/com/bjtds/brichat/entity/dataset/TDatasetFiles.java b/chat-server/src/main/java/com/bjtds/brichat/entity/dataset/TDatasetFiles.java new file mode 100644 index 0000000..2a7ec6a --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/entity/dataset/TDatasetFiles.java @@ -0,0 +1,241 @@ +package com.bjtds.brichat.entity.dataset; + +import java.time.LocalDateTime; + +/** + * 知识库文件/文件夹实体类 + * 对应数据库表:t_dataset_files + */ +public class TDatasetFiles { + + /** + * 文件/文件夹唯一标识符 + */ + private Integer id; + + /** + * 文件或文件夹名称 + */ + private String name; + + /** + * 类型: file-文件, folder-文件夹 + */ + private String type; + + /** + * 父目录ID,如果是根目录则为NULL + */ + private Long parentId; + + /** + * 完整路径,从根目录开始的完整路径 + */ + private String path; + + /** + * 文件大小(字节),文件夹通常为0或子项总和 + */ + private Long size; + + /** + * 所有者用户ID + */ + private Long ownerId; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 最后更新时间 + */ + private LocalDateTime updatedAt; + + /** + * 逻辑删除标志:false-正常,true-已删除 + */ + private Boolean isDeleted; + + /** + * 文件MIME类型,如图像/jpeg,文本/plain等 + */ + private String mimeType; + + /** + * 文件扩展名,如.txt、.jpg等 + */ + private String extension; + + /** + * 关联的Dify文档ID,用于与Dify平台集成 + */ + private String difyDocId; + + /** + * dify的解析状态 + */ + private String indexingStatus; + + /** + * dify知识库id + */ + private String difyDatasetId; + + // 构造函数 + public TDatasetFiles() {} + + public TDatasetFiles(String name, String type, Long parentId, String path, Long ownerId, String difyDatasetId) { + this.name = name; + this.type = type; + this.parentId = parentId; + this.path = path; + this.ownerId = ownerId; + this.difyDatasetId = difyDatasetId; + this.size = 0L; + this.isDeleted = false; + } + + // Getter and Setter methods + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public Long getOwnerId() { + return ownerId; + } + + public void setOwnerId(Long ownerId) { + this.ownerId = ownerId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Boolean getIsDeleted() { + return isDeleted; + } + + public void setIsDeleted(Boolean isDeleted) { + this.isDeleted = isDeleted; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getExtension() { + return extension; + } + + public void setExtension(String extension) { + this.extension = extension; + } + + public String getDifyDocId() { + return difyDocId; + } + + public void setDifyDocId(String difyDocId) { + this.difyDocId = difyDocId; + } + + public String getIndexingStatus() { + return indexingStatus; + } + + public void setIndexingStatus(String indexingStatus) { + this.indexingStatus = indexingStatus; + } + + public String getDifyDatasetId() { + return difyDatasetId; + } + + public void setDifyDatasetId(String difyDatasetId) { + this.difyDatasetId = difyDatasetId; + } + + @Override + public String toString() { + return "TDatasetFiles{" + + "id=" + id + + ", name='" + name + '\'' + + ", type='" + type + '\'' + + ", parentId=" + parentId + + ", path='" + path + '\'' + + ", size=" + size + + ", ownerId=" + ownerId + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + ", isDeleted=" + isDeleted + + ", mimeType='" + mimeType + '\'' + + ", extension='" + extension + '\'' + + ", difyDocId='" + difyDocId + '\'' + + ", indexingStatus='" + indexingStatus + '\'' + + ", difyDatasetId='" + difyDatasetId + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/chat-server/src/main/java/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.java b/chat-server/src/main/java/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.java new file mode 100644 index 0000000..962e089 --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.java @@ -0,0 +1,148 @@ +package com.bjtds.brichat.mapper.opengauss; + +import com.bjtds.brichat.entity.dataset.TDatasetFiles; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 知识库文件/文件夹 Mapper 接口 + * + * @author system + */ +@Mapper +public interface TDatasetFilesMapper { + + /** + * 根据知识库id和parent_id 分页查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param parentId 父目录ID + * @param orderBy 排序字段 (name, size, created_at, updated_at) + * @param orderDirection 排序方向 (ASC, DESC) + * @param offset 偏移量 + * @param limit 限制数量 + * @return 文件/文件夹列表 + */ + List selectByDatasetIdAndParentId(@Param("difyDatasetId") String difyDatasetId, + @Param("parentId") Long parentId, + @Param("orderBy") String orderBy, + @Param("orderDirection") String orderDirection, + @Param("offset") Integer offset, + @Param("limit") Integer limit); + + /** + * 根据知识库id和parent_id 查询总数 + * + * @param difyDatasetId 知识库ID + * @param parentId 父目录ID + * @return 总数 + */ + int countByDatasetIdAndParentId(@Param("difyDatasetId") String difyDatasetId, + @Param("parentId") Long parentId); + + /** + * 根据知识库id和name查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param name 文件/文件夹名称(支持模糊查询) + * @param orderBy 排序字段 + * @param orderDirection 排序方向 + * @param offset 偏移量 + * @param limit 限制数量 + * @return 文件/文件夹列表 + */ + List selectByDatasetIdAndName(@Param("difyDatasetId") String difyDatasetId, + @Param("name") String name, + @Param("orderBy") String orderBy, + @Param("orderDirection") String orderDirection, + @Param("offset") Integer offset, + @Param("limit") Integer limit); + + /** + * 根据知识库id和name查询总数 + * + * @param difyDatasetId 知识库ID + * @param name 文件/文件夹名称 + * @return 总数 + */ + int countByDatasetIdAndName(@Param("difyDatasetId") String difyDatasetId, + @Param("name") String name); + + /** + * 根据ID查询文件/文件夹 + * + * @param id 文件/文件夹ID + * @return 文件/文件夹信息 + */ + TDatasetFiles selectById(@Param("id") Integer id); + + /** + * 插入文件/文件夹 + * + * @param datasetFiles 文件/文件夹信息 + * @return 影响行数 + */ + int insert(TDatasetFiles datasetFiles); + + /** + * 更新文件/文件夹信息 + * + * @param datasetFiles 文件/文件夹信息 + * @return 影响行数 + */ + int updateById(TDatasetFiles datasetFiles); + + /** + * 逻辑删除文件/文件夹(设置 is_deleted = true) + * + * @param id 文件/文件夹ID + * @return 影响行数 + */ + int deleteById(@Param("id") Integer id); + + /** + * 根据父目录ID逻辑删除所有子文件/文件夹 + * + * @param parentId 父目录ID + * @return 影响行数 + */ + int deleteByParentId(@Param("parentId") Long parentId); + + /** + * 重命名文件/文件夹 + * + * @param id 文件/文件夹ID + * @param newName 新名称 + * @param newPath 新路径 + * @return 影响行数 + */ + int renameById(@Param("id") Integer id, + @Param("newName") String newName, + @Param("newPath") String newPath); + + /** + * 检查同一父目录下是否存在同名文件/文件夹 + * + * @param parentId 父目录ID + * @param name 文件/文件夹名称 + * @param difyDatasetId 知识库ID + * @param excludeId 排除的ID(用于重命名时排除自己) + * @return 存在的记录数 + */ + int checkNameExists(@Param("parentId") Long parentId, + @Param("name") String name, + @Param("difyDatasetId") String difyDatasetId, + @Param("excludeId") Integer excludeId); + + /** + * 根据路径查询文件/文件夹 + * + * @param path 路径 + * @param difyDatasetId 知识库ID + * @return 文件/文件夹信息 + */ + TDatasetFiles selectByPath(@Param("path") String path, + @Param("difyDatasetId") String difyDatasetId); +} \ No newline at end of file diff --git a/chat-server/src/main/java/com/bjtds/brichat/service/DatasetFilesService.java b/chat-server/src/main/java/com/bjtds/brichat/service/DatasetFilesService.java new file mode 100644 index 0000000..af94014 --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/service/DatasetFilesService.java @@ -0,0 +1,129 @@ +package com.bjtds.brichat.service; + +import com.bjtds.brichat.entity.dataset.TDatasetFiles; +import com.bjtds.common.utils.Pagination; + +import java.util.List; + +/** + * 知识库文件/文件夹业务层接口 + * + * @author system + */ +public interface DatasetFilesService { + + /** + * 根据知识库id和parent_id 分页查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param parentId 父目录ID + * @param orderBy 排序字段 (name, size, created_at, updated_at) + * @param orderDirection 排序方向 (ASC, DESC) + * @param pageNo 页码 + * @param pageSize 页大小 + * @return 分页结果 + */ + Pagination getFilesByDatasetIdAndParentId(String difyDatasetId, + Long parentId, + String orderBy, + String orderDirection, + Integer pageNo, + Integer pageSize); + + /** + * 根据知识库id和name查询 文件/文件夹 + * + * @param difyDatasetId 知识库ID + * @param name 文件/文件夹名称(支持模糊查询) + * @param orderBy 排序字段 + * @param orderDirection 排序方向 + * @param pageNo 页码 + * @param pageSize 页大小 + * @return 分页结果 + */ + Pagination getFilesByDatasetIdAndName(String difyDatasetId, + String name, + String orderBy, + String orderDirection, + Integer pageNo, + Integer pageSize); + + /** + * 根据ID查询文件/文件夹详情 + * + * @param id 文件/文件夹ID + * @return 文件/文件夹信息 + */ + TDatasetFiles getFileById(Integer id); + + /** + * 创建文件/文件夹 + * + * @param datasetFiles 文件/文件夹信息对象 + * @return 创建的文件/文件夹信息 + */ + TDatasetFiles createFile(TDatasetFiles datasetFiles); + + /** + * 删除文件/文件夹(逻辑删除) + * + * @param id 文件/文件夹ID + * @return 是否删除成功 + */ + boolean deleteFile(Integer id); + + /** + * 重命名文件/文件夹 + * + * @param id 文件/文件夹ID + * @param newName 新名称 + * @return 是否重命名成功 + */ + boolean renameFile(Integer id, String newName); + + /** + * 检查同一父目录下是否存在同名文件/文件夹 + * + * @param parentId 父目录ID + * @param name 文件/文件夹名称 + * @param difyDatasetId 知识库ID + * @param excludeId 排除的ID(用于重命名时排除自己) + * @return 是否存在同名文件/文件夹 + */ + boolean checkNameExists(Long parentId, String name, String difyDatasetId, Integer excludeId); + + /** + * 根据路径查询文件/文件夹 + * + * @param path 路径 + * @param difyDatasetId 知识库ID + * @return 文件/文件夹信息 + */ + TDatasetFiles getFileByPath(String path, String difyDatasetId); + + /** + * 更新文件/文件夹信息 + * + * @param datasetFiles 文件/文件夹信息 + * @return 是否更新成功 + */ + boolean updateFile(TDatasetFiles datasetFiles); + + /** + * 构建文件完整路径 + * + * @param parentPath 父目录路径 + * @param fileName 文件名 + * @return 完整路径 + */ + String buildFilePath(String parentPath, String fileName); + + /** + * 获取父目录路径 + * + * @param parentId 父目录ID + * @param difyDatasetId 知识库ID + * @return 父目录路径,如果是根目录返回空字符串 + */ + String getParentPath(Long parentId, String difyDatasetId); +} \ No newline at end of file diff --git a/chat-server/src/main/java/com/bjtds/brichat/service/impl/DatasetFilesServiceImpl.java b/chat-server/src/main/java/com/bjtds/brichat/service/impl/DatasetFilesServiceImpl.java new file mode 100644 index 0000000..4f61682 --- /dev/null +++ b/chat-server/src/main/java/com/bjtds/brichat/service/impl/DatasetFilesServiceImpl.java @@ -0,0 +1,331 @@ +package com.bjtds.brichat.service.impl; + +import com.bjtds.brichat.entity.dataset.TDatasetFiles; +import com.bjtds.brichat.mapper.opengauss.TDatasetFilesMapper; +import com.bjtds.brichat.service.DatasetFilesService; +import com.bjtds.common.utils.Pagination; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.List; + +/** + * 知识库文件/文件夹业务层实现类 + * + * @author system + */ +@Service +public class DatasetFilesServiceImpl implements DatasetFilesService { + + @Autowired + private TDatasetFilesMapper datasetFilesMapper; + + // 允许的排序字段 + private static final List ALLOWED_ORDER_FIELDS = Arrays.asList("name", "size", "created_at", "updated_at", "type"); + + @Override + public Pagination getFilesByDatasetIdAndParentId(String difyDatasetId, + Long parentId, + String orderBy, + String orderDirection, + Integer pageNo, + Integer pageSize) { + // 参数校验 + if (!StringUtils.hasText(difyDatasetId)) { + throw new IllegalArgumentException("知识库ID不能为空"); + } + if (pageNo == null || pageNo < 1) { + pageNo = 1; + } + if (pageSize == null || pageSize < 1) { + pageSize = 20; + } + + // 排序参数校验 + orderBy = validateOrderBy(orderBy); + orderDirection = validateOrderDirection(orderDirection); + + // 计算偏移量 + Integer offset = (pageNo - 1) * pageSize; + + // 查询数据 + List files = datasetFilesMapper.selectByDatasetIdAndParentId( + difyDatasetId, parentId, orderBy, orderDirection, offset, pageSize); + + // 查询总数 + int total = datasetFilesMapper.countByDatasetIdAndParentId(difyDatasetId, parentId); + + // 构建分页结果 + Pagination pagination = new Pagination<>(); + pagination.setContent(files); + pagination.setTotal((long) total); + pagination.setPageNo(pageNo); + pagination.setPageSize(pageSize); + + return pagination; + } + + @Override + public Pagination getFilesByDatasetIdAndName(String difyDatasetId, + String name, + String orderBy, + String orderDirection, + Integer pageNo, + Integer pageSize) { + // 参数校验 + if (!StringUtils.hasText(difyDatasetId)) { + throw new IllegalArgumentException("知识库ID不能为空"); + } + if (!StringUtils.hasText(name)) { + throw new IllegalArgumentException("搜索名称不能为空"); + } + if (pageNo == null || pageNo < 1) { + pageNo = 1; + } + if (pageSize == null || pageSize < 1) { + pageSize = 20; + } + + // 排序参数校验 + orderBy = validateOrderBy(orderBy); + orderDirection = validateOrderDirection(orderDirection); + + // 计算偏移量 + Integer offset = (pageNo - 1) * pageSize; + + // 查询数据 + List files = datasetFilesMapper.selectByDatasetIdAndName( + difyDatasetId, name, orderBy, orderDirection, offset, pageSize); + + // 查询总数 + int total = datasetFilesMapper.countByDatasetIdAndName(difyDatasetId, name); + + // 构建分页结果 + Pagination pagination = new Pagination<>(); + pagination.setContent(files); + pagination.setTotal((long) total); + pagination.setPageNo(pageNo); + pagination.setPageSize(pageSize); + + return pagination; + } + + @Override + public TDatasetFiles getFileById(Integer id) { + if (id == null) { + throw new IllegalArgumentException("文件ID不能为空"); + } + return datasetFilesMapper.selectById(id); + } + + @Override + @Transactional + public TDatasetFiles createFile(TDatasetFiles datasetFiles) { + // 参数校验 + if (datasetFiles == null) { + throw new IllegalArgumentException("文件/文件夹信息不能为空"); + } + if (!StringUtils.hasText(datasetFiles.getName())) { + throw new IllegalArgumentException("文件/文件夹名称不能为空"); + } + if (!StringUtils.hasText(datasetFiles.getType())) { + throw new IllegalArgumentException("类型不能为空"); + } + if (!"file".equals(datasetFiles.getType()) && !"folder".equals(datasetFiles.getType())) { + throw new IllegalArgumentException("类型必须是 file 或 folder"); + } + if (!StringUtils.hasText(datasetFiles.getDifyDatasetId())) { + throw new IllegalArgumentException("知识库ID不能为空"); + } + if (datasetFiles.getOwnerId() == null) { + throw new IllegalArgumentException("所有者ID不能为空"); + } + + // 检查同名文件/文件夹是否存在 + if (checkNameExists(datasetFiles.getParentId(), datasetFiles.getName(), datasetFiles.getDifyDatasetId(), null)) { + throw new IllegalArgumentException("同一目录下已存在同名文件/文件夹:" + datasetFiles.getName()); + } + + // 构建路径 + String parentPath = getParentPath(datasetFiles.getParentId(), datasetFiles.getDifyDatasetId()); + String fullPath = buildFilePath(parentPath, datasetFiles.getName()); + datasetFiles.setPath(fullPath); + + // 设置默认值 + if (datasetFiles.getSize() == null) { + datasetFiles.setSize(0L); + } + if (datasetFiles.getIsDeleted() == null) { + datasetFiles.setIsDeleted(false); + } + + // 根据文件类型设置相应字段 + if ("file".equals(datasetFiles.getType())) { + // 文件类型需要确保有 difyDocId, indexingStatus, difyDatasetId + if (!StringUtils.hasText(datasetFiles.getDifyDocId())) { + // 如果没有提供 difyDocId,可以在这里生成或抛出异常 + // datasetFiles.setDifyDocId(generateDifyDocId()); // 根据业务需求实现 + } + if (!StringUtils.hasText(datasetFiles.getIndexingStatus())) { + // 可以设置默认的索引状态 + datasetFiles.setIndexingStatus("pending"); // 或其他默认状态 + } + } else if ("folder".equals(datasetFiles.getType())) { + // 文件夹类型只需要 difyDatasetId,清除文件相关字段 + datasetFiles.setDifyDocId(null); + datasetFiles.setIndexingStatus(null); + datasetFiles.setMimeType(null); + datasetFiles.setExtension(null); + } + + // 插入数据库 + int result = datasetFilesMapper.insert(datasetFiles); + if (result > 0) { + return datasetFiles; + } else { + throw new RuntimeException("创建文件/文件夹失败"); + } + } + + @Override + @Transactional + public boolean deleteFile(Integer id) { + if (id == null) { + throw new IllegalArgumentException("文件ID不能为空"); + } + + // 查询文件信息 + TDatasetFiles file = datasetFilesMapper.selectById(id); + if (file == null) { + throw new IllegalArgumentException("文件/文件夹不存在"); + } + + // 如果是文件夹,需要删除所有子文件/文件夹 + if ("folder".equals(file.getType())) { + datasetFilesMapper.deleteByParentId(id.longValue()); + } + + // 删除文件/文件夹本身 + return datasetFilesMapper.deleteById(id) > 0; + } + + @Override + @Transactional + public boolean renameFile(Integer id, String newName) { + if (id == null) { + throw new IllegalArgumentException("文件ID不能为空"); + } + if (!StringUtils.hasText(newName)) { + throw new IllegalArgumentException("新名称不能为空"); + } + + // 查询原文件信息 + TDatasetFiles file = datasetFilesMapper.selectById(id); + if (file == null) { + throw new IllegalArgumentException("文件/文件夹不存在"); + } + + // 检查同名文件/文件夹是否存在(排除自己) + if (checkNameExists(file.getParentId(), newName, file.getDifyDatasetId(), id)) { + throw new IllegalArgumentException("同一目录下已存在同名文件/文件夹:" + newName); + } + + // 构建新路径 + String parentPath = getParentPath(file.getParentId(), file.getDifyDatasetId()); + String newPath = buildFilePath(parentPath, newName); + + // 更新名称和路径 + return datasetFilesMapper.renameById(id, newName, newPath) > 0; + } + + @Override + public boolean checkNameExists(Long parentId, String name, String difyDatasetId, Integer excludeId) { + if (!StringUtils.hasText(name) || !StringUtils.hasText(difyDatasetId)) { + return false; + } + int count = datasetFilesMapper.checkNameExists(parentId, name, difyDatasetId, excludeId); + return count > 0; + } + + @Override + public TDatasetFiles getFileByPath(String path, String difyDatasetId) { + if (!StringUtils.hasText(path) || !StringUtils.hasText(difyDatasetId)) { + return null; + } + return datasetFilesMapper.selectByPath(path, difyDatasetId); + } + + @Override + @Transactional + public boolean updateFile(TDatasetFiles datasetFiles) { + if (datasetFiles == null || datasetFiles.getId() == null) { + throw new IllegalArgumentException("文件信息或ID不能为空"); + } + return datasetFilesMapper.updateById(datasetFiles) > 0; + } + + @Override + public String buildFilePath(String parentPath, String fileName) { + if (!StringUtils.hasText(fileName)) { + throw new IllegalArgumentException("文件名不能为空"); + } + + if (!StringUtils.hasText(parentPath) || "/".equals(parentPath)) { + return "/" + fileName; + } + + // 确保父路径以 / 开头,不以 / 结尾 + if (!parentPath.startsWith("/")) { + parentPath = "/" + parentPath; + } + if (parentPath.endsWith("/") && !"/".equals(parentPath)) { + parentPath = parentPath.substring(0, parentPath.length() - 1); + } + + return parentPath + "/" + fileName; + } + + @Override + public String getParentPath(Long parentId, String difyDatasetId) { + if (parentId == null) { + return "/"; + } + + TDatasetFiles parentFile = datasetFilesMapper.selectById(parentId.intValue()); + if (parentFile == null) { + return "/"; + } + + return parentFile.getPath(); + } + + /** + * 校验排序字段 + */ + private String validateOrderBy(String orderBy) { + if (!StringUtils.hasText(orderBy)) { + return null; + } + if (!ALLOWED_ORDER_FIELDS.contains(orderBy.toLowerCase())) { + return null; + } + return orderBy.toLowerCase(); + } + + /** + * 校验排序方向 + */ + private String validateOrderDirection(String orderDirection) { + if (!StringUtils.hasText(orderDirection)) { + return "ASC"; + } + String direction = orderDirection.toUpperCase(); + if (!"ASC".equals(direction) && !"DESC".equals(direction)) { + return "ASC"; + } + return direction; + } +} \ No newline at end of file diff --git a/chat-server/src/main/resources/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.xml b/chat-server/src/main/resources/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.xml new file mode 100644 index 0000000..4a90dad --- /dev/null +++ b/chat-server/src/main/resources/com/bjtds/brichat/mapper/opengauss/TDatasetFilesMapper.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + id, name, type, parent_id, path, size, owner_id, created_at, updated_at, + is_deleted, mime_type, extension, dify_doc_id, indexing_status, dify_dataset_id + + + + + + + + + + + + + + + + + + + + INSERT INTO t_dataset_files ( + name, type, parent_id, path, size, owner_id, + is_deleted, mime_type, extension, dify_doc_id, + indexing_status, dify_dataset_id + ) VALUES ( + #{name}, #{type}, #{parentId}, #{path}, #{size}, #{ownerId}, + #{isDeleted}, #{mimeType}, #{extension}, #{difyDocId}, + #{indexingStatus}, #{difyDatasetId} + ) + + + + + UPDATE t_dataset_files + + name = #{name}, + type = #{type}, + parent_id = #{parentId}, + path = #{path}, + size = #{size}, + owner_id = #{ownerId}, + is_deleted = #{isDeleted}, + mime_type = #{mimeType}, + extension = #{extension}, + dify_doc_id = #{difyDocId}, + indexing_status = #{indexingStatus}, + dify_dataset_id = #{difyDatasetId}, + + WHERE id = #{id} + + + + + UPDATE t_dataset_files + SET is_deleted = true + WHERE id = #{id} + + + + + UPDATE t_dataset_files + SET is_deleted = true + WHERE parent_id = #{parentId} + + + + + UPDATE t_dataset_files + SET name = #{newName}, path = #{newPath} + WHERE id = #{id} + + + + + + + + + \ No newline at end of file