diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 78e1437..3a11f8b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -38,7 +38,21 @@ "Bash(mvn install:*)", "Bash(mvn spring-boot:run -q)", "Bash(git config:*)", - "Bash(env)" + "Bash(env)", + "Bash(javap -p C:/Project/java/all-docs/all-docs-infrastructure/target/classes/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.class)", + "Bash(javap:*)", + "Bash(git log:*)", + "Bash(mvn spring-boot:run -pl all-docs-bootstrap -q)", + "Bash(taskkill //F //IM java.exe)", + "Bash(netstat -ano)", + "Bash(mvn spring-boot:run -pl all-docs-bootstrap)", + "Bash(grep:*)", + "Bash(ls -la *.md *.sql)", + "Bash(mvn dependency:tree)", + "Bash(git:*)", + "Bash(xxd \"C:\\\\Project\\\\java\\\\all-docs\\\\all-docs-bootstrap\\\\src\\\\main\\\\resources\\\\i18n\\\\messages_zh_CN.properties\")", + "Bash(./mvnw compile:*)", + "Bash(xxd \"C:\\\\Project\\\\java\\\\all-docs\\\\all-docs-infrastructure\\\\src\\\\main\\\\resources\\\\mapper\\\\CommentMapper.xml\")" ] } } diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..fe1eb14 --- /dev/null +++ b/TODO.md @@ -0,0 +1,44 @@ +# 待补充功能 + +以下功能在 Statistics API 扩展中暂未实现,后续补充: + +## 1. 浏览量统计 (viewNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的文档浏览次数 + +**方案:** 在文档浏览接口中增加 Redis ZSet 计数,key 为 `doc:hot` + +**待办:** +- [ ] 在文档浏览接口中调用 `redisService.incrementDocScore(docId, 1)` +- [ ] 验证 `doc:hot` ZSet 是否正常更新 + +## 2. 下载次数统计 (downloadNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的文档下载次数 + +**方案:** 在文档下载接口中增加 Redis 计数 + +**待办:** +- [ ] 确定下载接口位置 +- [ ] 增加下载计数逻辑 +- [ ] 在 `all()` 方法中聚合下载次数 + +## 3. 搜索次数统计 (searchNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的搜索次数 + +**方案:** 使用 Redis ZSet `search:hot` 的总分数作为搜索次数 + +**待办:** +- [ ] 在 `all()` 方法中聚合 `search:hot` ZSet 的总分 + +## 4. 用户活跃度统计 (userActivity) + +**目标:** 实现 `/statistics/userActivity` 接口 + +**方案:** 在用户登录/关键操作时记录活跃状态,按月聚合 + +**待办:** +- [ ] 设计用户活跃度记录机制(Redis 或数据库) +- [ ] 实现按月统计活跃用户数 +- [ ] 实现 `userActivity()` 方法 diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/config/JwtFilterConfig.java b/all-docs-api/src/main/java/com/jiaruiblog/api/config/JwtFilterConfig.java new file mode 100644 index 0000000..ec91d7a --- /dev/null +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/config/JwtFilterConfig.java @@ -0,0 +1,23 @@ +package com.jiaruiblog.api.config; + +import com.jiaruiblog.api.filter.JwtFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author luojiarui + **/ +@Configuration +public class JwtFilterConfig { + + @Bean + public FilterRegistrationBean jwtFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new JwtFilter()); + registration.addUrlPatterns("/*"); + registration.setName("JwtFilter"); + registration.setOrder(1); + return registration; + } +} \ No newline at end of file diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CollectController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CollectController.java index ee78575..cd4e69e 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CollectController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CollectController.java @@ -53,8 +53,13 @@ public ApiResult insert(@RequestBody CollectDTO collect, HttpServletReques log.error("文档不存在,文档ID: {}", relationship.getDocId()); throw new BusinessException(ErrorCode.DOCUMENT_NOT_FOUND); } - collectService.insert(relationship); - log.info("文档收藏成功,文档ID: {}, 用户ID: {}", relationship.getDocId(), relationship.getUserId()); + try { + collectService.insert(relationship); + log.info("文档收藏成功,文档ID: {}, 用户ID: {}", relationship.getDocId(), relationship.getUserId()); + } catch (Exception e) { + log.error("文档收藏失败,文档ID: {}, 用户ID: {}, 错误: {}", relationship.getDocId(), relationship.getUserId(), e.getMessage(), e); + throw e; + } return ApiResult.success(); } @@ -68,8 +73,13 @@ public ApiResult insert(@RequestBody CollectDTO collect, HttpServletReques public ApiResult remove(@RequestBody CollectDTO collect, HttpServletRequest request) { log.info("开始执行取消收藏操作,文档ID: {}, 用户ID: {}", collect.getDocId(), request.getAttribute("id")); CollectDocRelationship relationship = setRelationshipValue(collect, request); - collectService.remove(relationship); - log.info("取消收藏成功,文档ID: {}, 用户ID: {}", relationship.getDocId(), relationship.getUserId()); + try { + collectService.remove(relationship); + log.info("取消收藏成功,文档ID: {}, 用户ID: {}", relationship.getDocId(), relationship.getUserId()); + } catch (Exception e) { + log.error("取消收藏失败,文档ID: {}, 用户ID: {}, 错误: {}", relationship.getDocId(), relationship.getUserId(), e.getMessage(), e); + throw e; + } return ApiResult.success(); } diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java index f9fcb3a..ac06f1d 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java @@ -2,10 +2,12 @@ import com.jiaruiblog.api.auth.Permission; import com.jiaruiblog.application.service.ICommentService; +import com.jiaruiblog.application.service.converter.CommentConverter; import com.jiaruiblog.common.ApiResult; import com.jiaruiblog.common.exception.BusinessException; import com.jiaruiblog.common.exception.ErrorCode; import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.dto.BatchIdDTO; import com.jiaruiblog.domain.entity.dto.CommentDTO; @@ -13,6 +15,7 @@ import com.jiaruiblog.domain.entity.vo.CommentWithUserVO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.infrastructure.repository.DocumentRepository; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -22,9 +25,11 @@ import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * 评论系统的控制器 @@ -40,6 +45,12 @@ public class CommentController { @Resource ICommentService commentService; + @Resource + CommentConverter commentConverter; + + @Resource + DocumentRepository documentRepository; + @Operation(summary = "新增单个评论", description = "添加新的评论") @PostMapping(value = "/auth/insert") public ApiResult insert(@RequestBody CommentDTO commentDTO, HttpServletRequest request) { @@ -102,7 +113,8 @@ private Comment getComment(CommentDTO commentDTO, HttpServletRequest request) { @PostMapping(value = "/auth/myComments") public ApiResult> queryMyComments(@RequestBody BasePageDTO pageDTO, HttpServletRequest request) { String userId = (String) request.getAttribute("id"); - PageVO result = commentService.queryAllComments(pageDTO, userId, false); + PageVO commentPage = commentService.queryAllComments(pageDTO, userId, false); + PageVO result = buildCommentWithUserVO(commentPage); return ApiResult.success(result); } @@ -110,7 +122,44 @@ public ApiResult> queryMyComments(@RequestBody BasePag @Permission(PermissionEnum.ADMIN) @PostMapping(value = "/auth/allComments") public ApiResult> queryAllComments(@RequestBody BasePageDTO pageDTO) { - PageVO result = commentService.queryAllComments(pageDTO, null, true); + PageVO commentPage = commentService.queryAllComments(pageDTO, null, true); + PageVO result = buildCommentWithUserVO(commentPage); return ApiResult.success(result); } + + private PageVO buildCommentWithUserVO(PageVO commentPage) { + if (commentPage == null || commentPage.getList() == null || commentPage.getList().isEmpty()) { + return PageVO.builder() + .total(0) + .list(new ArrayList<>()) + .pageNum(commentPage != null ? commentPage.getPageNum() : 1) + .pageSize(commentPage != null ? commentPage.getPageSize() : 10) + .build(); + } + + List docIdList = commentPage.getList().stream() + .map(Comment::getDocId) + .collect(Collectors.toList()); + + List documents = documentRepository.findByIdList(docIdList); + Map docNameMap = documents.stream() + .collect(Collectors.toMap(FileDocument::getId, FileDocument::getName)); + +List voList = commentPage.getList().stream() + .map(comment -> { + String docName = docNameMap.get(comment.getDocId()); + if (docName == null) { + docName = comment.getDocName(); // fallback to stored docName when document is deleted + } + return commentConverter.toVO(comment, docName); + }) + .collect(Collectors.toList()); + + return PageVO.builder() + .total(commentPage.getTotal()) + .list(voList) + .pageNum(commentPage.getPageNum()) + .pageSize(commentPage.getPageSize()) + .build(); + } } \ No newline at end of file diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocLogController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocLogController.java index 87532ad..d37b352 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocLogController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocLogController.java @@ -36,7 +36,7 @@ @Slf4j @CrossOrigin @RestController -@RequestMapping("/api/v1/doc-log") +@RequestMapping("/api/v1/docLog") public class DocLogController { @Resource diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocReviewController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocReviewController.java index 3dc69a1..d0960c7 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocReviewController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocReviewController.java @@ -37,7 +37,7 @@ @Slf4j @CrossOrigin @RestController -@RequestMapping("/api/v1/doc-review") +@RequestMapping("/api/v1/docReview") public class DocReviewController { @Resource diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocumentController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocumentController.java index 043e30d..c10eebb 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocumentController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocumentController.java @@ -1,25 +1,25 @@ package com.jiaruiblog.api.controller; import com.jiaruiblog.api.auth.Permission; -import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.api.intercepter.SensitiveFilter; +import com.jiaruiblog.application.service.DocumentService; +import com.jiaruiblog.application.service.IDocLogService; +import com.jiaruiblog.application.service.RedisService; +import com.jiaruiblog.application.service.impl.DocLogServiceImpl; +import com.jiaruiblog.application.service.impl.RedisServiceImpl; import com.jiaruiblog.common.ApiResult; -import com.jiaruiblog.domain.entity.po.FileDocument; -import com.jiaruiblog.domain.entity.po.User; +import com.jiaruiblog.common.enums.FilterTypeEnum; +import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.common.exception.BusinessException; +import com.jiaruiblog.common.exception.ErrorCode; import com.jiaruiblog.domain.entity.dto.DocumentDTO; import com.jiaruiblog.domain.entity.dto.RemoveObjectDTO; +import com.jiaruiblog.domain.entity.dto.SearchQuery; import com.jiaruiblog.domain.entity.dto.document.UpdateInfoDTO; +import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.po.User; import com.jiaruiblog.domain.entity.vo.DocWithCateVO; -import com.jiaruiblog.domain.entity.vo.DocumentVO; import com.jiaruiblog.domain.entity.vo.PageVO; -import com.jiaruiblog.common.enums.FilterTypeEnum; -import com.jiaruiblog.common.exception.BusinessException; -import com.jiaruiblog.common.exception.ErrorCode; -import com.jiaruiblog.api.intercepter.SensitiveFilter; -import com.jiaruiblog.application.service.IDocLogService; -import com.jiaruiblog.application.service.DocumentService; -import com.jiaruiblog.application.service.RedisService; -import com.jiaruiblog.application.service.impl.DocLogServiceImpl; -import com.jiaruiblog.application.service.impl.RedisServiceImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Resource; @@ -57,17 +57,18 @@ public class DocumentController { public ApiResult list(@RequestBody @Schema(description = "文档查询DTO") DocumentDTO documentDTO) throws IOException { String userId = documentDTO.getUserId(); - if (StringUtils.hasText(documentDTO.getFilterWord()) && - documentDTO.getType() == FilterTypeEnum.FILTER) { + if (StringUtils.hasText(documentDTO.getFilterWord())) { String filterWord = documentDTO.getFilterWord(); - //非法敏感词汇判断 + // 敏感词汇判断 SensitiveFilter filter = SensitiveFilter.getInstance(); int n = filter.checkSensitiveWord(filterWord, 0, 1); - //存在非法字符 + // 存在非法字符时仅记录日志,不影响搜索 if (n > 0) { log.error("这个人输入了非法字符--> {},不知道他到底要查什么~", filterWord); } else { + // 热门搜索词所有人都能看,记录到 ZSet redisService.incrementScoreByUserId(filterWord, RedisServiceImpl.SEARCH_KEY); + // 用户搜索历史,只在用户已登录时记录 if (StringUtils.hasText(userId)) { redisService.addSearchHistoryByUserId(userId, filterWord); } @@ -148,31 +149,13 @@ public ApiResult hot() { return ApiResult.success(keyList); } - @Operation(summary = "2.4 文档全文搜索", description = "根据关键字搜索已审核且解析成功的文档") - @GetMapping(value = "/search") + @Operation(summary = "文档搜索", description = "根据多条件搜索文档") + @PostMapping(value = "/searchList") + @Permission(value = PermissionEnum.USER) public ApiResult search( - @RequestParam(value = "keyword") - @Schema(description = "搜索关键字") String keyword, - @RequestParam(value = "page", defaultValue = "1") - @Schema(description = "页码") int page, - @RequestParam(value = "size", defaultValue = "10") - @Schema(description = "每页大小") int size) throws IOException { - if (keyword == null || keyword.trim().isEmpty()) { - return ApiResult.success(PageVO.builder() - .pageNum(page) - .pageSize(size) - .total(0) - .list(new java.util.ArrayList<>()) - .build()); - } - // 记录搜索词 - if (StringUtils.hasText(keyword)) { - SensitiveFilter filter = SensitiveFilter.getInstance(); - int n = filter.checkSensitiveWord(keyword, 0, 1); - if (n <= 0) { - redisService.incrementScoreByUserId(keyword, RedisServiceImpl.SEARCH_KEY); - } - } - return ApiResult.success(documentService.search(keyword.trim(), page, size)); + @RequestBody @Schema(description = "搜索参数") SearchQuery query, + HttpServletRequest request) { + String userId = (String) request.getAttribute("id"); + return ApiResult.success(documentService.search(query, userId)); } } \ No newline at end of file diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/FileController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/FileController.java index 774eafd..b6a17a3 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/FileController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/FileController.java @@ -3,34 +3,34 @@ import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import com.auth0.jwt.interfaces.Claim; -import com.jiaruiblog.common.constants.StorageConstants; -import com.jiaruiblog.common.enums.PermissionEnum; -import com.jiaruiblog.common.ApiResult; -import com.jiaruiblog.infrastructure.config.SystemConfig; -import com.jiaruiblog.domain.entity.po.FileDocument; -import com.jiaruiblog.domain.entity.po.User; -import com.jiaruiblog.domain.entity.dto.BasePageDTO; -import com.jiaruiblog.domain.entity.dto.upload.FileUploadDTO; -import com.jiaruiblog.domain.entity.dto.upload.UrlUploadDTO; -import com.jiaruiblog.common.enums.DocStateEnum; -import com.jiaruiblog.common.exception.BusinessException; -import com.jiaruiblog.common.exception.ErrorCode; +import com.jiaruiblog.api.auth.Permission; import com.jiaruiblog.api.intercepter.SensitiveFilter; -import com.jiaruiblog.application.service.IDocLogService; +import com.jiaruiblog.api.util.JwtUtil; import com.jiaruiblog.application.service.DocumentService; +import com.jiaruiblog.application.service.IDocLogService; import com.jiaruiblog.application.service.IUserService; import com.jiaruiblog.application.service.TaskExecuteService; import com.jiaruiblog.application.service.impl.DocLogServiceImpl; -import com.jiaruiblog.infrastructure.util.FileContentTypeUtils; +import com.jiaruiblog.common.ApiResult; +import com.jiaruiblog.common.constants.StorageConstants; +import com.jiaruiblog.common.enums.DocStateEnum; +import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.common.exception.BusinessException; +import com.jiaruiblog.common.exception.ErrorCode; import com.jiaruiblog.common.util.HmacUtil; -import com.jiaruiblog.api.util.JwtUtil; +import com.jiaruiblog.domain.entity.dto.BasePageDTO; +import com.jiaruiblog.domain.entity.dto.upload.FileUploadDTO; +import com.jiaruiblog.domain.entity.dto.upload.UrlUploadDTO; +import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.po.User; +import com.jiaruiblog.infrastructure.config.SystemConfig; +import com.jiaruiblog.infrastructure.util.FileContentTypeUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.apache.http.auth.AuthenticationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.data.redis.core.StringRedisTemplate; @@ -55,7 +55,6 @@ */ @Tag(name = "查询文档详情的接口") @Slf4j -@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/v1/file") public class FileController { @@ -98,7 +97,7 @@ public List list(@ModelAttribute BasePageDTO basePageDTO) { */ @Operation(summary = "查询文档预览结果") @GetMapping("/view/{id}") - public ResponseEntity serveFileOnline(@PathVariable String id) + public ResponseEntity serveFileOnline(@PathVariable String id, HttpServletRequest request) throws UnsupportedEncodingException { Optional fileOpt = fileService.getById(id); if (fileOpt.isEmpty()) { @@ -106,7 +105,11 @@ public ResponseEntity serveFileOnline(@PathVariable String id) } FileDocument fileDocument = fileOpt.get(); + String username = (String) request.getAttribute("username"); + String userId = (String) request.getAttribute("id"); User user = new User(); + user.setUsername(username); + user.setId(userId); docLogService.addLog(user, fileDocument, DocLogServiceImpl.Action.PREVIEW); // 从MinIO下载文件内容 @@ -278,19 +281,16 @@ public ResponseEntity downloadFileById(@PathVariable String id) throws U * @return BaseApiResult */ @PostMapping("auth/upload") + @Permission public ApiResult documentUpload(@RequestParam("file") MultipartFile file, - HttpServletRequest request) - throws AuthenticationException { + HttpServletRequest request) { String username = (String) request.getAttribute(USERNAME); String userId = (String) request.getAttribute("id"); + // 用户非管理员且普通用户禁止上传 User user = userService.queryById(userId); - if (user == null) { - throw new AuthenticationException(); - } - // 用户非管理员且普通用户禁止 - if (Boolean.TRUE.equals(!systemConfig.getUserUpload()) && user.getPermissionEnum() != PermissionEnum.ADMIN) { - throw new AuthenticationException(); + if (user == null || Boolean.TRUE.equals(!systemConfig.getUserUpload()) && user.getPermissionEnum() != PermissionEnum.ADMIN) { + throw new BusinessException(ErrorCode.FORBIDDEN); } fileService.documentUpload(file, userId, username); @@ -467,11 +467,9 @@ public byte[] previewThumb(@PathVariable String thumbId, } try (InputStream inputStream = fileService.getFileThumb(thumbId)) { if (inputStream == null) { - return new byte[0]; + return new byte[]{}; } - byte[] bytes = new byte[inputStream.available()]; - inputStream.read(bytes, 0, inputStream.available()); - return bytes; + return inputStream.readAllBytes(); } } @@ -495,6 +493,7 @@ public byte[] test() { public ResponseEntity previewThumb1(@PathVariable String id) throws IOException { if (StringUtils.hasText(id)) { + log.info("thumb endpoint called with id={}", id); InputStream inputStream = fileService.getFileThumb(id); if (inputStream == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); @@ -503,6 +502,7 @@ public ResponseEntity previewThumb1(@PathVariable String id) throws IOEx try (inputStream) { bytes = IoUtil.readBytes(inputStream); } + log.info("thumb endpoint result length={}", bytes.length); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "fileName=" + id) .header(HttpHeaders.CONTENT_TYPE, "image/png") @@ -522,9 +522,14 @@ public byte[] previewThumb2(@PathVariable String thumbId, // response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // return new byte[]{}; // } + log.info("thumbi_id : + {}", thumbId); // 设置响应头,缓存 1 小时 response.setHeader("Cache-Control", "max-age=3600, public"); - return fileService.getFileBytes(thumbId, StorageConstants.THUMBS); + // 缩略图存储时带了 .jpg 后缀,读取时需要拼接完整路径 + String objectKey = StorageConstants.thumbPath(thumbId, "jpg"); + byte[] result = fileService.getFileBytes(objectKey, ""); + log.info("image2 endpoint called with thumbId={}, result length={}", thumbId, result.length); + return result; } @GetMapping(value = "/text2/{txtId}", produces = MediaType.TEXT_PLAIN_VALUE) @@ -552,9 +557,12 @@ public static void extracted(HttpServletResponse response, byte[] buffer) throws // 解决跨域问题,配置可访问的域名 String allowedOrigin = System.getenv("CORS_ALLOWED_ORIGIN"); if (allowedOrigin == null || allowedOrigin.isEmpty()) { - allowedOrigin = "*"; + // 当 credentials 为 true 时,不能使用 "*" + // 使用 allowedOriginPatterns 代替,或者在前端配置具体的 origin + allowedOrigin = "http://localhost:8080"; } response.addHeader("Access-Control-Allow-Origin", allowedOrigin); + response.addHeader("Access-Control-Allow-Credentials", "true"); response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); //Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存 //attachment表示以附件方式下载 inline表示在线打开 "Content-Disposition: inline; filename=文件名.mp3" diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/LikeController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/LikeController.java index 3551c22..f86d8a2 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/LikeController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/LikeController.java @@ -28,12 +28,19 @@ public class LikeController{ // entityType: 1:点赞 // entityType: 2:收藏 - @PostMapping("") + @PostMapping("/") public ApiResult like(@RequestBody LikeRequest request, HttpServletRequest httpRequest) { String userId = (String) httpRequest.getAttribute("id"); - likeService.like(userId, request.getEntityType(), request.getEntityId()); - LikeVO result = buildLikeVO(userId, request.getEntityId()); - return ApiResult.success(result); + log.info("用户 {} 正在执行点赞操作,entityType={}, entityId={}", userId, request.getEntityType(), request.getEntityId()); + try { + likeService.like(userId, request.getEntityType(), request.getEntityId()); + LikeVO result = buildLikeVO(userId, request.getEntityId()); + log.info("用户 {} 点赞操作成功,entityType={}, entityId={}", userId, request.getEntityType(), request.getEntityId()); + return ApiResult.success(result); + } catch (Exception e) { + log.error("用户 {} 点赞操作失败,entityType={}, entityId={}", userId, request.getEntityType(), request.getEntityId(), e); + throw e; + } } @GetMapping("/info") diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java index c06357e..fe7ee69 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java @@ -10,9 +10,14 @@ import com.jiaruiblog.domain.entity.po.Tag; import com.jiaruiblog.domain.entity.po.TagDocRelationship; import com.jiaruiblog.domain.entity.dto.SearchKeyDTO; +import com.jiaruiblog.domain.entity.vo.CategoryDistVO; +import com.jiaruiblog.domain.entity.vo.DocTypeDistVO; import com.jiaruiblog.domain.entity.vo.DocumentVO; +import com.jiaruiblog.domain.entity.vo.HotDocVO; +import com.jiaruiblog.domain.entity.vo.SearchHotWordVO; import com.jiaruiblog.domain.entity.vo.StatsVO; import com.jiaruiblog.domain.entity.vo.TrendVO; +import com.jiaruiblog.domain.entity.vo.UserActivityVO; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -71,6 +76,36 @@ public ApiResult all() { return ApiResult.success(statisticsService.all()); } + @Operation(summary = "文档类型分布", description = "查询各文档类型的数量分布") + @GetMapping("/docTypeDist") + public ApiResult> docTypeDist() { + return ApiResult.success(statisticsService.docTypeDist()); + } + + @Operation(summary = "分类文档分布", description = "查询各分类下的文档数量") + @GetMapping("/categoryDist") + public ApiResult> categoryDist() { + return ApiResult.success(statisticsService.categoryDist()); + } + + @Operation(summary = "热门文档 TOP 10", description = "查询热门文档排行") + @GetMapping("/hotDocs") + public ApiResult> hotDocs() { + return ApiResult.success(statisticsService.hotDocs()); + } + + @Operation(summary = "搜索热词排行", description = "查询搜索热词排行") + @GetMapping("/searchHotWords") + public ApiResult> searchHotWords() { + return ApiResult.success(statisticsService.searchHotWords()); + } + + @Operation(summary = "用户活跃度趋势", description = "查询用户活跃度趋势") + @GetMapping("/userActivity") + public ApiResult> userActivity() { + return ApiResult.success(statisticsService.userActivity()); + } + /** * @return com.jiaruiblog.utils.ApiResult * @author luojiarui @@ -81,8 +116,9 @@ public ApiResult all() { public ApiResult>> getSearchResult(@RequestHeader HttpHeaders headers) { List userSearchList = Lists.newArrayList(); List stringList = headers.get("id"); + String userId = null; if (!CollectionUtils.isEmpty(stringList)) { - String userId = stringList.get(0); + userId = stringList.get(0); if (StringUtils.hasText(userId)) { userSearchList = redisService.getSearchHistoryByUserId(userId); } @@ -92,6 +128,8 @@ public ApiResult>> getSearchResult(@RequestHeader HttpH Map> result = new HashMap<>(); result.put("userSearch", userSearchList); result.put("hotSearch", hotSearchList); + log.info("getSearchResult called, userId={}, userSearchSize={}, hotSearchSize={}", + userId, userSearchList.size(), hotSearchList.size()); return ApiResult.success(result); } @@ -121,7 +159,10 @@ public ApiResult> getHotTrend() { List docIdList = redisService.getHotList(null, RedisServiceImpl.DOC_KEY); if (CollectionUtils.isEmpty(docIdList)) { - throw new BusinessException(ErrorCode.PARAMS_ERROR); + Map result = new HashMap<>(); + result.put("top1", null); + result.put("others", List.of()); + return ApiResult.success(result); } diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/SystemConfigController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/SystemConfigController.java index e726148..c2469b0 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/SystemConfigController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/SystemConfigController.java @@ -39,7 +39,7 @@ @Slf4j @CrossOrigin @RestController -@RequestMapping("/api/v1/system-config") +@RequestMapping("/api/v1/system") public class SystemConfigController { public static final String STATIC_CENSOR_WORD_TXT = "static" + File.separator + "censorWord.txt"; @@ -93,7 +93,7 @@ public void downloadTxt(HttpServletResponse response) { } } } catch (IOException ex) { - log.error("下载最新的违禁词错误"); + log.error("下载最新的违禁词错误", ex); } } diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/UserController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/UserController.java index 147de93..2b596f2 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/controller/UserController.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/UserController.java @@ -14,8 +14,8 @@ import com.jiaruiblog.common.exception.BusinessException; import com.jiaruiblog.common.exception.ErrorCode; import com.jiaruiblog.application.service.IUserService; +import com.jiaruiblog.application.service.ITokenService; import com.jiaruiblog.application.transformer.DTO2BOConverter; -import com.jiaruiblog.api.util.JwtUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -58,13 +58,16 @@ public class UserController { @Resource IUserService userService; + @Resource + ITokenService tokenService; + @Resource SystemConfig systemConfig; @Operation(summary = "新增单个用户", description = "新增单个用户") @PostMapping(value = "/insert") - public ApiResult insertObj(@RequestBody @Valid RegistryUserDTO userDTO) { + public ApiResult insertObj(@RequestBody @Valid BasicRegistryDTO userDTO) { // 判断是否开启用户注册 if (Boolean.FALSE.equals(systemConfig.getUserRegistry())) { throw new BusinessException(ErrorCode.PARAMS_ERROR); @@ -75,8 +78,8 @@ public ApiResult insertObj(@RequestBody @Valid RegistryUserDTO userDTO) @Operation(summary = "批量新增用户", description = "批量新增用户; 支持使用xls进行导入用户信息") @PostMapping(value = "/batchInsert") - public ApiResult batchInsert(@RequestBody List userDTOS) { - for (RegistryUserDTO item : userDTOS) { + public ApiResult batchInsert(@RequestBody List userDTOS) { + for (BasicRegistryDTO item : userDTOS) { userService.registry(item); } return ApiResult.success(); @@ -183,7 +186,7 @@ public ApiResult checkLoginState(HttpServletRequest request, HttpServlet if (!StringUtils.hasText(token)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } - Map userData = JwtUtil.verifyToken(token); + Map userData = tokenService.verifyToken(token); if (CollectionUtils.isEmpty(userData)) { throw new BusinessException(ErrorCode.OPERATE_FAILED); } diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/filter/JwtFilter.java b/all-docs-api/src/main/java/com/jiaruiblog/api/filter/JwtFilter.java index cec3d91..50653bd 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/filter/JwtFilter.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/filter/JwtFilter.java @@ -4,6 +4,7 @@ import com.jiaruiblog.api.util.JwtUtil; import com.jiaruiblog.common.context.TimeZoneContext; import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -23,7 +24,7 @@ * @version v2.0 */ @Slf4j -//@WebFilter(filterName = "JwtFilter", urlPatterns = {"/*"}) +@WebFilter(filterName = "JwtFilter", urlPatterns = {"/*"}) public class JwtFilter implements Filter { private static final String OPTIONS = "OPTIONS"; @@ -35,11 +36,17 @@ public class JwtFilter implements Filter { "/api/v1/user/login", "/api/v1/user/register", "/api/v1/file/view", + "/api/v1/file/view2", "/api/v1/file/image", + "/api/v1/file/image2", "/api/v1/document/list", "/api/v1/category/all" ); + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // No initialization needed + } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { @@ -109,4 +116,9 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } } + @Override + public void destroy() { + // No cleanup needed + } + } \ No newline at end of file diff --git a/all-docs-api/src/main/java/com/jiaruiblog/api/util/JwtUtil.java b/all-docs-api/src/main/java/com/jiaruiblog/api/util/JwtUtil.java index f38edc7..a2b9874 100644 --- a/all-docs-api/src/main/java/com/jiaruiblog/api/util/JwtUtil.java +++ b/all-docs-api/src/main/java/com/jiaruiblog/api/util/JwtUtil.java @@ -6,6 +6,8 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.jiaruiblog.domain.entity.po.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; @@ -18,21 +20,18 @@ * @Date 2022/6/19 9:20 下午 * @Version 1.0 **/ +@Component public class JwtUtil { private JwtUtil() { - throw new IllegalStateException("jwtUtil error"); + // Spring bean constructor } /** - * 密钥 - 从环境变量获取 + * 密钥持有类 - 解决静态字段无法注入的问题 */ - private static final String SECRET = System.getenv("JWT_SECRET"); - - static { - if (SECRET == null || SECRET.isEmpty()) { - throw new IllegalStateException("JWT_SECRET environment variable must be configured"); - } + private static class JwtSecretHolder { + private static String SECRET; } /** @@ -41,6 +40,11 @@ private JwtUtil() { **/ private static final long EXPIRATION = 864000L; + @Value("${jwt.secret}") + public void setSecret(String secret) { + JwtSecretHolder.SECRET = secret; + } + /** * 生成用户token,设置token超时时间 */ @@ -61,7 +65,7 @@ public static String createToken(User user) { //签发时间 .withIssuedAt(new Date()) //SECRET加密 - .sign(Algorithm.HMAC256(SECRET)); + .sign(Algorithm.HMAC256(JwtSecretHolder.SECRET)); } @@ -71,7 +75,7 @@ public static String createToken(User user) { public static Map verifyToken(String token) { DecodedJWT jwt; try { - JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build(); + JWTVerifier verifier = JWT.require(Algorithm.HMAC256(JwtSecretHolder.SECRET)).build(); jwt = verifier.verify(token); } catch (Exception e) { //解码异常则抛出异常 diff --git a/all-docs-application/pom.xml b/all-docs-application/pom.xml index 547bec2..f0ea5ab 100644 --- a/all-docs-application/pom.xml +++ b/all-docs-application/pom.xml @@ -64,6 +64,18 @@ itextpdf + + + org.apache.pdfbox + pdfbox + + + + + org.apache.poi + poi-ooxml + + org.ansj @@ -77,6 +89,13 @@ 0.4.20 + + + com.auth0 + java-jwt + ${java-jwt.version} + + org.projectlombok diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/config/QuartzConfig.java b/all-docs-application/src/main/java/com/jiaruiblog/application/config/QuartzConfig.java deleted file mode 100644 index 56b8de8..0000000 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/config/QuartzConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.jiaruiblog.application.config; - -import com.jiaruiblog.application.task.like.LikeTask; -import org.quartz.*; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 定时任务 Quartz - * @author luojiarui - **/ -@Configuration -public class QuartzConfig { - - private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz"; - - @Bean - public JobDetail quartzDetail(){ - return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build(); - - } - - @Bean - - public SimpleTrigger quartzTrigger() { - SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() - // .withIntervalInSeconds(10) - // 设置时间周期单位秒 - .withIntervalInHours(2) //两个小时执行一次 - .repeatForever(); - return TriggerBuilder.newTrigger().forJob(quartzDetail()) - .withIdentity(LIKE_TASK_IDENTITY) - .withSchedule(scheduleBuilder) - .build(); - } -} \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/DocumentService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/DocumentService.java index 6828728..b4be23c 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/DocumentService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/DocumentService.java @@ -3,12 +3,13 @@ import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.dto.DocumentDTO; +import com.jiaruiblog.domain.entity.dto.SearchQuery; import com.jiaruiblog.domain.entity.dto.document.UpdateInfoDTO; +import com.jiaruiblog.domain.entity.vo.DocSearchVO; import com.jiaruiblog.domain.entity.vo.DocWithCateVO; import com.jiaruiblog.domain.entity.vo.DocumentVO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.common.enums.DocStateEnum; -import org.apache.http.auth.AuthenticationException; import org.springframework.web.multipart.MultipartFile; import java.io.FileNotFoundException; @@ -29,10 +30,11 @@ public interface DocumentService { */ FileDocument saveFile(String md5, MultipartFile file); - /** +/** * 用户上传文档 + * @return FileDocument 上传后的文档对象 */ - void documentUpload(MultipartFile file, String userId, String username) throws AuthenticationException; + FileDocument documentUpload(MultipartFile file, String userId, String username); /** * 批量上传 @@ -232,4 +234,23 @@ void uploadByUrl(String category, List tags, String name, * @return 分页结果 */ PageVO search(String keyword, int pageNum, int pageSize); + + /** + * 多条件搜索文档(支持标签、分类筛选、排序、分页) + * + * @param query 搜索参数 + * @param userId 当前用户ID(用于查询点赞/收藏状态) + * @return 符合条件的文档分页结果 + */ + PageVO search(SearchQuery query, String userId); + + /** + * Get tag names for a document + */ + List getTagNamesByDocId(String docId); + + /** + * Get category name for a document + */ + String getCategoryNameByDocId(String docId); } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ElasticService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ElasticService.java index 2f2ea87..2ea5c4a 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ElasticService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ElasticService.java @@ -1,7 +1,10 @@ package com.jiaruiblog.application.service; +import com.jiaruiblog.domain.entity.dto.SearchQuery; +import com.jiaruiblog.domain.entity.dto.SearchResultItem; import com.jiaruiblog.domain.entity.po.SearchDocument; import com.jiaruiblog.domain.entity.vo.PageVO; +import com.jiaruiblog.domain.entity.vo.SearchResultVO; import java.io.InputStream; import java.util.List; @@ -94,4 +97,29 @@ public interface ElasticService { * 获取词云统计数据 */ java.util.List> getWordStat(); + +/** + * 多条件文档检索 + * @param query 搜索参数 + * @return 匹配的文档ID列表 + */ + List searchDocuments(SearchQuery query); + + /** + * 多条件文档检索(带高亮) + * @param query 搜索参数 + * @return 匹配的文档ID列表及高亮片段 + */ + java.util.List searchDocumentsWithHighlight(SearchQuery query); + + /** + * 全文检索 + 分页 + 多高亮片段 + * @param filterWord 关键词 + * @param tagId 标签ID(可选) + * @param categoryId 分类ID(可选) + * @param page 页码 + * @param rows 每页条数 + * @return SearchResultVO 含 total 和 items + */ + SearchResultVO searchDocumentsFullText(String filterWord, String tagId, String categoryId, int page, int rows); } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java index 33b9f89..464b10d 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java @@ -3,7 +3,6 @@ import com.jiaruiblog.domain.entity.po.Comment; import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.dto.CommentListDTO; -import com.jiaruiblog.domain.entity.vo.CommentWithUserVO; import com.jiaruiblog.domain.entity.vo.PageVO; import java.util.List; @@ -36,5 +35,5 @@ public interface ICommentService { long countAllFile(); - PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin); + PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin); } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/IDocLogService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/IDocLogService.java index 2401e9f..39243b4 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/IDocLogService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/IDocLogService.java @@ -17,14 +17,6 @@ */ public interface IDocLogService { - /** - * @author luojiarui - * @Description 新增操作日志 - * @Date 15:43 2022/11/5 - * @Param [docLog] - */ - void insert(DocLog docLog); - /** * @author luojiarui * @Description 删除操作日志 diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ITokenService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ITokenService.java new file mode 100644 index 0000000..0153691 --- /dev/null +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ITokenService.java @@ -0,0 +1,23 @@ +package com.jiaruiblog.application.service; + +import com.jiaruiblog.domain.entity.po.User; +import com.auth0.jwt.interfaces.Claim; + +import java.util.Map; + +/** + * Token服务接口 + * 负责生成和验证用户认证Token + */ +public interface ITokenService { + + /** + * 根据用户信息生成Token + */ + String createToken(User user); + + /** + * 验证Token并返回解析后的用户数据 + */ + Map verifyToken(String token); +} \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/IUserService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/IUserService.java index 6236a14..c72c5a4 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/IUserService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/IUserService.java @@ -4,6 +4,7 @@ import com.jiaruiblog.domain.entity.po.User; import com.jiaruiblog.domain.entity.bo.UserBO; import com.jiaruiblog.domain.entity.dto.BasePageDTO; +import com.jiaruiblog.domain.entity.dto.BasicRegistryDTO; import com.jiaruiblog.domain.entity.dto.RegistryUserDTO; import com.jiaruiblog.domain.entity.dto.UserRoleDTO; import com.jiaruiblog.domain.entity.vo.PageVO; @@ -22,12 +23,11 @@ public interface IUserService { Map login(RegistryUserDTO userDTO); - /** * 注册用户 - * @param userDTO 用户注册信息 + * @param userDTO 用户注册信息(简化版,仅需账号密码) */ - void registry(RegistryUserDTO userDTO); + void registry(BasicRegistryDTO userDTO); /** * @author luojiarui diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/LikeService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/LikeService.java index d767625..744b6cf 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/LikeService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/LikeService.java @@ -10,12 +10,13 @@ public interface LikeService { /** - * 增加点赞,取消点赞;增加收藏,取消收藏 + * 点赞或取消点赞(toggle) * @param userId 用户id * @param entityType 实体类型:1:点赞;2:收藏 * @param entityId 实体的id + * @return true=点赞成功, false=取消点赞成功 */ - void like(String userId, Integer entityType, String entityId); + boolean like(String userId, Integer entityType, String entityId); /** * 获取点赞的数量 @@ -29,7 +30,7 @@ public interface LikeService { * 获取当前用户点赞的状态 * @param userId 用户id * @param entityType 实体类型:1:点赞;2:收藏 - * @param entityId 实体的id + * @param entityId 实体id * @return 返回该用户点赞的状态 */ int findEntityLikeStatus(String userId, Integer entityType, String entityId); @@ -37,7 +38,6 @@ public interface LikeService { /** * @author luojiarui * @Description 新增点赞 - * @Date 13:40 2023/4/5 * @Param [like] **/ void insert(LikeDocRelationship like); @@ -45,7 +45,6 @@ public interface LikeService { /** * @author luojiarui * @Description 保存点赞/收藏信息到数据库中 - * @Date 13:43 2023/4/5 * @Param [like] * @return java.lang.Boolean **/ @@ -54,7 +53,6 @@ public interface LikeService { /** * @author luojiarui * @Description 移除点赞 - * @Date 13:40 2023/4/5 * @Param [like] **/ void remove(LikeDocRelationship like); @@ -62,7 +60,6 @@ public interface LikeService { /** * @author luojiarui * @Description 查询文档点赞数量 - * @Date 13:45 2023/4/5 * @Param [docId] * @return java.lang.Long **/ @@ -71,15 +68,7 @@ public interface LikeService { /** * @author luojiarui * @Description 根据文档ID移除所有相关点赞关系 - * @Date 13:45 2023/4/5 * @Param [docId] **/ void removeRelateByDocId(String docId); - - /** - * @author luojiarui - * @Description 将Redis中的点赞数据同步到数据库 - * @Date 13:45 2023/4/5 - **/ - void transLikedFromRedis2DB(); -} \ No newline at end of file +} diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/RedisService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/RedisService.java index 78ddea8..47c8904 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/RedisService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/RedisService.java @@ -252,4 +252,11 @@ public interface RedisService { * 增加用户搜索词分数 */ void incrementScoreByUserId(String searchWord, String key); + + /** + * 增加文档热度分数 + * @param docId 文档ID + * @param delta 增加的值(正数点赞,负数取消点赞) + */ + void incrementDocScore(String docId, int delta); } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/StatisticsService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/StatisticsService.java index c6df42c..fecf1d4 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/StatisticsService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/StatisticsService.java @@ -1,9 +1,14 @@ package com.jiaruiblog.application.service; import com.jiaruiblog.domain.entity.dto.StatisticsDTO; +import com.jiaruiblog.domain.entity.vo.CategoryDistVO; +import com.jiaruiblog.domain.entity.vo.DocTypeDistVO; +import com.jiaruiblog.domain.entity.vo.HotDocVO; import com.jiaruiblog.domain.entity.vo.MonthStatVO; +import com.jiaruiblog.domain.entity.vo.SearchHotWordVO; import com.jiaruiblog.domain.entity.vo.StatsVO; import com.jiaruiblog.domain.entity.vo.TrendVO; +import com.jiaruiblog.domain.entity.vo.UserActivityVO; import java.util.List; import java.util.Map; @@ -92,4 +97,29 @@ public interface StatisticsService { * 获取月度统计数据 */ List getMonthStat(); + + /** + * 按文档类型统计分布 + */ + List docTypeDist(); + + /** + * 按分类统计分布 + */ + List categoryDist(); + + /** + * 获取热门文档 + */ + List hotDocs(); + + /** + * 获取搜索热词 + */ + List searchHotWords(); + + /** + * 获取用户活动统计 + */ + List userActivity(); } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ThumbnailService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ThumbnailService.java index 09ad4c6..f159723 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/ThumbnailService.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ThumbnailService.java @@ -35,4 +35,46 @@ public interface ThumbnailService { * @return java.lang.String 预览图ID */ String makePreview(InputStream inputStream, String fileName); + + /** + * @Description 生成预览图 + * @Param [inputStream, fileName, previewId] - previewId 为null时自动生成UUID + * @return java.lang.String 预览图ID + */ + String makePreview(InputStream inputStream, String fileName, String previewId); + + /** + * @Description 生成PDF预览图 + * @Param [inputStream, fileName, previewId] - previewId 为null时自动生成UUID + * @return java.lang.String 预览图ID + */ + String makePreviewForPdf(InputStream inputStream, String fileName, String previewId); + + /** + * @Description 生成PPT/PPTX预览图 + * @Param [inputStream, fileName, previewId] - previewId 为null时自动生成UUID + * @return java.lang.String 预览图ID + */ + String makePreviewForPpt(InputStream inputStream, String fileName, String previewId); + + /** + * @Description 生成PDF缩略图(第一页渲染),存储到thumbnails文件夹 + * @Param [inputStream, fileName, thumbId] - thumbId 为null时自动生成UUID + * @return java.lang.String 缩略图ID + */ + String makeThumbForPdf(InputStream inputStream, String fileName, String thumbId); + + /** + * @Description 生成PPT缩略图(第一页渲染),存储到thumbnails文件夹 + * @Param [inputStream, fileName, thumbId] - thumbId 为null时自动生成UUID + * @return java.lang.String 缩略图ID + */ + String makeThumbForPpt(InputStream inputStream, String fileName, String thumbId); + + /** + * @Description 生成DOCX缩略图(首页渲染),存储到thumbnails文件夹 + * @Param [inputStream, fileName, thumbId] - thumbId 为null时自动生成UUID + * @return java.lang.String 缩略图ID + */ + String makeThumbForDocx(InputStream inputStream, String fileName, String thumbId); } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java new file mode 100644 index 0000000..4f7fbf1 --- /dev/null +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java @@ -0,0 +1,21 @@ +package com.jiaruiblog.application.service.converter; + +import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.domain.entity.vo.CommentWithUserVO; +import org.springframework.stereotype.Component; + +@Component +public class CommentConverter { + + public CommentWithUserVO toVO(Comment comment, String docName) { + CommentWithUserVO vo = new CommentWithUserVO(); + vo.setId(comment.getId()); + vo.setUserId(comment.getUserId()); + vo.setUserName(comment.getUserName()); + vo.setContent(comment.getContent()); + vo.setDocId(comment.getDocId()); + vo.setDocName(docName); + vo.setCreateDate(comment.getCreateDate()); + return vo; + } +} diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java index 61a3699..5dc68f2 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java @@ -17,6 +17,7 @@ import com.jiaruiblog.infrastructure.repository.CollectRepository; import com.jiaruiblog.infrastructure.repository.DocumentRepository; import com.jiaruiblog.infrastructure.repository.TagRepository; +import com.jiaruiblog.infrastructure.repository.mysql.TagDocRelationshipMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.compress.utils.Lists; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -58,6 +60,9 @@ public class CategoryServiceImpl implements CategoryService { @Resource CollectRepository collectRepository; + @Resource + TagDocRelationshipMapper tagDocRelationshipMapper; + /** * 新增分类记录 * 注意:需要处理并发插入的事务问题 @@ -71,6 +76,7 @@ public void insert(Category category) { if (!isNameExist(category.getName()).isEmpty()) { throw BusinessExceptionBuilder.of(ErrorCode.OPERATE_FAILED).build(); } + category.setId(UUID.randomUUID().toString()); // 保存分类信息 categoryRepository.save(category); } @@ -117,6 +123,7 @@ public String saveOrUpdateCate(String cateName) { if (nameExist.isEmpty()) { // 创建新分类 Category category = new Category(); + category.setId(UUID.randomUUID().toString()); category.setUpdateDate(new Date()); category.setCreateDate(new Date()); category.setName(cateName); @@ -197,6 +204,7 @@ public void addRelationShip(CateDocRelationship relationship) { || !StringUtils.hasText(relationship.getFileId())) { throw BusinessExceptionBuilder.of(ErrorCode.INVALID_PARAM).detail("分类关系不能为空").build(); } + relationship.setId(UUID.randomUUID().toString()); categoryRepository.saveRelationship(relationship); log.info("分类关联创建成功:categoryId={}, fileId={}", relationship.getCategoryId(), relationship.getFileId()); } @@ -317,6 +325,7 @@ public void addRelationShipDefault(String categoryId, String docId) { return; } CateDocRelationship relationship = new CateDocRelationship(); + relationship.setId(UUID.randomUUID().toString()); relationship.setCategoryId(categoryId); relationship.setFileId(docId); relationship.setCreateDate(new Date()); @@ -384,10 +393,10 @@ public PageVO getDocByTagAndCate(String cateId, String tagId, S // Step 1: Get doc IDs based on tag and category filters if (StringUtils.hasText(tagId)) { List tagRelationships = tagRepository.findRelationshipsByTagId(tagId); - docIds = tagRelationships.stream().map(TagDocRelationship::getFileId).collect(Collectors.toList()); + docIds = tagRelationships == null ? new ArrayList<>() : tagRelationships.stream().map(TagDocRelationship::getFileId).collect(Collectors.toList()); } else if (StringUtils.hasText(cateId)) { List cateRelationships = categoryRepository.findRelationshipsByCategoryId(cateId, Sort.unsorted()); - docIds = cateRelationships.stream().map(CateDocRelationship::getFileId).collect(Collectors.toList()); + docIds = cateRelationships == null ? new ArrayList<>() : cateRelationships.stream().map(CateDocRelationship::getFileId).collect(Collectors.toList()); } else { docIds = new ArrayList<>(); } @@ -479,6 +488,28 @@ public PageVO getMyCollection(String cateId, String tagId, .filter(doc -> collectedDocIdSet.contains(doc.getId())) .collect(Collectors.toList()); + // Step 3.5: Filter by category and tag if provided + if (StringUtils.hasText(cateId)) { + List docIds = filteredDocs.stream().map(FileDocument::getId).collect(Collectors.toList()); + List cateRelationships = categoryRepository.findByCategoryIdAndDocIdIn(cateId, docIds); + Set fileIdsInCategory = cateRelationships.stream() + .map(CateDocRelationship::getFileId) + .collect(Collectors.toSet()); + filteredDocs = filteredDocs.stream() + .filter(doc -> fileIdsInCategory.contains(doc.getId())) + .collect(Collectors.toList()); + } + if (StringUtils.hasText(tagId)) { + List docIds = filteredDocs.stream().map(FileDocument::getId).collect(Collectors.toList()); + List tagRelationships = tagDocRelationshipMapper.findByTagIdAndFileIds(tagId, docIds); + Set fileIdsWithTag = tagRelationships.stream() + .map(TagDocRelationship::getFileId) + .collect(Collectors.toSet()); + filteredDocs = filteredDocs.stream() + .filter(doc -> fileIdsWithTag.contains(doc.getId())) + .collect(Collectors.toList()); + } + // Step 4: Convert to DTO List mappedResults = filteredDocs.stream().map(doc -> { FileDocumentDTO dto = new FileDocumentDTO(); @@ -517,6 +548,28 @@ public PageVO getMyUploaded(String cateId, String tagId, String Sort.by(Sort.Direction.DESC, "uploadDate")); } + // Filter by category and tag if provided + if (StringUtils.hasText(cateId)) { + List docIds = documents.stream().map(FileDocument::getId).collect(Collectors.toList()); + List cateRelationships = categoryRepository.findByCategoryIdAndDocIdIn(cateId, docIds); + Set fileIdsInCategory = cateRelationships.stream() + .map(CateDocRelationship::getFileId) + .collect(Collectors.toSet()); + documents = documents.stream() + .filter(doc -> fileIdsInCategory.contains(doc.getId())) + .collect(Collectors.toList()); + } + if (StringUtils.hasText(tagId)) { + List docIds = documents.stream().map(FileDocument::getId).collect(Collectors.toList()); + List tagRelationships = tagDocRelationshipMapper.findByTagIdAndFileIds(tagId, docIds); + Set fileIdsWithTag = tagRelationships.stream() + .map(TagDocRelationship::getFileId) + .collect(Collectors.toSet()); + documents = documents.stream() + .filter(doc -> fileIdsWithTag.contains(doc.getId())) + .collect(Collectors.toList()); + } + // Convert to DTO List mappedResults = documents.stream().map(doc -> { FileDocumentDTO dto = new FileDocumentDTO(); diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CollectServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CollectServiceImpl.java index 79854d7..7503b78 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CollectServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CollectServiceImpl.java @@ -9,7 +9,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Date; import java.util.List; +import java.util.UUID; /** * 收藏服务实现类 @@ -30,7 +32,7 @@ public class CollectServiceImpl implements CollectService { public void insert(CollectDocRelationship collect) { Boolean aBoolean = insertRelationShip(collect); if (Boolean.FALSE.equals(aBoolean)) { - throw BusinessExceptionBuilder.of(ErrorCode.OPERATE_FAILED).build(); + throw BusinessExceptionBuilder.of(ErrorCode.DOCUMENT_ALREADY_COLLECTED).build(); } } @@ -40,6 +42,9 @@ public Boolean insertRelationShip(CollectDocRelationship collect) { if (collectDb != null) { return false; } + collect.setId(UUID.randomUUID().toString()); + collect.setCreateDate(new Date()); + collect.setUpdateDate(new Date()); collectRepository.save(collect); return true; } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java index ee62673..ebcb4f2 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java @@ -1,12 +1,15 @@ package com.jiaruiblog.application.service.impl; +import cn.hutool.core.util.IdUtil; import com.jiaruiblog.application.service.ICommentService; import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.dto.CommentListDTO; import com.jiaruiblog.domain.entity.vo.CommentWithUserVO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.infrastructure.repository.CommentRepository; +import com.jiaruiblog.infrastructure.repository.DocumentRepository; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; @@ -31,12 +34,24 @@ public class CommentServiceImpl implements ICommentService { @Resource CommentRepository commentRepository; + @Resource + DocumentRepository documentRepository; + @Override public void insert(Comment comment) { if (comment == null || !StringUtils.hasText(comment.getUserId()) || !StringUtils.hasText(comment.getUserName())) { return; } + // Fetch document name and store it for resilience when document is deleted + if (StringUtils.hasText(comment.getDocId())) { + FileDocument doc = documentRepository.findById(comment.getDocId()); + if (doc != null) { + comment.setDocName(doc.getName()); + } + } // Note: Sensitive filtering should be done at the API layer before calling this method + comment.setId(IdUtil.fastUUID()); + comment.setCreateUser(comment.getUserId()); comment.setCreateDate(new Date()); comment.setUpdateDate(new Date()); commentRepository.save(comment); @@ -122,24 +137,30 @@ public long countAllFile() { } @Override - public PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin) { + public PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin) { log.info("查询的参数是:{}, {}", page, userId); - // Note: For admin, return all comments; for user, return only their own - List comments = commentRepository.findByUserId(userId); - List commentWithUserVOList = new ArrayList<>(); - - for (Comment comment : comments) { - CommentWithUserVO vo = new CommentWithUserVO(); - BeanUtils.copyProperties(comment, vo); - commentWithUserVOList.add(vo); + List comments; + if (Boolean.TRUE.equals(isAdmin)) { + comments = commentRepository.findAll(); + } else { + comments = commentRepository.findByUserId(userId); } - long count = commentRepository.count(); - return PageVO.builder() + long count = comments.size(); + + int pageNum = page.getPage(); + int pageSize = page.getRows(); + int skip = (pageNum - 1) * pageSize; + comments = comments.stream() + .skip(skip) + .limit(pageSize) + .toList(); + + return PageVO.builder() .total((int) count) - .list(commentWithUserVOList) - .pageNum(page.getPage()) - .pageSize(page.getRows()) + .list(comments) + .pageNum(pageNum) + .pageSize(pageSize) .build(); } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocLogServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocLogServiceImpl.java index 358066b..54ac618 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocLogServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocLogServiceImpl.java @@ -1,6 +1,8 @@ package com.jiaruiblog.application.service.impl; import com.jiaruiblog.application.service.IDocLogService; +import com.jiaruiblog.common.exception.BusinessException; +import com.jiaruiblog.common.exception.ErrorCode; import com.jiaruiblog.domain.entity.po.DocLog; import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.po.User; @@ -15,6 +17,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * @author luojiarui @@ -30,16 +33,6 @@ public enum Action { @Resource private DocLogRepository docLogRepository; - @Override - public void insert(DocLog docLog) { - if (docLog == null) { - return; - } - docLog.setCreateDate(new Date()); - docLog.setUpdateDate(new Date()); - docLogRepository.save(docLog); - } - @Override public void remove(DocLog docLog) { if (docLog == null || docLog.getId() == null) { @@ -94,6 +87,7 @@ public String addLog(User user, FileDocument document, DocLogServiceImpl.Action return null; } DocLog docLog = new DocLog(); + docLog.setId(UUID.randomUUID().toString()); docLog.setUserId(user.getId()); docLog.setUserName(user.getUsername()); docLog.setDocId(document.getId()); @@ -101,13 +95,27 @@ public String addLog(User user, FileDocument document, DocLogServiceImpl.Action docLog.setAction(action.name()); docLog.setCreateDate(new Date()); docLog.setUpdateDate(new Date()); - return docLogRepository.save(docLog).getId(); + int save = docLogRepository.save(docLog); + if (save < 1) { + throw new BusinessException(ErrorCode.INTERNAL_ERROR); + } + return docLog.getId(); } @Override public Map queryDocLogs(BasePageDTO page) { - List docLogList = docLogRepository.findByUserId(""); // Placeholder - long count = docLogRepository.count(); + List docLogList = docLogRepository.findAll(); + long count = docLogList.size(); + + // Pagination: page is 1-indexed, convert to 0-indexed for skip + int pageNum = page.getPage(); + int pageSize = page.getRows(); + int skip = (pageNum - 1) * pageSize; + docLogList = docLogList.stream() + .skip(skip) + .limit(pageSize) + .toList(); + Map result = new HashMap<>(); result.put("total", count); result.put("data", docLogList); diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocReviewServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocReviewServiceImpl.java index 1e8a189..19067f6 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocReviewServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocReviewServiceImpl.java @@ -3,9 +3,9 @@ import com.jiaruiblog.application.service.DocReviewService; import com.jiaruiblog.application.service.TaskExecuteService; import com.jiaruiblog.common.enums.DocStateEnum; +import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.po.DocReview; import com.jiaruiblog.domain.entity.po.FileDocument; -import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.infrastructure.repository.DocReviewRepository; import com.jiaruiblog.infrastructure.repository.DocumentRepository; @@ -14,9 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.util.*; /** * @author luojiarui @@ -40,8 +38,11 @@ public void insert(FileDocument document) { return; } DocReview review = new DocReview(); + review.setId(UUID.randomUUID().toString()); review.setDocId(document.getId()); + review.setDocName(document.getName()); review.setUserId(document.getUserId()); + review.setUserName(document.getUserName()); review.setCreateDate(new Date()); docReviewRepository.save(review); log.info("文档审核记录创建:docId={}", document.getId()); @@ -166,7 +167,18 @@ public PageVO queryReviewLog(BasePageDTO page, String userId, Boolean } } - List reviews = docReviewRepository.findByPage(pageNum - 1, pageSize, userId, isAdmin != null && isAdmin); + boolean adminFlag = isAdmin != null && isAdmin; + long total = docReviewRepository.countByUserId(userId, adminFlag); + int totalPages = (int) Math.ceil((double) total / pageSize); + if (pageNum > totalPages && total > 0) { + pageVO.setList(Collections.emptyList()); + pageVO.setPageNum(pageNum); + pageVO.setPageSize(pageSize); + pageVO.setTotal(total); + return pageVO; + } + + List reviews = docReviewRepository.findByPage(pageNum, pageSize, userId, adminFlag); // 过滤掉不属于该用户的记录 List filteredList = new ArrayList<>(); @@ -183,7 +195,7 @@ public PageVO queryReviewLog(BasePageDTO page, String userId, Boolean pageVO.setList(filteredList); pageVO.setPageNum(pageNum); pageVO.setPageSize(pageSize); - pageVO.setTotal(docReviewRepository.countByUserId(userId, isAdmin != null && isAdmin)); + pageVO.setTotal(total); return pageVO; } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java index 93117b7..ee87100 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java @@ -1,29 +1,37 @@ package com.jiaruiblog.application.service.impl; import cn.hutool.core.util.IdUtil; -import com.jiaruiblog.application.service.DocumentService; -import com.jiaruiblog.application.service.ElasticService; -import com.jiaruiblog.application.service.CollectService; -import com.jiaruiblog.application.service.ICommentService; +import com.jiaruiblog.application.service.*; +import com.jiaruiblog.application.service.IDocLogService; import com.jiaruiblog.common.constants.StorageConstants; import com.jiaruiblog.common.enums.DocStateEnum; -import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.common.enums.FilterTypeEnum; import com.jiaruiblog.domain.entity.dto.BasePageDTO; import com.jiaruiblog.domain.entity.dto.DocumentDTO; +import com.jiaruiblog.domain.entity.dto.SearchQuery; +import com.jiaruiblog.domain.entity.dto.SearchResultItem; import com.jiaruiblog.domain.entity.dto.document.UpdateInfoDTO; -import com.jiaruiblog.domain.entity.vo.DocWithCateVO; -import com.jiaruiblog.domain.entity.vo.DocumentVO; -import com.jiaruiblog.domain.entity.vo.PageVO; -import com.jiaruiblog.infrastructure.repository.DocumentRepository; +import com.jiaruiblog.domain.entity.po.*; +import com.jiaruiblog.domain.entity.vo.*; +import com.jiaruiblog.infrastructure.repository.CategoryRepository; +import com.jiaruiblog.infrastructure.repository.CollectRepository; +import com.jiaruiblog.infrastructure.repository.TagRepository; +import com.jiaruiblog.infrastructure.repository.mysql.CateDocRelationshipMapper; +import com.jiaruiblog.infrastructure.repository.mysql.DocumentMybatisRepository; +import com.jiaruiblog.infrastructure.repository.mysql.TagDocRelationshipMapper; import com.jiaruiblog.infrastructure.storage.StorageFactory; import com.jiaruiblog.infrastructure.storage.StorageStrategy; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; @@ -35,7 +43,7 @@ public class DocumentServiceImpl implements DocumentService { @Resource - private DocumentRepository documentRepository; + private DocumentMybatisRepository documentMybatisRepository; @Resource private StorageFactory storageFactory; @@ -49,7 +57,32 @@ public class DocumentServiceImpl implements DocumentService { @Resource private ElasticService elasticService; - private static final String FILE_NAME = "filename"; + @Resource + private TaskExecuteService taskExecuteService; + + @Resource + private DocReviewService docReviewService; + + @Resource + private CateDocRelationshipMapper cateDocRelationshipMapper; + + @Resource + private TagDocRelationshipMapper tagDocRelationshipMapper; + + @Resource + private TagRepository tagRepository; + + @Resource + private CategoryRepository categoryRepository; + + @Resource + private CollectRepository collectRepository; + + @Resource + private LikeService likeService; + + @Resource + private IDocLogService docLogService; @Override public String uploadFileToGridFs(String fileName, InputStream inputStream, String contentType, String md5) { @@ -57,17 +90,17 @@ public String uploadFileToGridFs(String fileName, InputStream inputStream, Strin throw new IllegalArgumentException("InputStream cannot be null"); } StorageStrategy storageStrategy = storageFactory.getStorageStrategy(); - // 使用UUID作为唯一key,路径前缀为 documents/ - String uniqueKey = IdUtil.simpleUUID(); - String objectKey = StorageConstants.documentPath(uniqueKey); - storageStrategy.upload(inputStream, objectKey, contentType); - log.info("Uploaded file to MinIO: objectKey={}, filename={}", objectKey, fileName); - return uniqueKey; // 返回唯一key,用于存储到MySQL的gridfsId字段 + // 使用 md5 + originalFilename 作为 objectKey + String objectKey = md5 + "_" + fileName; + String fullPath = StorageConstants.documentPath(objectKey); + storageStrategy.upload(inputStream, fullPath, contentType); + log.info("Uploaded file to MinIO: objectKey={}, filename={}", fullPath, fileName); + return objectKey; // 返回 objectKey,用于存储到MySQL的gridfsId字段 } @Override public List list() { - return documentRepository.findByPage(1, 100, Sort.by(Sort.Direction.DESC, "uploadDate")); + return documentMybatisRepository.findByPage(1, 100, Sort.by(Sort.Direction.DESC, "uploadDate")); } @Override @@ -75,7 +108,7 @@ public void insert(FileDocument document) { if (document == null) { return; } - documentRepository.save(document); + documentMybatisRepository.save(document); } @Override @@ -98,7 +131,7 @@ public void remove(FileDocument document) { return; } // Delete from MySQL - documentRepository.delete(document.getId()); + documentMybatisRepository.delete(document.getId()); // Delete from MinIO - 文档原文 if (document.getGridfsId() != null) { storageFactory.getStorageStrategy().delete(StorageConstants.documentPath(document.getGridfsId())); @@ -113,11 +146,11 @@ public void remove(FileDocument document) { } // Delete from MinIO - 文本文件 if (document.getTextFileId() != null) { - storageFactory.getStorageStrategy().delete(StorageConstants.textPath(document.getTextFileId())); + storageFactory.getStorageStrategy().delete(StorageConstants.documentTextPath(document.getTextFileId())); } // Delete from ES - if (document.getMd5() != null) { - elasticService.deleteById(document.getMd5()); + if (document.getId() != null) { + elasticService.deleteById(document.getId()); } log.info("Removed document: id={}", document.getId()); } @@ -136,7 +169,7 @@ public FileDocument queryById(String documentId) { if (documentId == null || documentId.isEmpty()) { return null; } - return documentRepository.findById(documentId); + return documentMybatisRepository.findById(documentId); } @Override @@ -144,7 +177,7 @@ public FileDocument queryByMd5(String md5) { if (md5 == null || md5.isEmpty()) { return null; } - return documentRepository.findByMd5(md5); + return documentMybatisRepository.findByMd5(md5); } @Override @@ -160,12 +193,12 @@ public List queryByDocIdList(List docIds) { if (docIds == null || docIds.isEmpty()) { return Collections.emptyList(); } - return documentRepository.findByIdList(docIds); + return documentMybatisRepository.findByIdList(docIds); } @Override public List queryAll() { - return documentRepository.findByPage(1, Integer.MAX_VALUE, Sort.by(Sort.Direction.DESC, "uploadDate")); + return documentMybatisRepository.findByPage(1, Integer.MAX_VALUE, Sort.by(Sort.Direction.DESC, "uploadDate")); } @Override @@ -216,14 +249,14 @@ public FileDocument insertReturnEntity(FileDocument document) { if (document == null) { return null; } - documentRepository.save(document); + documentMybatisRepository.save(document); return document; } @Override public PageVO queryByPage(FileDocument document, int pageNum, int pageSize) { - List documents = documentRepository.findByPage(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "uploadDate")); - long total = documentRepository.count(); + List documents = documentMybatisRepository.findByPage(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "uploadDate")); + long total = documentMybatisRepository.count(); return PageVO.builder() .pageNum(pageNum) .pageSize(pageSize) @@ -237,7 +270,7 @@ public FileDocument saveFile(String md5, MultipartFile file) { if (md5 == null || file == null) { return null; } - FileDocument existing = documentRepository.findByMd5(md5); + FileDocument existing = documentMybatisRepository.findByMd5(md5); if (existing != null) { return existing; } @@ -250,14 +283,98 @@ public FileDocument saveFile(String md5, MultipartFile file) { if (file.getOriginalFilename() != null && file.getOriginalFilename().contains(".")) { document.setSuffix(file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))); } - documentRepository.save(document); + documentMybatisRepository.save(document); return document; } @Override - public void documentUpload(MultipartFile file, String userId, String username) { - // Implementation for document upload - log.info("Document upload: userId={}, username={}", userId, username); + public FileDocument documentUpload(MultipartFile file, String userId, String username) { + if (file == null || file.isEmpty()) { + log.warn("Document upload failed: file is empty"); + return null; + } + + try { + // 1. Read file bytes once for both MD5 calculation and upload + byte[] fileBytes = file.getBytes(); + + // 2. Calculate MD5 + String md5 = calculateMd5(fileBytes); + + // 3. Check for duplicate + FileDocument existing = documentMybatisRepository.findByMd5(md5); + if (existing != null) { + log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); + return existing; + } + + // 4. Upload to MinIO + String uniqueKey = uploadFileToGridFs(file.getOriginalFilename(), new ByteArrayInputStream(fileBytes), + file.getContentType(), md5); + + // 5. Create and save FileDocument + FileDocument document = new FileDocument(); + document.setId(IdUtil.simpleUUID()); + document.setName(file.getOriginalFilename()); + document.setSize(file.getSize()); + document.setMd5(md5); + document.setContentType(file.getContentType()); + document.setSuffix(getFileSuffix(file.getOriginalFilename())); + document.setUploadDate(new Date()); + document.setGridfsId(uniqueKey); + document.setUserId(userId); + document.setUserName(username); + document.setDocState(DocStateEnum.WAIT); + document.setReviewing(true); + document.setCreateDate(new Date()); + documentMybatisRepository.save(document); + + // 6. Index document to ES + indexDocumentToEs(document); + + // 7. Create review record + docReviewService.insert(document); + + // 8. Submit async task for text extraction and ES indexing + taskExecuteService.execute(document); + + log.info("Document upload success: userId={}, username={}, docId={}, filename={}", + userId, username, document.getId(), file.getOriginalFilename()); + + // 添加上传日志 + User user = new User(); + user.setId(userId); + user.setUsername(username); + docLogService.addLog(user, document, DocLogServiceImpl.Action.UPLOAD); + + return document; + } catch (IOException e) { + log.error("Document upload failed: userId={}, username={}, error={}", + userId, username, e.getMessage()); + return null; + } + } + + private String calculateMd5(byte[] fileBytes) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (Exception e) { + throw new RuntimeException("MD5 calculation failed", e); + } + byte[] digest = md.digest(fileBytes); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + private String getFileSuffix(String fileName) { + if (fileName != null && fileName.contains(".")) { + return fileName.substring(fileName.lastIndexOf(".")); + } + return ""; } @Override @@ -267,7 +384,75 @@ public String uploadBatch(String category, List tags, String description @Override public void uploadByUrl(String category, List tags, String name, String description, String url, String userId, String username) { - log.info("Upload by URL: {}", url); + if (url == null || url.isEmpty()) { + log.warn("Upload by URL failed: url is empty"); + return; + } + + try { + // 1. Download file from URL + byte[] fileBytes = cn.hutool.http.HttpUtil.createGet(url).timeout(30000).execute().bodyBytes(); + if (fileBytes == null || fileBytes.length == 0) { + log.warn("Upload by URL failed: downloaded content is empty, url={}", url); + return; + } + + // 2. Calculate MD5 + String md5 = calculateMd5(fileBytes); + + // 3. Check for duplicate + FileDocument existing = documentMybatisRepository.findByMd5(md5); + if (existing != null) { + log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); + return; + } + + // 4. Determine content type and suffix from name or URL + String contentType = cn.hutool.core.io.FileUtil.getMimeType(name != null ? name : url); + String suffix = getFileSuffix(name != null ? name : url); + + // 5. Upload to MinIO + String uniqueKey = uploadFileToGridFs(name, new ByteArrayInputStream(fileBytes), contentType, md5); + + // 6. Create and save FileDocument + FileDocument document = new FileDocument(); + document.setId(IdUtil.simpleUUID()); + document.setName(name); + document.setSize(fileBytes.length); + document.setMd5(md5); + document.setContentType(contentType); + document.setSuffix(suffix); + document.setDescription(description); + document.setUploadDate(new Date()); + document.setGridfsId(uniqueKey); + document.setUserId(userId); + document.setUserName(username); + document.setDocState(DocStateEnum.WAIT); + document.setReviewing(true); + document.setCreateDate(new Date()); + documentMybatisRepository.save(document); + + // 7. Index document to ES + indexDocumentToEs(document); + + // 8. Create review record + docReviewService.insert(document); + + // 9. Submit async task for text extraction and ES indexing + taskExecuteService.execute(document); + + log.info("Upload by URL success: userId={}, username={}, docId={}, filename={}, url={}", + userId, username, document.getId(), name, url); + + // 添加上传日志 + User user = new User(); + user.setId(userId); + user.setUsername(username); + docLogService.addLog(user, document, DocLogServiceImpl.Action.UPLOAD); + } catch (Exception e) { + log.error("Upload by URL failed: userId={}, username={}, url={}, error={}", + userId, username, url, e.getMessage()); + } } @Override @@ -277,7 +462,7 @@ public FileDocument saveFile(FileDocument fileDocument, InputStream inputStream) } String objectId = uploadFileToGridFs(fileDocument.getName(), inputStream, fileDocument.getContentType(), fileDocument.getMd5()); fileDocument.setGridfsId(objectId); - documentRepository.save(fileDocument); + documentMybatisRepository.save(fileDocument); return fileDocument; } @@ -286,7 +471,7 @@ public void updateFile(FileDocument fileDocument) { if (fileDocument == null || fileDocument.getId() == null) { return; } - documentRepository.update(fileDocument); + documentMybatisRepository.update(fileDocument); } @Override @@ -298,7 +483,7 @@ public void updateState(FileDocument fileDocument, DocStateEnum state, String er if (errorMsg != null) { fileDocument.setErrorMsg(errorMsg); } - documentRepository.update(fileDocument); + documentMybatisRepository.update(fileDocument); } @Override @@ -306,9 +491,9 @@ public void removeFile(String id, boolean isDeleteFile) { if (id == null || id.isEmpty()) { return; } - FileDocument document = documentRepository.findById(id); + FileDocument document = documentMybatisRepository.findById(id); if (document != null) { - documentRepository.delete(id); + documentMybatisRepository.delete(id); if (isDeleteFile) { // 删除文档原文 if (document.getGridfsId() != null) { @@ -324,7 +509,7 @@ public void removeFile(String id, boolean isDeleteFile) { } // 删除文本文件 if (document.getTextFileId() != null) { - storageFactory.getStorageStrategy().delete(StorageConstants.textPath(document.getTextFileId())); + storageFactory.getStorageStrategy().delete(StorageConstants.documentTextPath(document.getTextFileId())); } } } @@ -335,7 +520,7 @@ public Optional getById(String id) { if (id == null || id.isEmpty()) { return Optional.empty(); } - return Optional.ofNullable(documentRepository.findById(id)); + return Optional.ofNullable(documentMybatisRepository.findById(id)); } @Override @@ -345,7 +530,7 @@ public Optional getPreviewById(String id) { @Override public FileDocument getByMd5(String md5) { - return documentRepository.findByMd5(md5); + return documentMybatisRepository.findByMd5(md5); } @Override @@ -355,7 +540,7 @@ public List getByMd5Set(Set md5Set) { } List result = new ArrayList<>(); for (String md5 : md5Set) { - FileDocument doc = documentRepository.findByMd5(md5); + FileDocument doc = documentMybatisRepository.findByMd5(md5); if (doc != null) { result.add(doc); } @@ -365,7 +550,7 @@ public List getByMd5Set(Set md5Set) { @Override public List listFilesByPage(int pageIndex, int pageSize) { - return documentRepository.findByPage(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "uploadDate")); + return documentMybatisRepository.findByPage(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "uploadDate")); } @Override @@ -373,7 +558,7 @@ public List listAndFilterByPage(int pageIndex, int pageSize, Colle if (ids == null || ids.isEmpty()) { return Collections.emptyList(); } - return documentRepository.findByIdList(new ArrayList<>(ids)); + return documentMybatisRepository.findByIdList(new ArrayList<>(ids)); } @Override @@ -386,14 +571,99 @@ public PageVO list(DocumentDTO documentDTO) { if (documentDTO == null) { return PageVO.builder().build(); } - List documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); + + String filterWord = documentDTO.getFilterWord(); + + // filterWord 不为空时走 ES 全文检索 + if (StringUtils.hasText(filterWord)) { + return searchFullText(documentDTO); + } + + // 否则走现有 MySQL 逻辑(保留现有代码) + List documents; + long total; + + FilterTypeEnum type = documentDTO.getType(); + if (type == FilterTypeEnum.TAG && StringUtils.hasText(documentDTO.getTagId())) { + documents = documentMybatisRepository.findByPageByTag(documentDTO.getTagId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentMybatisRepository.countByTagId(documentDTO.getTagId()); + } else if (type == FilterTypeEnum.CATEGORY && StringUtils.hasText(documentDTO.getCategoryId())) { + documents = documentMybatisRepository.findByPageByCategory(documentDTO.getCategoryId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentMybatisRepository.countByCategoryId(documentDTO.getCategoryId()); + } else { + documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); + total = documentMybatisRepository.count(); + } + List voList = documents.stream() .map(doc -> convertDocument(new DocumentVO(), doc)) - .collect(Collectors.toList()); + .toList(); return PageVO.builder() .pageNum(documentDTO.getPage()) .pageSize(documentDTO.getRows()) - .total(documentRepository.count()) + .total(total) + .list(voList) + .build(); + } + + /** + * ES 全文检索模式 + */ + private PageVO searchFullText(DocumentDTO documentDTO) { + String filterWord = documentDTO.getFilterWord(); + String tagId = documentDTO.getTagId(); + String categoryId = documentDTO.getCategoryId(); + int page = documentDTO.getPage(); + int rows = documentDTO.getRows(); + + // 1. ES 检索 + SearchResultVO searchResult = elasticService.searchDocumentsFullText( + filterWord, tagId, categoryId, page, rows); + + if (searchResult == null || searchResult.getItems() == null || searchResult.getItems().isEmpty()) { + return PageVO.builder() + .pageNum(page) + .pageSize(rows) + .total(0) + .list(new ArrayList<>()) + .build(); + } + + // 2. 获取文档 ID 列表 + List docIds = searchResult.getItems().stream() + .map(SearchResultItem::getId) + .toList(); + + // 3. 批量查询 MySQL 获取文档详情 + List documents = documentMybatisRepository.findByIdList(new ArrayList<>(docIds)); + + // 4. 构建 ID -> 高亮片段的映射 + Map> highlightMap = searchResult.getItems().stream() + .filter(item -> item.getId() != null) + .collect(Collectors.toMap( + SearchResultItem::getId, + item -> item.getHighlightFragments() != null ? item.getHighlightFragments() : Collections.emptyList(), + (existing, replacement) -> replacement + )); + + // 5. 转换为 DocumentVO,高亮片段存入 description + List voList = documents.stream() + .map(doc -> { + DocumentVO vo = convertDocument(new DocumentVO(), doc); + List highlights = highlightMap.get(doc.getId()); + if (highlights != null && !highlights.isEmpty()) { + vo.setDescription(String.join("\n---\n", highlights)); + } + return vo; + }) + .toList(); + + return PageVO.builder() + .pageNum(page) + .pageSize(rows) + .total(searchResult.getTotal()) .list(voList) .build(); } @@ -417,7 +687,7 @@ public void updateInfo(UpdateInfoDTO updateInfoDTO) { if (updateInfoDTO == null || updateInfoDTO.getId() == null) { return; } - FileDocument document = documentRepository.findById(updateInfoDTO.getId()); + FileDocument document = documentMybatisRepository.findById(updateInfoDTO.getId()); if (document != null) { if (updateInfoDTO.getName() != null) { document.setName(updateInfoDTO.getName()); @@ -425,13 +695,90 @@ public void updateInfo(UpdateInfoDTO updateInfoDTO) { if (updateInfoDTO.getDesc() != null) { document.setDescription(updateInfoDTO.getDesc()); } - documentRepository.update(document); + documentMybatisRepository.update(document); } } @Override public PageVO listWithCategory(DocumentDTO documentDTO) { - return PageVO.builder().build(); + if (documentDTO == null) { + return PageVO.builder().build(); + } + + String filterWord = documentDTO.getFilterWord(); + List documents; + long total; + + // 根据是否有过滤词选择不同的查询方法 + if (StringUtils.hasText(filterWord)) { + documents = documentMybatisRepository.findByPageWithFilter( + documentDTO.getPage(), documentDTO.getRows(), + Sort.by(Sort.Direction.DESC, "uploadDate"), filterWord); + total = documentMybatisRepository.countWithFilter(filterWord); + } else { + documents = documentMybatisRepository.findByPage( + documentDTO.getPage(), documentDTO.getRows(), + Sort.by(Sort.Direction.DESC, "uploadDate")); + total = documentMybatisRepository.count(); + } + + List voList = documents.stream() + .map(doc -> convertToDocWithCateVO(doc, documentDTO.getTagId(), documentDTO.getCategoryId())) + .toList(); + return PageVO.builder() + .pageNum(documentDTO.getPage()) + .pageSize(documentDTO.getRows()) + .total(total) + .list(voList) + .build(); + } + + private DocWithCateVO convertToDocWithCateVO(FileDocument doc, String tagId, String categoryId) { + DocWithCateVO vo = new DocWithCateVO(); + vo.setId(doc.getId()); + vo.setTitle(doc.getName()); + vo.setSize(doc.getSize()); + vo.setUserName(doc.getUserName()); + vo.setCreateTime(doc.getUploadDate()); + vo.setChecked(false); + + // Build category info and check if already belongs to this category + if (StringUtils.hasText(categoryId)) { + CategoryVO categoryVO = new CategoryVO(); + categoryVO.setId(categoryId); + List cateRels = cateDocRelationshipMapper.findByFileId(doc.getId()); + for (CateDocRelationship rel : cateRels) { + if (rel.getCategoryId().equals(categoryId)) { + categoryVO.setRelationShipId(rel.getId()); + vo.setChecked(true); + break; + } + } + vo.setCategoryVO(categoryVO); + } + + // Build tag list info and check if already belongs to this tag + if (StringUtils.hasText(tagId)) { + List tagRels = tagDocRelationshipMapper.findByFileId(doc.getId()); + for (TagDocRelationship rel : tagRels) { + if (rel.getTagId().equals(tagId)) { + vo.setChecked(true); + break; + } + } + List tagVOList = tagRels.stream() + .map(rel -> { + TagVO tagVO = new TagVO(); + tagVO.setId(rel.getTagId()); + return tagVO; + }) + .toList(); + vo.setTagVOList(tagVOList); + } else { + vo.setTagVOList(new ArrayList<>()); + } + + return vo; } @Override @@ -465,7 +812,7 @@ public List queryByDocIds(String... docId) { if (docId == null || docId.length == 0) { return Collections.emptyList(); } - return documentRepository.findByIdList(Arrays.asList(docId)); + return documentMybatisRepository.findByIdList(Arrays.asList(docId)); } @Override @@ -473,8 +820,8 @@ public List queryAndRemove(String... docId) { if (docId == null || docId.length == 0) { return Collections.emptyList(); } - List result = documentRepository.findByIdList(Arrays.asList(docId)); - documentRepository.deleteByIdList(Arrays.asList(docId)); + List result = documentMybatisRepository.findByIdList(Arrays.asList(docId)); + documentMybatisRepository.deleteByIdList(Arrays.asList(docId)); log.info("Query and remove documents: {}", Arrays.asList(docId)); return result; } @@ -484,7 +831,7 @@ public List queryAndUpdate(String... docId) { if (docId == null || docId.length == 0) { return Collections.emptyList(); } - return documentRepository.findByIdList(Arrays.asList(docId)); + return documentMybatisRepository.findByIdList(Arrays.asList(docId)); } @Override @@ -494,20 +841,20 @@ public List queryFileDocument(BasePageDTO pageDTO, boolean reviewi } int page = pageDTO.getPage() != null ? pageDTO.getPage() : 1; int size = pageDTO.getRows() != null ? pageDTO.getRows() : 10; - return documentRepository.findByPage(page, size, Sort.by(Sort.Direction.DESC, "uploadDate")); + return documentMybatisRepository.findByPage(page, size, Sort.by(Sort.Direction.DESC, "uploadDate")); } @Override public Map queryFileDocumentResult(BasePageDTO pageDTO, boolean reviewing) { Map result = new HashMap<>(); result.put("data", queryFileDocument(pageDTO, reviewing)); - result.put("total", documentRepository.count()); + result.put("total", documentMybatisRepository.count()); return result; } @Override public long countAllFile() { - return documentRepository.count(); + return documentMybatisRepository.count(); } @Override @@ -515,7 +862,7 @@ public boolean isExist(String docId) { if (docId == null || docId.isEmpty()) { return false; } - return documentRepository.findById(docId) != null; + return documentMybatisRepository.findById(docId) != null; } @Override @@ -538,10 +885,39 @@ public DocumentVO convertDocument(DocumentVO documentVO, FileDocument fileDocume if (collectService != null) { documentVO.setCollectNum(collectService.collectNum(docId)); } + if (likeService != null) { + documentVO.setLikeNum(likeService.likeNum(docId)); + } documentVO.setDocState(fileDocument.getDocState()); documentVO.setErrorMsg(fileDocument.getErrorMsg()); documentVO.setTxtId(fileDocument.getTextFileId()); documentVO.setPreviewFileId(fileDocument.getPreviewFileId()); + + // Populate categoryVO + List cateRels = cateDocRelationshipMapper.findByFileId(docId); + if (!cateRels.isEmpty()) { + CateDocRelationship rel = cateRels.get(0); + Category category = categoryRepository.findById(rel.getCategoryId()).orElse(null); + if (category != null) { + CategoryVO categoryVO = new CategoryVO(); + categoryVO.setId(category.getId()); + categoryVO.setName(category.getName()); + categoryVO.setRelationShipId(rel.getId()); + documentVO.setCategoryVO(categoryVO); + } + } + + // Populate tagVOList + List tagRels = tagDocRelationshipMapper.findByFileId(docId); + if (!tagRels.isEmpty()) { + List tagVOList = tagRels.stream().map(rel -> { + TagVO tagVO = new TagVO(); + tagVO.setId(rel.getTagId()); + return tagVO; + }).toList(); + documentVO.setTagVOList(tagVOList); + } + return documentVO; } @@ -566,10 +942,10 @@ public PageVO search(String keyword, int pageNum, int pageSize) { .build(); } // Step 2: Query MySQL, filter by reviewing=false AND docState=SUCCESS - java.util.List allMatchedDocs = documentRepository.findByIdList(matchedIds); + java.util.List allMatchedDocs = documentMybatisRepository.findByIdList(matchedIds); java.util.List filteredDocs = allMatchedDocs.stream() - .filter(doc -> !doc.isReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) - .collect(java.util.stream.Collectors.toList()); + .filter(doc -> !doc.getReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) + .toList(); // Step 3: Pagination int total = filteredDocs.size(); int start = (pageNum - 1) * pageSize; @@ -580,7 +956,7 @@ public PageVO search(String keyword, int pageNum, int pageSize) { // Step 4: Convert to VO java.util.List voList = pagedDocs.stream() .map(this::convertToVO) - .collect(java.util.stream.Collectors.toList()); + .toList(); return PageVO.builder() .pageNum(pageNum) .pageSize(pageSize) @@ -600,4 +976,295 @@ private DocumentVO convertToVO(FileDocument doc) { vo.setCreateTime(doc.getUploadDate()); return vo; } + + @Override + public PageVO search(SearchQuery query, String userId) { + // Step 1: ES retrieval - get candidate doc IDs with highlights + List searchResults = elasticService.searchDocumentsWithHighlight(query); + if (searchResults == null || searchResults.isEmpty()) { + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + + // Build highlight map: docId -> highlightFragment (use first fragment from list) + Map highlightMap = new HashMap<>(); + Set candidateIds = new HashSet<>(); + for (SearchResultItem item : searchResults) { + candidateIds.add(item.getId()); + if (item.getHighlightFragments() != null && !item.getHighlightFragments().isEmpty()) { + highlightMap.put(item.getId(), item.getHighlightFragments().get(0)); + } + } + + // Step 2: Tags filtering - intersect with docs that have ALL specified tags + if (query.getTags() != null && !query.getTags().isEmpty()) { + List tags = tagRepository.findByNames(query.getTags()); + if (tags.isEmpty()) { + // No matching tags found, return empty result + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + List tagIds = tags.stream().map(Tag::getId).toList(); + List docIdsWithAllTags = tagDocRelationshipMapper.findByFileIds(new ArrayList<>(candidateIds)) + .stream() + .collect(java.util.stream.Collectors.groupingBy(TagDocRelationship::getFileId)) + .entrySet().stream() + .filter(entry -> { + Set docTagIds = entry.getValue().stream() + .map(TagDocRelationship::getTagId) + .collect(Collectors.toSet()); + return docTagIds.containsAll(tagIds); + }) + .map(Map.Entry::getKey) + .toList(); + candidateIds.retainAll(docIdsWithAllTags); + if (candidateIds.isEmpty()) { + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + } + + // Step 3: Category filtering - intersect with docs in the specified category + if (StringUtils.hasText(query.getCategory())) { + List categories = categoryRepository.findByName(query.getCategory()); + if (categories.isEmpty()) { + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + String categoryId = categories.get(0).getId(); + List docIdsInCategory = cateDocRelationshipMapper.findByFileIds(new ArrayList<>(candidateIds)) + .stream() + .filter(rel -> categoryId.equals(rel.getCategoryId())) + .map(CateDocRelationship::getFileId) + .toList(); + candidateIds.retainAll(docIdsInCategory); + if (candidateIds.isEmpty()) { + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + } + + // Step 4: Query documents from MySQL + List documents = documentMybatisRepository.findByIdList(new ArrayList<>(candidateIds)); + // Filter by reviewing=false AND docState=SUCCESS + List filteredDocs = documents.stream() + .filter(doc -> !doc.getReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) + .collect(Collectors.toList()); + + // Step 5: Sorting + String sortField = query.getSortField(); + String sortOrder = query.getSortOrder(); + Sort.Direction direction = "asc".equalsIgnoreCase(sortOrder) ? Sort.Direction.ASC : Sort.Direction.DESC; + Comparator comparator = switch (sortField != null ? sortField : "createTime") { + case "name" -> Comparator.comparing(FileDocument::getName, Comparator.nullsLast(Comparator.naturalOrder())); + case "size" -> Comparator.comparing(FileDocument::getSize, Comparator.nullsLast(Comparator.naturalOrder())); + default -> Comparator.comparing(FileDocument::getUploadDate, Comparator.nullsLast(Comparator.naturalOrder())); + }; + if (direction == Sort.Direction.DESC) { + comparator = comparator.reversed(); + } + filteredDocs.sort(comparator); + + // Step 6: Pagination + int total = filteredDocs.size(); + int page = query.getPage() != null ? query.getPage() : 1; + int pageSize = query.getPageSize() != null ? query.getPageSize() : 20; + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, total); + List pagedDocs = (start >= total) + ? new ArrayList<>() + : filteredDocs.subList(start, end); + + // Step 7: Assemble results - get liked/collected status and tags + List voList = pagedDocs.stream() + .map(doc -> convertToDocSearchVO(doc, userId, highlightMap)) + .toList(); + + return PageVO.builder() + .pageNum(page) + .pageSize(pageSize) + .total(total) + .list(voList) + .build(); + } + + private DocSearchVO convertToDocSearchVO(FileDocument doc, String userId, Map highlightMap) { + DocSearchVO vo = new DocSearchVO(); + vo.setId(doc.getId()); + vo.setName(doc.getName()); + vo.setType(doc.getSuffix()); + vo.setSize(doc.getSize()); + vo.setSizeDisplay(formatSize(doc.getSize())); + // Use highlight fragment as description if available, otherwise use original description + String highlight = highlightMap.get(doc.getId()); + vo.setDescription(highlight != null ? highlight : doc.getDescription()); + vo.setCreateTime(doc.getUploadDate()); + vo.setUpdateTime(doc.getUpdateDate()); + + // Query liked status + if (StringUtils.hasText(userId)) { + int likeStatus = likeService.findEntityLikeStatus(userId, 1, doc.getId()); + vo.setLiked(likeStatus > 0); + int collectStatus = likeService.findEntityLikeStatus(userId, 2, doc.getId()); + vo.setCollected(collectStatus > 0); + } else { + vo.setLiked(false); + vo.setCollected(false); + } + + // Query tags with color + List tagRels = tagDocRelationshipMapper.findByFileId(doc.getId()); + List tagColorVOList = tagRels.stream() + .map(rel -> { + Tag tag = tagRepository.findById(rel.getTagId()); + TagColorVO tagColorVO = new TagColorVO(); + if (tag != null) { + tagColorVO.setName(tag.getName()); + tagColorVO.setColor(tag.getColor()); + } + return tagColorVO; + }) + .filter(t -> t.getName() != null) + .toList(); + vo.setTags(tagColorVOList); + + // Query category + List cateRels = cateDocRelationshipMapper.findByFileId(doc.getId()); + if (!cateRels.isEmpty()) { + Category category = categoryRepository.findById(cateRels.get(0).getCategoryId()).orElse(null); + if (category != null) { + vo.setCategory(category.getName()); + } + } + + return vo; + } + + private String formatSize(Long size) { + if (size == null) return "0 B"; + if (size < 1024) return size + " B"; + if (size < 1024 * 1024) return String.format("%.1f KB", size / 1024.0); + if (size < 1024 * 1024 * 1024) return String.format("%.1f MB", size / (1024.0 * 1024)); + return String.format("%.1f GB", size / (1024.0 * 1024 * 1024)); + } + + /** + * Index document to Elasticsearch + */ + private void indexDocumentToEs(FileDocument document) { + if (document == null || document.getId() == null) { + log.warn("Cannot index null or id-less document to ES"); + return; + } + try { + SearchDocument searchDocument = new SearchDocument(); + searchDocument.setId(document.getId()); // Use UUID as ES document ID + searchDocument.setName(document.getName()); + searchDocument.setType(document.getSuffix()); + searchDocument.setContent(""); // empty initially, will be filled by text extraction task + searchDocument.setTagNames(getTagNamesByDocId(document.getId())); + searchDocument.setCategoryName(getCategoryNameByDocId(document.getId())); + elasticService.upload(searchDocument); + log.info("Document indexed to ES: id={}, name={}", document.getId(), document.getName()); + } catch (Exception e) { + log.error("Failed to index document to ES: id={}", document.getId(), e); + } + } + + /** + * Get tag names for a document + */ + @Override + public List getTagNamesByDocId(String docId) { + if (docId == null || docId.isEmpty()) { + return Collections.emptyList(); + } + try { + List relationships = tagDocRelationshipMapper.findByFileId(docId); + if (relationships == null || relationships.isEmpty()) { + return Collections.emptyList(); + } + List tagIds = relationships.stream() + .map(TagDocRelationship::getTagId) + .collect(Collectors.toList()); + List tags = tagRepository.findByIds(tagIds); + return tags.stream() + .map(Tag::getName) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("Failed to get tag names for docId={}", docId, e); + return Collections.emptyList(); + } + } + + /** + * Get category name for a document + */ + @Override + public String getCategoryNameByDocId(String docId) { + if (docId == null || docId.isEmpty()) { + return ""; + } + try { + List relationships = cateDocRelationshipMapper.findByFileId(docId); + if (relationships == null || relationships.isEmpty()) { + return ""; + } + // Return the first category's name + String categoryId = relationships.get(0).getCategoryId(); + Optional category = categoryRepository.findById(categoryId); + return category.map(Category::getName).orElse(""); + } catch (Exception e) { + log.error("Failed to get category name for docId={}", docId, e); + return ""; + } + } + + /** + * Update document content in ES after text extraction + */ + private void updateFileContentToEs(String docId, String content) { + if (docId == null || docId.isEmpty()) { + return; + } + FileDocument document = documentMybatisRepository.findById(docId); + if (document == null || document.getId() == null) { + log.warn("Cannot update ES content: document not found for docId={}", docId); + return; + } + try { + SearchDocument searchDocument = new SearchDocument(); + searchDocument.setId(document.getId()); // Use UUID as ES document ID + searchDocument.setName(document.getName()); + searchDocument.setType(document.getSuffix()); + searchDocument.setContent(content); + searchDocument.setTagNames(getTagNamesByDocId(docId)); + searchDocument.setCategoryName(getCategoryNameByDocId(docId)); + elasticService.updateFileObj(null, searchDocument); + log.info("Document content updated in ES: docId={}", docId); + } catch (Exception e) { + log.error("Failed to update document content in ES: docId={}", docId, e); + } + } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ElasticServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ElasticServiceImpl.java index 4e61b15..cc6451c 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ElasticServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ElasticServiceImpl.java @@ -1,10 +1,18 @@ package com.jiaruiblog.application.service.impl; import com.jiaruiblog.application.service.ElasticService; +import com.jiaruiblog.domain.entity.dto.SearchQuery; +import com.jiaruiblog.domain.entity.dto.SearchResultItem; +import com.jiaruiblog.domain.entity.po.Category; import com.jiaruiblog.domain.entity.po.SearchDocument; +import com.jiaruiblog.domain.entity.po.Tag; import com.jiaruiblog.domain.entity.vo.PageVO; +import com.jiaruiblog.domain.entity.vo.SearchResultVO; +import com.jiaruiblog.infrastructure.repository.CategoryRepository; +import com.jiaruiblog.infrastructure.repository.TagRepository; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHits; @@ -12,12 +20,14 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map; import java.util.stream.Collectors; /** @@ -32,6 +42,12 @@ public class ElasticServiceImpl implements ElasticService { @Resource private ElasticsearchOperations elasticsearchOperations; + @Resource + private TagRepository tagRepository; + + @Resource + private CategoryRepository categoryRepository; + @Override public void upload(SearchDocument fileObj) { if (fileObj == null || fileObj.getId() == null) { @@ -216,4 +232,301 @@ public List> getWordStat() { return new ArrayList<>(); } } + + @Override + public List searchDocuments(SearchQuery query) { + try { + String keyword = query.getKeyword(); + Boolean fullText = query.getFullText(); + Boolean segment = query.getSegment(); + String searchType = query.getSearchType(); + + log.debug("searchDocuments - keyword: {}, fullText: {}, segment: {}, searchType: {}", + keyword, fullText, segment, searchType); + + // Determine fields to search based on fullText and searchType + List fieldCriteria = buildFieldCriteria(keyword, fullText, searchType); + + // Build final criteria + Criteria criteria; + if (fieldCriteria.isEmpty()) { + // Empty keyword - return all documents with pagination + criteria = new Criteria("id").exists(); + } else { + // Combine field criteria with OR + criteria = fieldCriteria.get(0); + for (int i = 1; i < fieldCriteria.size(); i++) { + criteria = criteria.or(fieldCriteria.get(i)); + } + } + + // Build query with pagination + int page = query.getPage() != null ? query.getPage() : 1; + int pageSize = query.getPageSize() != null ? query.getPageSize() : 20; + Query esQuery = new CriteriaQuery(criteria) + .setPageable(org.springframework.data.domain.PageRequest.of(page - 1, pageSize)); + + // Execute search + SearchHits searchHits = elasticsearchOperations.search(esQuery, SearchDocument.class); + + List docIds = searchHits.getSearchHits().stream() + .map(SearchHit::getContent) + .map(SearchDocument::getId) + .collect(Collectors.toList()); + + log.info("searchDocuments - found {} documents", docIds.size()); + return docIds; + + } catch (Exception e) { + log.error("searchDocuments failed", e); + return new ArrayList<>(); + } + } + + @Override + public List searchDocumentsWithHighlight(SearchQuery query) { + try { + String keyword = query.getKeyword(); + Boolean fullText = query.getFullText(); + String searchType = query.getSearchType(); + + log.debug("searchDocumentsWithHighlight - keyword: {}, fullText: {}, searchType: {}", + keyword, fullText, searchType); + + // Determine fields to search based on fullText and searchType + List fieldCriteria = buildFieldCriteria(keyword, fullText, searchType); + + // Build final criteria + Criteria criteria; + if (fieldCriteria.isEmpty()) { + criteria = new Criteria("id").exists(); + } else { + criteria = fieldCriteria.get(0); + for (int i = 1; i < fieldCriteria.size(); i++) { + criteria = criteria.or(fieldCriteria.get(i)); + } + } + + // Build query with pagination + int page = query.getPage() != null ? query.getPage() : 1; + int pageSize = query.getPageSize() != null ? query.getPageSize() : 20; + Query esQuery = new CriteriaQuery(criteria) + .setPageable(org.springframework.data.domain.PageRequest.of(page - 1, pageSize)); + + // Execute search + SearchHits searchHits = elasticsearchOperations.search(esQuery, SearchDocument.class); + + // Build result items with manual highlights + List resultItems = new ArrayList<>(); + for (SearchHit hit : searchHits.getSearchHits()) { + String docId = hit.getContent().getId(); + SearchDocument doc = hit.getContent(); + + // Manually generate highlight fragments + List highlightFragments = new ArrayList<>(); + String highlightSource = null; + + // Priority: content > name > tagNames > categoryName + if (doc.getContent() != null && doc.getContent().contains(keyword)) { + String fragment = extractHighlightFragment(doc.getContent(), keyword); + if (fragment != null) { + highlightFragments.add(fragment); + } + highlightSource = "content"; + } else if (doc.getName() != null && doc.getName().contains(keyword)) { + highlightFragments.add(wrapWithHighlight(doc.getName(), keyword)); + highlightSource = "name"; + } else if (doc.getTagNames() != null) { + for (String tag : doc.getTagNames()) { + if (tag != null && tag.contains(keyword)) { + highlightFragments.add(wrapWithHighlight(tag, keyword)); + highlightSource = "tagNames"; + break; + } + } + } else if (doc.getCategoryName() != null && doc.getCategoryName().contains(keyword)) { + highlightFragments.add(wrapWithHighlight(doc.getCategoryName(), keyword)); + highlightSource = "categoryName"; + } + + resultItems.add(SearchResultItem.builder() + .id(docId) + .highlightFragments(highlightFragments.isEmpty() ? null : highlightFragments) + .highlightSource(highlightSource) + .build()); + } + + log.info("searchDocumentsWithHighlight - found {} documents", resultItems.size()); + return resultItems; + + } catch (Exception e) { + log.error("searchDocumentsWithHighlight failed", e); + return new ArrayList<>(); + } + } + + /** + * Extract a fragment of text around the keyword with highlighting + */ + private String extractHighlightFragment(String content, String keyword) { + if (content == null || keyword == null) { + return null; + } + int index = content.indexOf(keyword); + if (index < 0) { + return null; + } + // Get surrounding context + int start = Math.max(0, index - 30); + int end = Math.min(content.length(), index + keyword.length() + 50); + String fragment = content.substring(start, end); + // Add ellipsis if truncated + if (start > 0) { + fragment = "..." + fragment; + } + if (end < content.length()) { + fragment = fragment + "..."; + } + return wrapWithHighlight(fragment, keyword); + } + + /** + * Wrap keyword matches with highlight tags + */ + private String wrapWithHighlight(String text, String keyword) { + if (text == null || keyword == null) { + return text; + } + return text.replace(keyword, "" + keyword + ""); + } + + /** + * Build criteria for searchable fields based on fullText and searchType settings + */ + private List buildFieldCriteria(String keyword, Boolean fullText, String searchType) { + List criteriaList = new ArrayList<>(); + + // If keyword is empty, no field criteria needed + if (keyword == null || keyword.trim().isEmpty()) { + return criteriaList; + } + + if (Boolean.TRUE.equals(fullText)) { + // fullText=true: search all relevant fields based on searchType + if ("all".equalsIgnoreCase(searchType)) { + // Search name + content + tagNames + categoryName + criteriaList.add(new Criteria("name").matches(keyword)); + criteriaList.add(new Criteria("content").matches(keyword)); + criteriaList.add(new Criteria("tagNames").matches(keyword)); + criteriaList.add(new Criteria("categoryName").matches(keyword)); + } else if ("name".equalsIgnoreCase(searchType)) { + // Search only name + criteriaList.add(new Criteria("name").matches(keyword)); + } else if ("description".equalsIgnoreCase(searchType)) { + // Search only content + criteriaList.add(new Criteria("content").matches(keyword)); + } else { + // Default to all fields + criteriaList.add(new Criteria("name").matches(keyword)); + criteriaList.add(new Criteria("content").matches(keyword)); + criteriaList.add(new Criteria("tagNames").matches(keyword)); + criteriaList.add(new Criteria("categoryName").matches(keyword)); + } + } else { + // fullText=false: only search name + criteriaList.add(new Criteria("name").matches(keyword)); + } + + return criteriaList; + } + + /** + * Full-text search with pagination, tag/category filtering, and multiple highlight fragments + * + * @param filterWord the keyword to search for + * @param tagId optional tag ID for filtering + * @param categoryId optional category ID for filtering + * @param page page number (0-based) + * @param rows page size + * @return SearchResultVO containing total count and result items + */ + public SearchResultVO searchDocumentsFullText(String filterWord, String tagId, String categoryId, int page, int rows) { + try { + // 1. Build base criteria - full-text search on name, content, tagNames, categoryName + Criteria criteria = new Criteria("name").matches(filterWord) + .or("content").matches(filterWord) + .or("tagNames").matches(filterWord) + .or("categoryName").matches(filterWord); + + // 2. Handle tagId -> tagNames filtering + if (StringUtils.hasText(tagId)) { + Tag tag = tagRepository.findById(tagId); + if (tag != null) { + criteria = criteria.and("tagNames").contains(tag.getName()); + } + } + + // 3. Handle categoryId -> categoryName filtering + if (StringUtils.hasText(categoryId)) { + Category category = categoryRepository.findById(categoryId).orElse(null); + if (category != null) { + criteria = criteria.and("categoryName").contains(category.getName()); + } + } + + // 4. ES paginated query + Query esQuery = new CriteriaQuery(criteria) + .setPageable(PageRequest.of(page, rows)); + SearchHits searchHits = elasticsearchOperations.search(esQuery, SearchDocument.class); + + // 5. Build results + List items = new ArrayList<>(); + for (SearchHit hit : searchHits.getSearchHits()) { + SearchDocument doc = hit.getContent(); + List fragments = new ArrayList<>(); + + // Extract multiple highlight fragments from content (max 3) + if (doc.getContent() != null) { + int lastIndex = 0; + while (fragments.size() < 3) { + int idx = doc.getContent().toLowerCase().indexOf(filterWord.toLowerCase(), lastIndex); + if (idx < 0) break; + String frag = extractHighlightFragment(doc.getContent(), filterWord, idx); + if (frag != null) fragments.add(frag); + lastIndex = idx + 1; + } + } + + // If no content match, try name + if (fragments.isEmpty() && doc.getName() != null && doc.getName().contains(filterWord)) { + fragments.add(wrapWithHighlight(doc.getName(), filterWord)); + } + + items.add(SearchResultItem.builder() + .id(doc.getId()) + .highlightFragments(fragments.isEmpty() ? null : fragments) + .highlightSource(fragments.isEmpty() ? null : "content") + .build()); + } + + long total = searchHits.getTotalHits(); + return SearchResultVO.builder().total(total).items(items).build(); + + } catch (Exception e) { + log.error("searchDocumentsFullText failed", e); + return SearchResultVO.builder().total(0).items(new ArrayList<>()).build(); + } + } + + /** + * Extract a fragment of text around the keyword at the specified index with highlighting + */ + private String extractHighlightFragment(String content, String keyword, int index) { + int start = Math.max(0, index - 30); + int end = Math.min(content.length(), index + keyword.length() + 50); + String fragment = content.substring(start, end); + if (start > 0) fragment = "..." + fragment; + if (end < content.length()) fragment = fragment + "..."; + return wrapWithHighlight(fragment, keyword); + } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java index ce0b76f..fa485ed 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java @@ -1,20 +1,18 @@ package com.jiaruiblog.application.service.impl; +import com.jiaruiblog.application.service.CollectService; import com.jiaruiblog.application.service.LikeService; import com.jiaruiblog.application.service.RedisService; -import com.jiaruiblog.application.task.like.UserLikeDetail; +import com.jiaruiblog.common.enums.RedisActionEnum; import com.jiaruiblog.domain.entity.po.CollectDocRelationship; import com.jiaruiblog.domain.entity.po.LikeDocRelationship; -import com.jiaruiblog.application.service.CollectService; -import com.jiaruiblog.common.enums.RedisActionEnum; +import com.jiaruiblog.infrastructure.repository.mysql.LikeDocRelationshipMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +import java.util.Date; /** * @author luojiarui @@ -29,22 +27,41 @@ public class LikeServiceImpl implements LikeService { @Resource private RedisService redisService; - // 对实体进行点赞的类型 - // 0: entityType,1表示点赞;2表示收藏信息 - // 1: 用户信息 + @Resource + private LikeDocRelationshipMapper likeDocRelationshipMapper; + public static final String ENTITY_LIKE_KEY_FORMAT = "like:entity:{0}:{1}"; @Override - public void like(String userId, Integer entityType, String entityId) { + public boolean like(String userId, Integer entityType, String entityId) { if (userId == null || entityType == null || entityId == null || entityId.isEmpty()) { - return; + return false; } - try { - String entityLikeKey = MessageFormat.format(ENTITY_LIKE_KEY_FORMAT, entityType, entityId); + String entityLikeKey = MessageFormat.format(ENTITY_LIKE_KEY_FORMAT, entityType, entityId); + + // 查询当前点赞状态 + boolean isLiked = redisService.isSetMember(entityLikeKey, userId); + + LikeDocRelationship like = new LikeDocRelationship(); + like.setUserId(userId); + like.setEntityType(entityType); + like.setEntityId(entityId); + like.setCreateDate(new Date()); + + if (isLiked) { + // 已点赞 → 取消点赞 + redisService.deleteSetMember(entityLikeKey, userId); + redisService.incrementDocScore(entityId, -1); + remove(like); + log.debug("用户 {} 取消点赞实体 {}:{}", userId, entityType, entityId); + return false; + } else { + // 未点赞 → 执行点赞 redisService.setSet(entityLikeKey, userId); - log.debug("用户 {} 点赞了类型为 {} 的实体 {}", userId, entityType, entityId); - } catch (Exception e) { - log.error("点赞失败: userId={}, entityType={}, entityId={}", userId, entityType, entityId, e); + redisService.incrementDocScore(entityId, 1); + insertRelationShip(like); + log.debug("用户 {} 点赞实体 {}:{}", userId, entityType, entityId); + return true; } } @@ -55,7 +72,12 @@ public Long findEntityLikeCount(Integer entityType, String entityId) { } try { String entityLikeKey = MessageFormat.format(ENTITY_LIKE_KEY_FORMAT, entityType, entityId); - return redisService.getSetSize(entityLikeKey); + Long redisCount = redisService.getSetSize(entityLikeKey); + if (redisCount != null && redisCount > 0) { + return redisCount; + } + // Redis 没有从 DB 获取,使用正确的 entityType 查询数量 + return likeDocRelationshipMapper.countByEntityIdAndEntityType(entityId, entityType); } catch (Exception e) { log.error("查询点赞数量失败: entityType={}, entityId={}", entityType, entityId, e); return 0L; @@ -82,12 +104,12 @@ public void insert(LikeDocRelationship like) { return; } try { - // 将LikeDocRelationship转换为CollectDocRelationship并保存 CollectDocRelationship collect = new CollectDocRelationship(); collect.setUserId(like.getUserId()); - collect.setDocId(like.getDocId()); + collect.setDocId(like.getEntityId()); + collect.setRedisActionEnum(RedisActionEnum.getActionByCode(like.getEntityType())); collectService.insert(collect); - log.info("点赞关系保存成功: userId={}, docId={}", like.getUserId(), like.getDocId()); + log.info("点赞关系保存成功: userId={}, docId={}", like.getUserId(), like.getEntityId()); } catch (Exception e) { log.error("保存点赞关系失败", e); } @@ -101,7 +123,8 @@ public Boolean insertRelationShip(LikeDocRelationship like) { try { CollectDocRelationship collect = new CollectDocRelationship(); collect.setUserId(like.getUserId()); - collect.setDocId(like.getDocId()); + collect.setDocId(like.getEntityId()); + collect.setRedisActionEnum(RedisActionEnum.getActionByCode(like.getEntityType())); return collectService.insertRelationShip(collect); } catch (Exception e) { log.error("保存点赞关系失败", e); @@ -117,9 +140,10 @@ public void remove(LikeDocRelationship like) { try { CollectDocRelationship collect = new CollectDocRelationship(); collect.setUserId(like.getUserId()); - collect.setDocId(like.getDocId()); + collect.setDocId(like.getEntityId()); + collect.setRedisActionEnum(RedisActionEnum.getActionByCode(like.getEntityType())); collectService.remove(collect); - log.info("点赞关系删除成功: userId={}, docId={}", like.getUserId(), like.getDocId()); + log.info("点赞关系删除成功: userId={}, docId={}", like.getUserId(), like.getEntityId()); } catch (Exception e) { log.error("删除点赞关系失败", e); } @@ -131,13 +155,12 @@ public Long likeNum(String docId) { return 0L; } try { - // 先尝试从Redis获取 Long redisCount = findEntityLikeCount(RedisActionEnum.LIKE.getCode(), docId); if (redisCount != null && redisCount > 0) { return redisCount; } - // 如果Redis没有,从CollectService获取 - return collectService.collectNum(docId); + // Redis 没有从 DB 获取,使用正确的 entityType 查询点赞数量 + return likeDocRelationshipMapper.countByEntityIdAndEntityType(docId, RedisActionEnum.LIKE.getCode()); } catch (Exception e) { log.error("查询点赞数量失败: docId={}", docId, e); return 0L; @@ -150,156 +173,10 @@ public void removeRelateByDocId(String docId) { return; } try { - // 从CollectService删除 collectService.removeRelateByDocId(docId); - // 从Redis删除相关点赞数据 - String likeKey = MessageFormat.format(ENTITY_LIKE_KEY_FORMAT, RedisActionEnum.LIKE.getCode(), docId); - redisService.deleteKey(likeKey); log.info("删除文档关联的点赞关系: docId={}", docId); } catch (Exception e) { log.error("删除文档点赞关系失败: docId={}", docId, e); } } - - @Override - public void transLikedFromRedis2DB() { - log.info("开始从Redis同步点赞数据到数据库"); - - try { - List likedDataFromRedis = getLikedDataFromRedis(); - if (likedDataFromRedis.isEmpty()) { - log.info("没有需要同步的点赞数据"); - return; - } - - // 过滤出点赞和收藏的数据 - List validData = likedDataFromRedis.stream() - .filter(item -> item.getAction() != null && - (item.getAction().equals(RedisActionEnum.LIKE) || - item.getAction().equals(RedisActionEnum.COLLECT))) - .toList(); - - if (validData.isEmpty()) { - log.info("没有有效的点赞或收藏数据需要同步"); - return; - } - - log.info("开始同步 {} 条点赞/收藏数据到数据库", validData.size()); - - List saveFailedList = new ArrayList<>(); - - // 批量保存点赞和收藏信息 - for (UserLikeDetail userLikeDetail : validData) { - try { - CollectDocRelationship relationship = userLikeDetailSwitch(userLikeDetail); - Boolean success = collectService.insertRelationShip(relationship); - - if (Boolean.FALSE.equals(success)) { - log.warn("保存点赞关系失败: userId={}, entityId={}, action={}", - userLikeDetail.getUserId(), userLikeDetail.getEntityId(), userLikeDetail.getAction()); - saveFailedList.add(userLikeDetail); - } - } catch (Exception e) { - log.error("保存点赞关系时发生异常: userId={}, entityId={}, action={}", - userLikeDetail.getUserId(), userLikeDetail.getEntityId(), userLikeDetail.getAction(), e); - saveFailedList.add(userLikeDetail); - } - } - - // 从redis中清除保存失败的信息 - for (UserLikeDetail userLikeDetail : saveFailedList) { - try { - String key = MessageFormat.format(ENTITY_LIKE_KEY_FORMAT, - userLikeDetail.getAction().getCode(), userLikeDetail.getEntityId()); - redisService.deleteSetMember(key, userLikeDetail.getUserId()); - } catch (Exception e) { - log.error("从Redis移除失败数据时发生异常: userId={}, entityId={}", - userLikeDetail.getUserId(), userLikeDetail.getEntityId(), e); - } - } - - log.info("Redis到数据库的点赞数据同步完成,成功同步 {} 条,失败 {} 条", - validData.size() - saveFailedList.size(), saveFailedList.size()); - - } catch (Exception e) { - log.error("从Redis同步点赞数据到数据库时发生异常", e); - } - } - - /** - * 从redis中获取获取点赞和收藏的数据 - * @return 用户点赞详情列表 - */ - private List getLikedDataFromRedis() { - List result = new ArrayList<>(); - - try { - Set setKeys = redisService.keys("like:entity:*"); - if (setKeys == null || setKeys.isEmpty()) { - log.debug("Redis中没有找到点赞相关的key"); - return result; - } - - log.info("从Redis中获取到 {} 个点赞相关的key", setKeys.size()); - - for (String key : setKeys) { - Set members = redisService.getSet(key); - if (members == null || members.isEmpty()) { - continue; - } - - // 分离出动作类型,实体id - String[] split = key.split(":"); - if (split.length < 4) { - log.warn("Redis key格式不正确: {}", key); - continue; - } - - String actionType = split[2]; - String entityId = split[3]; - RedisActionEnum redisActionEnum = RedisActionEnum.getActionByCode(Integer.valueOf(actionType)); - - if (redisActionEnum == null) { - log.warn("无效的动作类型: {}", actionType); - continue; - } - - // 组装成 UserLikeDetail 对象 - for (String member : members) { - if (member == null || member.isEmpty()) { - continue; - } - - UserLikeDetail userLikeDetail = new UserLikeDetail(); - userLikeDetail.setUserId(member); - userLikeDetail.setEntityId(entityId); - userLikeDetail.setAction(redisActionEnum); - result.add(userLikeDetail); - } - } - - log.info("从Redis中获取到 {} 条点赞数据", result.size()); - return result; - } catch (Exception e) { - log.error("从Redis获取点赞数据失败", e); - return result; - } - } - - /** - * 将UserLikeDetail转换为CollectDocRelationship - * @param userLikeDetail 用户点赞详情 - * @return 收藏文档关系对象 - */ - private CollectDocRelationship userLikeDetailSwitch(UserLikeDetail userLikeDetail) { - if (userLikeDetail == null) { - return null; - } - - CollectDocRelationship relationship = new CollectDocRelationship(); - relationship.setDocId(userLikeDetail.getEntityId()); - relationship.setUserId(userLikeDetail.getUserId()); - relationship.setRedisActionEnum(userLikeDetail.getAction()); - return relationship; - } } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/RedisServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/RedisServiceImpl.java index d9506c2..5d904e3 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/RedisServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/RedisServiceImpl.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.util.List; import java.util.Map; @@ -170,7 +171,7 @@ public long increment(String key, long expireTime) { @Override public List getSearchHistoryByUserId(String userId) { - if (userId == null || userId.isEmpty()) { + if (!StringUtils.hasText(userId)) { return List.of(); } String key = "search:history:" + userId; @@ -190,7 +191,7 @@ public List getHotList(String userId, String key) { @Override public Long delSearchHistoryByUserId(String userId, String searchWord) { - if (userId == null || userId.isEmpty() || searchWord == null || searchWord.isEmpty()) { + if (!StringUtils.hasText(userId) || !StringUtils.hasText(searchWord)) { return 0L; } String key = "search:history:" + userId; @@ -200,7 +201,7 @@ public Long delSearchHistoryByUserId(String userId, String searchWord) { @Override public double score(String key, String docId) { - if (key == null || key.isEmpty() || docId == null || docId.isEmpty()) { + if (!StringUtils.hasText(key) || !StringUtils.hasText(docId)) { return 0.0; } Double score = redisSearchTemplate.opsForZSet().score(key, docId); @@ -209,7 +210,7 @@ public double score(String key, String docId) { @Override public void removeByDocId(String docId) { - if (docId == null || docId.isEmpty()) { + if (!StringUtils.hasText(docId)) { return; } // 删除文档相关的搜索热度和历史记录 @@ -220,22 +221,22 @@ public void removeByDocId(String docId) { @Override public void incrementScoreByUserId(String searchWord, String key) { - if (searchWord == null || searchWord.isEmpty()) { + if (!StringUtils.hasText(searchWord)) { return; } - if (key == null || key.isEmpty()) { + if (!StringUtils.hasText(key)) { key = SEARCH_KEY; } // 增加搜索词的热度分数 redisSearchTemplate.opsForZSet().incrementScore(key, searchWord, 1); // 设置过期时间,避免热词永不消失 redisSearchTemplate.expire(key, java.time.Duration.ofDays(7)); - log.debug("搜索词热度增加:word={}, key={}", searchWord, key); + log.info("搜索词热度增加:word={}, key={}", searchWord, key); } @Override public void addSearchHistoryByUserId(String userId, String searchWord) { - if (userId == null || userId.isEmpty() || searchWord == null || searchWord.isEmpty()) { + if (!StringUtils.hasText(userId) || !StringUtils.hasText(searchWord)) { return; } String key = "search:history:" + userId; @@ -247,6 +248,15 @@ public void addSearchHistoryByUserId(String userId, String searchWord) { redisSearchTemplate.opsForList().trim(key, 0, 9); // 设置过期时间 redisSearchTemplate.expire(key, java.time.Duration.ofDays(30)); - log.debug("添加搜索历史:userId={}, word={}", userId, searchWord); + log.info("添加搜索历史:userId={}, word={}", userId, searchWord); + } + + @Override + public void incrementDocScore(String docId, int delta) { + if (!StringUtils.hasText(docId)) { + return; + } + redisSearchTemplate.opsForZSet().incrementScore(DOC_KEY, docId, delta); + log.info("更新文档热度:docId={}, delta={}", docId, delta); } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/StatisticsServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/StatisticsServiceImpl.java index f27c252..0a8a7b7 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/StatisticsServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/StatisticsServiceImpl.java @@ -2,9 +2,15 @@ import com.jiaruiblog.application.service.StatisticsService; import com.jiaruiblog.domain.entity.dto.StatisticsDTO; +import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.vo.CategoryDistVO; +import com.jiaruiblog.domain.entity.vo.DocTypeDistVO; +import com.jiaruiblog.domain.entity.vo.HotDocVO; import com.jiaruiblog.domain.entity.vo.MonthStatVO; +import com.jiaruiblog.domain.entity.vo.SearchHotWordVO; import com.jiaruiblog.domain.entity.vo.StatsVO; import com.jiaruiblog.domain.entity.vo.TrendVO; +import com.jiaruiblog.domain.entity.vo.UserActivityVO; import com.jiaruiblog.common.enums.RedisActionEnum; import com.jiaruiblog.infrastructure.repository.DocumentRepository; import com.jiaruiblog.infrastructure.repository.UserRepository; @@ -22,6 +28,7 @@ import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -162,9 +169,14 @@ public List trend() { public StatsVO all() { StatsVO vo = new StatsVO(); vo.setDocNum(countDocument()); + vo.setUserNum(userRepository.count()); vo.setCategoryNum(countCategory()); vo.setTagNum(countTag()); vo.setCommentNum(commentRepository.count()); + // 以下暂无数据源,返回 0 + vo.setDownloadNum(0L); + vo.setSearchNum(0L); + vo.setViewNum(0L); return vo; } @@ -182,4 +194,79 @@ public List getMonthStat() { return List.of(); } } + + @Override + public List docTypeDist() { + List> rawList = documentRepository.countByDocType(); + List result = new ArrayList<>(); + for (Map map : rawList) { + DocTypeDistVO vo = new DocTypeDistVO(); + vo.setType((String) map.get("type")); + Object countObj = map.get("count"); + vo.setCount(countObj != null ? ((Number) countObj).longValue() : 0L); + result.add(vo); + } + return result; + } + + @Override + public List categoryDist() { + List> rawList = documentRepository.countByCategory(); + List result = new ArrayList<>(); + for (Map map : rawList) { + CategoryDistVO vo = new CategoryDistVO(); + vo.setCategory((String) map.get("category")); + Object countObj = map.get("count"); + vo.setCount(countObj != null ? ((Number) countObj).longValue() : 0L); + result.add(vo); + } + return result; + } + + @Override + public List hotDocs() { + List docIdList = redisService.getHotList(null, RedisServiceImpl.DOC_KEY); + List result = new ArrayList<>(); + if (docIdList == null || docIdList.isEmpty()) { + return result; + } + int limit = Math.min(docIdList.size(), 10); + for (int i = 0; i < limit; i++) { + String docId = docIdList.get(i); + FileDocument doc = documentRepository.findById(docId); + if (doc == null) { + continue; + } + HotDocVO vo = new HotDocVO(); + vo.setId(docId); + vo.setTitle(doc.getName()); + vo.setViewCount((long) redisService.score(RedisServiceImpl.DOC_KEY, docId)); + result.add(vo); + } + return result; + } + + @Override + public List searchHotWords() { + List hotList = redisService.getHotList(null, RedisServiceImpl.SEARCH_KEY); + List result = new ArrayList<>(); + if (hotList == null || hotList.isEmpty()) { + return result; + } + int limit = Math.min(hotList.size(), 10); + for (int i = 0; i < limit; i++) { + String keyword = hotList.get(i); + SearchHotWordVO vo = new SearchHotWordVO(); + vo.setKeyword(keyword); + vo.setCount((long) redisService.score(RedisServiceImpl.SEARCH_KEY, keyword)); + result.add(vo); + } + return result; + } + + @Override + public List userActivity() { + // 暂无数据源,返回空列表 + return new ArrayList<>(); + } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TagServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TagServiceImpl.java index 3137d92..fb9f7da 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TagServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TagServiceImpl.java @@ -46,6 +46,7 @@ public void insert(Tag tag) { if (tag == null || !StringUtils.hasText(tag.getName())) { throw BusinessExceptionBuilder.of(ErrorCode.INVALID_PARAM).detail("标签名称不能为空").build(); } + tag.setId(UUID.randomUUID().toString()); tag.setCreateDate(new Date()); tag.setUpdateDate(new Date()); tagRepository.save(tag); @@ -160,6 +161,7 @@ public void addRelationShip(TagDocRelationship tagRelationship) { || !StringUtils.hasText(tagRelationship.getFileId())) { throw BusinessExceptionBuilder.of(ErrorCode.INVALID_PARAM).detail("标签关系不能为空").build(); } + tagRelationship.setId(UUID.randomUUID().toString()); tagRepository.saveRelationship(tagRelationship); log.info("标签关联创建成功:tagId={}, fileId={}", tagRelationship.getTagId(), tagRelationship.getFileId()); } @@ -201,6 +203,7 @@ public String saveOrUpdateTag(String tagName) { return existingTags.get(0).getId(); } Tag tag = new Tag(); + tag.setId(UUID.randomUUID().toString()); tag.setName(tagName); tag.setCreateDate(new Date()); tag.setUpdateDate(new Date()); diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ThumbnailServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ThumbnailServiceImpl.java index db3653b..0c7476a 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ThumbnailServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ThumbnailServiceImpl.java @@ -7,13 +7,22 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import net.coobird.thumbnailator.Thumbnails; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.apache.poi.xslf.usermodel.XSLFSlide; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.util.List; import java.util.UUID; /** @@ -59,7 +68,7 @@ public String makeThumb(InputStream inputStream, String fileName, int width, int byte[] thumbBytes = baos.toByteArray(); String thumbId = UUID.randomUUID().toString(); - String objectKey = StorageConstants.thumbPath(thumbId); + String objectKey = StorageConstants.thumbPath(thumbId, "jpg"); String result = minioStorageStrategy.upload(new ByteArrayInputStream(thumbBytes), objectKey, "image/jpeg"); @@ -78,6 +87,11 @@ public String makeThumb(InputStream inputStream, String fileName, int width, int @Override public String makePreview(InputStream inputStream, String fileName) { + return makePreview(inputStream, fileName, null); + } + + @Override + public String makePreview(InputStream inputStream, String fileName, String previewId) { if (inputStream == null || fileName == null || fileName.isEmpty()) { log.warn("生成预览图失败:输入参数为空"); return ""; @@ -94,8 +108,10 @@ public String makePreview(InputStream inputStream, String fileName) { ImageIO.write(previewImage, "jpg", baos); byte[] previewBytes = baos.toByteArray(); - String previewId = UUID.randomUUID().toString(); - String objectKey = StorageConstants.previewPath(previewId); + if (previewId == null || previewId.isEmpty()) { + previewId = UUID.randomUUID().toString(); + } + String objectKey = StorageConstants.previewPath(previewId, "jpg"); String result = minioStorageStrategy.upload(new ByteArrayInputStream(previewBytes), objectKey, "image/jpeg"); @@ -111,4 +127,231 @@ public String makePreview(InputStream inputStream, String fileName) { return ""; } } + + @Override + public String makePreviewForPdf(InputStream inputStream, String fileName, String previewId) { + if (inputStream == null || fileName == null || fileName.isEmpty()) { + log.warn("生成PDF预览图失败:输入参数为空"); + return ""; + } + + PDDocument document = null; + try { + document = PDDocument.load(inputStream); + PDFRenderer pdfRenderer = new PDFRenderer(document); + + // 渲染第一页 + BufferedImage pdfImage = pdfRenderer.renderImageWithDPI(0, 150, ImageType.RGB); + // 转换为 jpg + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(pdfImage, "jpg", baos); + byte[] previewBytes = baos.toByteArray(); + + if (previewId == null || previewId.isEmpty()) { + previewId = UUID.randomUUID().toString(); + } + String objectKey = StorageConstants.previewPath(previewId, "jpg"); + + String result = minioStorageStrategy.upload(new ByteArrayInputStream(previewBytes), objectKey, "image/jpeg"); + + if (result != null) { + log.info("PDF预览图生成成功:fileName={}, previewId={}, objectKey={}, pages={}", + fileName, previewId, objectKey, document.getNumberOfPages()); + return previewId; + } + + log.error("PDF预览图上传失败:fileName={}", fileName); + return ""; + } catch (Exception e) { + log.error("生成PDF预览图异常:fileName={}", fileName, e); + return ""; + } finally { + if (document != null) { + try { + document.close(); + } catch (Exception ignored) { + } + } + } + } + + @Override + public String makePreviewForPpt(InputStream inputStream, String fileName, String previewId) { + if (inputStream == null || fileName == null || fileName.isEmpty()) { + log.warn("生成PPT预览图失败:输入参数为空"); + return ""; + } + + try { + String lowerName = fileName.toLowerCase(); + if (!lowerName.endsWith(".pptx")) { + log.warn("PPT预览图仅支持PPTX格式:fileName={}", fileName); + return ""; + } + + // PPTX 预览图生成需要 POI 5.x 与 Java Graphics2D 配合 + // 由于 POI 5.x API 变化,暂时标记为不支持 + log.info("PPT预览图生成暂未完全支持:fileName={},需要进一步适配 POI 5.x API", fileName); + return ""; + } catch (Exception e) { + log.error("生成PPT预览图异常:fileName={}", fileName, e); + return ""; + } + } + + @Override + public String makeThumbForPdf(InputStream inputStream, String fileName, String thumbId) { + if (inputStream == null || fileName == null || fileName.isEmpty()) { + log.warn("生成PDF缩略图失败:输入参数为空"); + return ""; + } + + PDDocument document = null; + try { + document = PDDocument.load(inputStream); + PDFRenderer pdfRenderer = new PDFRenderer(document); + + // 渲染第一页作为缩略图 + BufferedImage pdfImage = pdfRenderer.renderImageWithDPI(0, 150, ImageType.RGB); + // 转换为 jpg + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(pdfImage, "jpg", baos); + byte[] thumbBytes = baos.toByteArray(); + + if (thumbId == null || thumbId.isEmpty()) { + thumbId = UUID.randomUUID().toString(); + } + // 存储到thumbnails文件夹,带.jpg后缀 + String objectKey = StorageConstants.thumbPath(thumbId, "jpg"); + + String result = minioStorageStrategy.upload(new ByteArrayInputStream(thumbBytes), objectKey, "image/jpeg"); + + if (result != null) { + log.info("PDF缩略图生成成功:fileName={}, thumbId={}, objectKey={}, pages={}", + fileName, thumbId, objectKey, document.getNumberOfPages()); + return thumbId; + } + + log.error("PDF缩略图上传失败:fileName={}", fileName); + return ""; + } catch (Exception e) { + log.error("生成PDF缩略图异常:fileName={}", fileName, e); + return ""; + } finally { + if (document != null) { + try { + document.close(); + } catch (Exception ignored) { + } + } + } + } + + @Override + public String makeThumbForPpt(InputStream inputStream, String fileName, String thumbId) { + if (inputStream == null || fileName == null || fileName.isEmpty()) { + log.warn("生成PPT缩略图失败:输入参数为空"); + return ""; + } + + XMLSlideShow slideShow = null; + try { + String lowerName = fileName.toLowerCase(); + if (!lowerName.endsWith(".pptx")) { + log.warn("PPT缩略图仅支持PPTX格式:fileName={}", fileName); + return ""; + } + + slideShow = new XMLSlideShow(inputStream); + List slides = slideShow.getSlides(); + if (slides == null || slides.isEmpty()) { + log.warn("PPT文件中没有幻灯片:fileName={}", fileName); + return ""; + } + + // 获取第一张幻灯片 + XSLFSlide firstSlide = slides.get(0); + + // 获取幻灯片尺寸 + Dimension slideSize = slideShow.getPageSize(); + double width = slideSize.width; + double height = slideSize.height; + + // 创建缓冲区图像 + BufferedImage image = new BufferedImage((int) width, (int) height, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + + // 设置背景和渲染选项 + graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, java.awt.RenderingHints.VALUE_RENDER_QUALITY); + + // 设置白色背景 + graphics.setPaint(java.awt.Color.WHITE); + graphics.fill(new Rectangle2D.Float(0, 0, (float) width, (float) height)); + + // 渲染第一张幻灯片 + firstSlide.draw(graphics); + + // 缩放为缩略图 (200x200) + BufferedImage thumbImage = Thumbnails.of(image) + .size(200, 200) + .asBufferedImage(); + + // 转换为jpg + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(thumbImage, "jpg", baos); + byte[] thumbBytes = baos.toByteArray(); + + if (thumbId == null || thumbId.isEmpty()) { + thumbId = UUID.randomUUID().toString(); + } + // 存储到thumbnails文件夹,带.jpg后缀 + String objectKey = StorageConstants.thumbPath(thumbId, "jpg"); + + String result = minioStorageStrategy.upload(new ByteArrayInputStream(thumbBytes), objectKey, "image/jpeg"); + + if (result != null) { + log.info("PPT缩略图生成成功:fileName={}, thumbId={}, objectKey={}, totalSlides={}", + fileName, thumbId, objectKey, slides.size()); + return thumbId; + } + + log.error("PPT缩略图上传失败:fileName={}", fileName); + return ""; + } catch (Exception e) { + log.error("生成PPT缩略图异常:fileName={}", fileName, e); + return ""; + } finally { + if (slideShow != null) { + try { + slideShow.close(); + } catch (Exception ignored) { + } + } + } + } + + @Override + public String makeThumbForDocx(InputStream inputStream, String fileName, String thumbId) { + if (inputStream == null || fileName == null || fileName.isEmpty()) { + log.warn("生成DOCX缩略图失败:输入参数为空"); + return ""; + } + + try { + String lowerName = fileName.toLowerCase(); + if (!lowerName.endsWith(".docx")) { + log.warn("DOCX缩略图仅支持DOCX格式:fileName={}", fileName); + return ""; + } + + // DOCX缩略图生成需要XWPF和Java Graphics2D配合 + // 由于实现复杂度较高,暂使用占位实现 + log.info("DOCX缩略图生成暂未完全支持:fileName={},需要进一步实现", fileName); + return ""; + } catch (Exception e) { + log.error("生成DOCX缩略图异常:fileName={}", fileName, e); + return ""; + } + } } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TokenServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..7ad3d07 --- /dev/null +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TokenServiceImpl.java @@ -0,0 +1,68 @@ +package com.jiaruiblog.application.service.impl; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.jiaruiblog.application.service.ITokenService; +import com.jiaruiblog.domain.entity.po.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Token服务实现类 + * 使用JWT实现Token生成和验证 + */ +@Service +public class TokenServiceImpl implements ITokenService { + + /** + * 密钥持有类 - 解决静态字段无法注入的问题 + */ + private static class JwtSecretHolder { + private static String SECRET; + } + + /** + * 过期时间:2天 + * 单位为秒 + **/ + private static final long EXPIRATION = 864000L; + + @Value("${jwt.secret}") + public void setSecret(String secret) { + JwtSecretHolder.SECRET = secret; + } + + @Override + public String createToken(User user) { + Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000); + Map map = new HashMap<>(8); + map.put("alg", "HS256"); + map.put("typ", "JWT"); + return JWT.create() + .withHeader(map) + .withClaim("id", user.getId()) + .withClaim("username", user.getUsername()) + .withExpiresAt(expireDate) + .withIssuedAt(new Date()) + .sign(Algorithm.HMAC256(JwtSecretHolder.SECRET)); + } + + @Override + public Map verifyToken(String token) { + DecodedJWT jwt; + try { + JWTVerifier verifier = JWT.require(Algorithm.HMAC256(JwtSecretHolder.SECRET)).build(); + jwt = verifier.verify(token); + } catch (Exception e) { + return Map.of(); + } + return jwt.getClaims(); + } +} \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/UserServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/UserServiceImpl.java index a90fcab..71bf9ad 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/UserServiceImpl.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/UserServiceImpl.java @@ -1,6 +1,7 @@ package com.jiaruiblog.application.service.impl; import com.jiaruiblog.application.service.IUserService; +import com.jiaruiblog.application.service.ITokenService; import com.jiaruiblog.common.constants.StorageConstants; import com.jiaruiblog.common.enums.PermissionEnum; import com.jiaruiblog.common.exception.BusinessException; @@ -8,13 +9,13 @@ import com.jiaruiblog.domain.entity.po.User; import com.jiaruiblog.domain.entity.bo.UserBO; import com.jiaruiblog.domain.entity.dto.BasePageDTO; +import com.jiaruiblog.domain.entity.dto.BasicRegistryDTO; import com.jiaruiblog.domain.entity.dto.RegistryUserDTO; import com.jiaruiblog.domain.entity.dto.UserRoleDTO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.domain.entity.vo.UserVO; import com.jiaruiblog.infrastructure.repository.UserRepository; import com.jiaruiblog.infrastructure.storage.MinioStorageStrategy; -import com.jiaruiblog.common.util.HmacUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Sort; @@ -48,7 +49,7 @@ public class UserServiceImpl implements IUserService { MinioStorageStrategy minioStorageStrategy; @Resource - HmacUtil hmacUtil; + ITokenService tokenService; @Override public void initFirstUser() { @@ -56,6 +57,7 @@ public void initFirstUser() { List existingUsers = userRepository.findByUsername(DEFAULT_ADMIN_USERNAME); if (existingUsers == null || existingUsers.isEmpty()) { User admin = new User(); + admin.setId(UUID.randomUUID().toString()); admin.setUsername(DEFAULT_ADMIN_USERNAME); admin.setPassword(encodePassword(DEFAULT_ADMIN_PASSWORD)); admin.setPermissionEnum(PermissionEnum.ADMIN); @@ -88,10 +90,10 @@ public Map login(RegistryUserDTO userDTO) { // 生成token String token; try { - token = hmacUtil.generateHmac(user.getId()); + token = tokenService.createToken(user); } catch (Exception e) { log.error("生成token失败", e); - token = UUID.randomUUID().toString(); + throw new BusinessException(ErrorCode.OPERATE_FAILED, "生成token失败"); } log.info("用户登录成功:username={}, userId={}", userDTO.getUsername(), user.getId()); @@ -109,13 +111,13 @@ public Map login(RegistryUserDTO userDTO) { // 更新登录时间 user.setLastLogin(new Date()); - userRepository.update(user); + userRepository.updateLoginTime(user); return result; } @Override - public void registry(RegistryUserDTO userDTO) { + public void registry(BasicRegistryDTO userDTO) { if (userDTO == null || !StringUtils.hasText(userDTO.getUsername()) || !StringUtils.hasText(userDTO.getPassword())) { throw new BusinessException(ErrorCode.INVALID_PARAM, "用户名或密码不能为空"); } @@ -126,6 +128,7 @@ public void registry(RegistryUserDTO userDTO) { } User user = new User(); + user.setId(UUID.randomUUID().toString()); user.setUsername(userDTO.getUsername()); user.setPassword(encodePassword(userDTO.getPassword())); user.setPermissionEnum(PermissionEnum.USER); @@ -203,8 +206,10 @@ public void blockUser(String userId) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } + // 切换封禁状态 + user.setBanning(!user.getBanning()); userRepository.blockUser(user); - log.info("用户已被封禁:userId={}", userId); + log.info("用户封禁状态变更:userId={}, banning={}", userId, user.getBanning()); } @Override @@ -389,6 +394,9 @@ public boolean updateUserBySelf(UserBO userBO) { if (userBO.getMail() != null) { user.setMail(userBO.getMail()); } + if (userBO.getNickname() != null) { + user.setNickname(userBO.getNickname()); + } if (userBO.getMale() != null) { user.setMale(userBO.getMale()); } @@ -423,6 +431,9 @@ public boolean updateUserByAdmin(UserBO userBO) { if (userBO.getMail() != null) { user.setMail(userBO.getMail()); } + if (userBO.getNickname() != null) { + user.setNickname(userBO.getNickname()); + } if (userBO.getMale() != null) { user.setMale(userBO.getMale()); } @@ -462,7 +473,7 @@ private UserVO convertToUserVO(User user) { /** * 简单密码加密(实际应用中应使用BCrypt等更安全的方案) */ - private String encodePassword(String password) { + private static String encodePassword(String password) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8)); diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/data/TaskData.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/data/TaskData.java index 8aa81e6..1079346 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/data/TaskData.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/data/TaskData.java @@ -2,28 +2,26 @@ import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.common.enums.FileFormatEnum; +import lombok.Getter; +import lombok.Setter; /** * @author Jarrett Luo * @Date 2022/10/26 17:30 * @Version 1.0 */ +@Setter +@Getter public class TaskData { private FileDocument fileDocument; + private String txtFilePath; + private String thumbFilePath; + private String previewFilePath; + private FileFormatEnum docType; - public FileDocument getFileDocument() { return fileDocument; } - public void setFileDocument(FileDocument fileDocument) { this.fileDocument = fileDocument; } - public String getTxtFilePath() { return txtFilePath; } - public void setTxtFilePath(String txtFilePath) { this.txtFilePath = txtFilePath; } - public String getThumbFilePath() { return thumbFilePath; } - public void setThumbFilePath(String thumbFilePath) { this.thumbFilePath = thumbFilePath; } - public String getPreviewFilePath() { return previewFilePath; } - public void setPreviewFilePath(String previewFilePath) { this.previewFilePath = previewFilePath; } - public FileFormatEnum getDocType() { return docType; } - public void setDocType(FileFormatEnum docType) { this.docType = docType; } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/DocxExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/DocxExecutor.java index ff9ca78..4ac5914 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/DocxExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/DocxExecutor.java @@ -1,8 +1,11 @@ package com.jiaruiblog.application.task.executor; import com.jiaruiblog.application.service.FileOperationService; +import com.jiaruiblog.application.service.ThumbnailService; import com.jiaruiblog.application.task.data.TaskData; +import com.jiaruiblog.application.task.exception.TaskRunException; import com.jiaruiblog.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; import lombok.extern.slf4j.Slf4j; import java.io.*; @@ -14,6 +17,39 @@ @Slf4j public class DocxExecutor extends TaskExecutor { + @Override + public void execute(TaskData taskData) throws TaskRunException { + // 第一步下载文件,转换为byte数组 + FileDocument fileDocument = taskData.getFileDocument(); + byte[] dfsBytes = downFileBytes(fileDocument.getGridfsId()); + InputStream docInputStream = new ByteArrayInputStream(dfsBytes); + + // 第二步 将文本索引到es中 + try { + uploadFileToEs(docInputStream, fileDocument, taskData); + } catch (Exception e) { + throw new TaskRunException("建立索引的时候出错!", e); + } + + // 第三步 生成DOCX缩略图,存储到thumbnails文件夹 + docInputStream = new ByteArrayInputStream(dfsBytes); + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + String thumbId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makeThumbForDocx(docInputStream, fileDocument.getName(), thumbId); + if (result != null && !result.isEmpty()) { + fileDocument.setThumbId(result); + log.info("DOCX缩略图生成成功:docId={}, thumbId={}", fileDocument.getId(), result); + } + } catch (Exception e) { + log.error("DOCX缩略图生成失败:docId={}", fileDocument.getId(), e); + } + + // 第四步 DOCX不需要制作预览文件(预览文件指如PPT转PDF这类需要格式转换的文件) + docInputStream = new ByteArrayInputStream(dfsBytes); + makePreviewFile(docInputStream, taskData); + } + @Override protected void readText(InputStream is, String textFilePath) throws IOException { if (is == null) { @@ -33,11 +69,14 @@ protected void readText(InputStream is, String textFilePath) throws IOException @Override protected void makeThumb(InputStream is, String picPath) throws IOException { - log.debug("Office 文档缩略图生成暂未实现"); + // DOCX缩略图生成已移至execute()方法中通过ThumbnailService.makeThumbForDocx()实现 + // 此方法不再需要实现,保留接口签名以满足抽象类要求 + log.debug("DOCX缩略图通过execute()中的ThumbnailService.makeThumbForDocx()生成"); } @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - // 预览文件暂未实现 + // DOCX不需要格式转换,预览文件暂未实现 + log.debug("DOCX不需要生成预览文件"); } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PdfWordTaskExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PdfWordTaskExecutor.java index 6ad2ecf..05eeb8b 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PdfWordTaskExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PdfWordTaskExecutor.java @@ -1,12 +1,26 @@ package com.jiaruiblog.application.task.executor; +import com.jiaruiblog.application.service.DocumentService; +import com.jiaruiblog.application.service.ElasticService; import com.jiaruiblog.application.service.FileOperationService; +import com.jiaruiblog.application.service.ThumbnailService; import com.jiaruiblog.application.task.data.TaskData; +import com.jiaruiblog.application.task.exception.TaskRunException; +import com.jiaruiblog.common.constants.StorageConstants; +import com.jiaruiblog.common.enums.FileFormatEnum; import com.jiaruiblog.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.po.SearchDocument; +import com.jiaruiblog.infrastructure.storage.StorageFactory; +import com.jiaruiblog.infrastructure.storage.StorageStrategy; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; import java.io.*; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; /** * PDF 文档解析执行器,使用 Apache Tika 提取文字 @@ -14,6 +28,38 @@ @Slf4j public class PdfWordTaskExecutor extends TaskExecutor { + @Override + public void execute(TaskData taskData) throws TaskRunException { + // 第一步下载文件,转换为byte数组 + FileDocument fileDocument = taskData.getFileDocument(); + byte[] dfsBytes = downFileBytes(fileDocument.getGridfsId()); + InputStream docInputStream = new ByteArrayInputStream(dfsBytes); + + // 第二步 将文本索引到es中 + try { + uploadFileToEs(docInputStream, fileDocument, taskData); + } catch (Exception e) { + throw new TaskRunException("建立索引的时候出错!", e); + } + + // 第三步 生成PDF缩略图(第一页渲染),存储到thumbnails文件夹 + docInputStream = new ByteArrayInputStream(dfsBytes); + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + String thumbId = fileDocument.getMd5(); + String result = thumbnailService.makeThumbForPdf(docInputStream, fileDocument.getName(), thumbId); + if (result != null && !result.isEmpty()) { + fileDocument.setThumbId(result); + log.info("PDF缩略图生成成功:docId={}, thumbId={}", fileDocument.getId(), result); + } + } catch (Exception e) { + log.error("PDF缩略图生成失败:docId={}", fileDocument.getId(), e); + } + + // 第四步 PDF不需要制作预览文件(预览文件指如PPT转PDF这类需要格式转换的文件) + // makePreviewFile()在此不做任何处理 + } + @Override protected void readText(InputStream is, String textFilePath) throws IOException { if (is == null) { @@ -33,11 +79,15 @@ protected void readText(InputStream is, String textFilePath) throws IOException @Override protected void makeThumb(InputStream is, String picPath) throws IOException { - log.debug("PDF 缩略图生成暂未实现"); + // PDF缩略图生成已移至execute()方法中通过ThumbnailService.makeThumbForPdf()实现 + // 此方法不再需要实现,保留接口签名以满足抽象类要求 + log.debug("PDF缩略图通过execute()中的ThumbnailService.makeThumbForPdf()生成"); } @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - // 预览文件暂未实现 + // PDF不需要格式转换,预览文件指如PPT转PDF这类需要格式转换的文件 + // PDF的缩略图已在execute()中通过makeThumbForPdf()生成并设置thumbId + log.debug("PDF不需要生成预览文件"); } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PicExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PicExecutor.java index 7a09a29..6528177 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PicExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PicExecutor.java @@ -1,7 +1,10 @@ package com.jiaruiblog.application.task.executor; +import com.jiaruiblog.application.service.ThumbnailService; import com.jiaruiblog.application.task.data.TaskData; +import com.jiaruiblog.common.util.SpringApplicationContext; import com.jiaruiblog.domain.entity.po.FileDocument; +import lombok.extern.slf4j.Slf4j; import javax.imageio.ImageIO; import java.awt.*; @@ -18,6 +21,7 @@ * @Date 2023/10/6 23:36 * @Version 1.0 **/ +@Slf4j public class PicExecutor extends TaskExecutor { public static final String PNG = "png"; @@ -45,7 +49,9 @@ protected void makeThumb(InputStream is, String picPath) throws IOException { @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - // do nothing for pic + // 图片文件不需要格式转换,预览图直接使用缩略图即可 + // previewFileId 不需要设置,图片本身即是预览 + log.debug("图片文件不需要生成预览文件,直接使用缩略图即可"); } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/TaskExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/TaskExecutor.java index 710179f..b7babee 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/TaskExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/TaskExecutor.java @@ -4,10 +4,13 @@ import com.jiaruiblog.application.service.ElasticService; import com.jiaruiblog.application.task.data.TaskData; import com.jiaruiblog.application.task.exception.TaskRunException; +import com.jiaruiblog.common.constants.StorageConstants; import com.jiaruiblog.common.enums.FileFormatEnum; import com.jiaruiblog.common.util.SpringApplicationContext; import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.po.SearchDocument; +import com.jiaruiblog.infrastructure.storage.StorageFactory; +import com.jiaruiblog.infrastructure.storage.StorageStrategy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; @@ -40,17 +43,14 @@ public void execute(TaskData taskData) throws TaskRunException { } docInputStream = new ByteArrayInputStream(dfsBytes); try { - // 制作不同分辨率的缩略图 + // 第三步 制作不同分辨率的缩略图 updateFileThumb(docInputStream, taskData.getFileDocument(), taskData); } catch (Exception e) { throw new TaskRunException("建立缩略图的时候出错啦!", e); } - // 第三步 制作预览文件 + // 第四步 制作预览文件;例如ppt需要转成pdf进行预览 docInputStream = new ByteArrayInputStream(dfsBytes); makePreviewFile(docInputStream, taskData); - - // 清空内存占用 - dfsBytes = new byte[]{}; } /** @@ -76,11 +76,15 @@ public void uploadFileToEs(InputStream is, FileDocument fileDocument, TaskData t throw new TaskRunException("文本文件不存在,需要进行重新提取"); } SearchDocument searchDocument = new SearchDocument(); - searchDocument.setId(fileDocument.getMd5()); + searchDocument.setId(fileDocument.getId()); // Use UUID to match initial indexing searchDocument.setName(fileDocument.getName()); searchDocument.setType(fileDocument.getContentType()); // 直接读取提取的文本内容,不做 base64 编码 searchDocument.setContent(Files.readString(Paths.get(textFilePath), StandardCharsets.UTF_8)); + // Preserve tagNames and categoryName from initial indexing + DocumentService documentService = SpringApplicationContext.getBean(DocumentService.class); + searchDocument.setTagNames(documentService.getTagNamesByDocId(fileDocument.getId())); + searchDocument.setCategoryName(documentService.getCategoryNameByDocId(fileDocument.getId())); this.upload(searchDocument); } catch (IOException | TaskRunException e) { @@ -92,12 +96,19 @@ public void uploadFileToEs(InputStream is, FileDocument fileDocument, TaskData t // 被文本文件上传到gridFS系统中 try (FileInputStream inputStream = new FileInputStream(textFilePath)) { + // 使用 document-texts/{md5}_{originalName}.txt 路径存储文本文件 + String originalName = fileDocument.getName(); + String md5 = fileDocument.getMd5(); + // textObjectKey 已经包含 .txt 后缀,documentTextPath 会再添加一次,所以传入时不带 .txt + String textObjectKey = md5 + "_" + originalName; + String fullPath = StorageConstants.documentTextPath(textObjectKey); + DocumentService fileService = SpringApplicationContext.getBean(DocumentService.class); - String txtObjId = fileService.uploadFileToGridFs( - FileFormatEnum.TEXT.getFilePrefix(), - inputStream, - FileFormatEnum.TEXT.getContentType()); - fileDocument.setTextFileId(txtObjId); + StorageFactory storageFactory = SpringApplicationContext.getBean(StorageFactory.class); + StorageStrategy storageStrategy = storageFactory.getStorageStrategy(); + storageStrategy.upload(inputStream, fullPath, FileFormatEnum.TEXT.getContentType()); + + fileDocument.setTextFileId(textObjectKey); } catch (IOException e) { throw new TaskRunException("存储文本文件报错了,请核对", e); @@ -178,11 +189,12 @@ public void upload(SearchDocument searchDocument) throws IOException { public void updateFileThumb(InputStream inputStream, FileDocument fileDocument, TaskData taskData) throws IOException { - String picPath = "./" + java.util.UUID.randomUUID().toString() + ".png"; + String picPath = "./" + java.util.UUID.randomUUID() + ".png"; taskData.setThumbFilePath(picPath); - // 将pdf输入流转换为图片并临时保存下来 + // 将文档输入流转换为图片并临时保存下来 makeThumb(inputStream, picPath); + if ( !new File(picPath).exists()) { return; } @@ -202,7 +214,7 @@ public void updateFileThumb(InputStream inputStream, FileDocument fileDocument, try { Files.delete(Paths.get(picPath)); } catch (IOException e) { - log.error("删除文件路径{} ==> 失败信息{}", picPath, e); + log.error("删除文件路径{} ==> 失败信息{}", picPath, e.getMessage()); } } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptExecutor.java index 16335cd..dac6c18 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptExecutor.java @@ -1,9 +1,12 @@ package com.jiaruiblog.application.task.executor.slider; import com.jiaruiblog.application.service.FileOperationService; +import com.jiaruiblog.application.service.ThumbnailService; import com.jiaruiblog.application.task.data.TaskData; import com.jiaruiblog.application.task.executor.TaskExecutor; +import com.jiaruiblog.application.task.exception.TaskRunException; import com.jiaruiblog.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; import lombok.extern.slf4j.Slf4j; import java.io.*; @@ -15,6 +18,40 @@ @Slf4j public class PptExecutor extends TaskExecutor { + @Override + public void execute(TaskData taskData) throws TaskRunException { + // 第一步下载文件,转换为byte数组 + FileDocument fileDocument = taskData.getFileDocument(); + byte[] dfsBytes = downFileBytes(fileDocument.getGridfsId()); + InputStream docInputStream = new ByteArrayInputStream(dfsBytes); + + // 第二步 将文本索引到es中 + try { + uploadFileToEs(docInputStream, fileDocument, taskData); + } catch (Exception e) { + throw new TaskRunException("建立索引的时候出错!", e); + } + + // 第三步 生成PPT缩略图(第一页渲染),存储到thumbnails文件夹 + docInputStream = new ByteArrayInputStream(dfsBytes); + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + String thumbId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makeThumbForPpt(docInputStream, fileDocument.getName(), thumbId); + if (result != null && !result.isEmpty()) { + fileDocument.setThumbId(result); + log.info("PPT缩略图生成成功:docId={}, thumbId={}", fileDocument.getId(), result); + } + } catch (Exception e) { + log.error("PPT缩略图生成失败:docId={}", fileDocument.getId(), e); + } + + // 第四步 生成预览文件(PPT转PDF),存储到previews文件夹 + // PPT预览文件生成需要完整的PPT转PDF转换,目前暂未实现 + docInputStream = new ByteArrayInputStream(dfsBytes); + makePreviewFile(docInputStream, taskData); + } + @Override protected void readText(InputStream is, String textFilePath) throws IOException { if (is == null) { @@ -34,11 +71,31 @@ protected void readText(InputStream is, String textFilePath) throws IOException @Override protected void makeThumb(InputStream is, String picPath) throws IOException { - log.debug("PPT 缩略图生成暂未实现"); + // PPT缩略图生成已移至execute()方法中通过ThumbnailService.makeThumbForPpt()实现 + // 此方法不再需要实现,保留接口签名以满足抽象类要求 + log.debug("PPT缩略图通过execute()中的ThumbnailService.makeThumbForPpt()生成"); } @Override protected void makePreviewFile(InputStream inStream, TaskData taskData) { - // 预览文件暂未实现 + // PPT预览文件(PPT转PDF)暂未实现 + // 预览文件转换需要完整的格式转换支持 + if (inStream == null) { + log.warn("PPT预览文件生成失败:输入流为空"); + return; + } + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + FileDocument fileDocument = taskData.getFileDocument(); + // 使用 md5 + filename 作为预览文件ID + String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makePreviewForPpt(inStream, fileDocument.getName(), previewId); + if (result != null && !result.isEmpty()) { + fileDocument.setPreviewFileId(previewId); + log.info("PPT预览文件生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); + } + } catch (Exception e) { + log.error("PPT预览文件生成异常:docId={}", taskData.getFileDocument().getId(), e); + } } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptxExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptxExecutor.java index 1301613..50a9dc6 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptxExecutor.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/slider/PptxExecutor.java @@ -1,7 +1,10 @@ package com.jiaruiblog.application.task.executor.slider; +import com.jiaruiblog.application.service.ThumbnailService; import com.jiaruiblog.application.task.data.TaskData; import com.jiaruiblog.application.task.executor.DocxExecutor; +import com.jiaruiblog.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; @@ -18,6 +21,22 @@ public class PptxExecutor extends DocxExecutor { @Override protected void makePreviewFile(InputStream inStream, TaskData taskData) { - // no action + if (inStream == null) { + log.warn("PPTX预览图生成失败:输入流为空"); + return; + } + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + FileDocument fileDocument = taskData.getFileDocument(); + // 使用 md5 + filename 作为预览图ID + String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makePreviewForPpt(inStream, fileDocument.getName(), previewId); + if (result != null && !result.isEmpty()) { + fileDocument.setPreviewFileId(previewId); + log.info("PPTX预览图生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); + } + } catch (Exception e) { + log.error("PPTX预览图生成异常:docId={}", taskData.getFileDocument().getId(), e); + } } } \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/LikeTask.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/LikeTask.java deleted file mode 100644 index fa86974..0000000 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/LikeTask.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.jiaruiblog.application.task.like; - -import com.jiaruiblog.application.service.LikeService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.springframework.scheduling.quartz.QuartzJobBean; - - -/** - * @ClassName LikeTask - * @Description 点赞的定时任务 - * @author luojiarui - * @Date 2023/4/3 22:10 - * @Version 1.0 - **/ -@Slf4j -public class LikeTask extends QuartzJobBean { - - @Resource - LikeService likeService; - - @Override - protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { - // 将 Redis 里的点赞信息同步到数据库里 - likeService.transLikedFromRedis2DB(); - } -} \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/UserLikeDetail.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/UserLikeDetail.java deleted file mode 100644 index 3d5b62a..0000000 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/like/UserLikeDetail.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.jiaruiblog.application.task.like; - -import com.jiaruiblog.common.enums.RedisActionEnum; -import lombok.Data; - -/** - * @ClassName UserLikeDetail - * @Description 用户通过redis点赞的信息实体 - * @author luojiarui - * @Date 2023/4/5 11:52 - * @Version 1.0 - **/ -@Data -public class UserLikeDetail { - - private String id; - - private String entityId; - - private String userId; - - private RedisActionEnum action; - -} \ No newline at end of file diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/task/thread/MainTask.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/thread/MainTask.java index 5a864b8..9511dbf 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/task/thread/MainTask.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/task/thread/MainTask.java @@ -52,6 +52,7 @@ public MainTask(FileDocument fileDocument) { @Override public void success() { taskData.getFileDocument().setDocState(DocStateEnum.SUCCESS); + taskData.getFileDocument().setReviewing(false); updateTaskStatus(); // 更新文档的数据 @@ -64,6 +65,7 @@ public void failed(Throwable throwable) { log.error("解析文件报错啦", throwable); String errorMsg = throwable.getMessage(); taskData.getFileDocument().setDocState(DocStateEnum.FAIL); + taskData.getFileDocument().setReviewing(false); taskData.getFileDocument().setErrorMsg(errorMsg +" " + throwable.getCause()); updateTaskStatus(); } diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/DTO2BOConverter.java b/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/DTO2BOConverter.java index 580fe66..1124a1a 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/DTO2BOConverter.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/DTO2BOConverter.java @@ -33,6 +33,7 @@ public static UserBO userDTO2BO(UserDTO userDTO) { } userBO.setPhone(userDTO.getPhone()); userBO.setMail(userDTO.getMail()); + userBO.setNickname(userDTO.getNickname()); userBO.setMale(userDTO.isMale()); userBO.setBirthtime(userDTO.getBirthtime()); userBO.setDescription(userDTO.getDescription()); diff --git a/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/PO2VOConverter.java b/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/PO2VOConverter.java index 2421f83..233a6ee 100644 --- a/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/PO2VOConverter.java +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/transformer/PO2VOConverter.java @@ -20,7 +20,7 @@ public class PO2VOConverter { public static final String DELETE = "DELETE"; public static final String DOWNLOAD = "DOWNLOAD"; public static final String UPLOAD = "UPLOAD"; - public static final String PREVIEW = "VIEW"; + public static final String PREVIEW = "PREVIEW"; public static List docLogListConvert(List docLogList) { if (CollectionUtil.isEmpty(docLogList)) { @@ -41,6 +41,7 @@ public static DocLogVO docLogConvert(DocLog docLog) { DocLogVO docLogVO = new DocLogVO(); docLogVO.setId(docLog.getId()); + docLogVO.setUserId(docLog.getUserId()); docLogVO.setUserName(docLog.getUserName()); docLogVO.setDocName(docLog.getDocName()); docLogVO.setCreateDate(docLog.getCreateDate()); diff --git a/all-docs-bootstrap/pom.xml b/all-docs-bootstrap/pom.xml index 156189b..b847a8c 100644 --- a/all-docs-bootstrap/pom.xml +++ b/all-docs-bootstrap/pom.xml @@ -50,6 +50,10 @@ mysql-connector-j runtime + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/all-docs-bootstrap/src/main/resources/application-dev.yml b/all-docs-bootstrap/src/main/resources/application-dev.yml index 0c262f3..71bf105 100644 --- a/all-docs-bootstrap/src/main/resources/application-dev.yml +++ b/all-docs-bootstrap/src/main/resources/application-dev.yml @@ -9,9 +9,9 @@ spring: # 开发环境 MySQL datasource: - url: jdbc:mysql://${MYSQL_HOST:192.168.1.29}:${MYSQL_PORT:5455}/${MYSQL_DATABASE:all_docs}?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4 + url: jdbc:mysql://${MYSQL_HOST:192.168.1.29}:${MYSQL_PORT:5455}/${MYSQL_DATABASE:all_docs}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 username: root - password: ${MYSQL_PASSWORD:} + password: ${MYSQL_PASSWORD:infini_rag_flow} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 @@ -24,7 +24,7 @@ spring: database: 0 host: ${REDIS_HOST:192.168.1.29} port: ${REDIS_PORT:16379} - password: ${REDIS_PWD:} + password: ${REDIS_PWD:infini_rag_flow} timeout: 3000 jedis: pool: @@ -40,6 +40,12 @@ spring: pathmatch: matching-strategy: ant_path_matcher + # Spring Data Elasticsearch 配置 + elasticsearch: + uris: http://${ES_HOST:192.168.1.29}:${ES_PORT:1200} + username: ${ES_USERNAME:elastic} + password: ${ES_PASSWORD:infini_rag_flow} + logging: level: root: INFO @@ -49,6 +55,14 @@ cloud: elasticsearch: host: ${ES_HOST:192.168.1.29} port: ${ES_PORT:1200} + username: ${ES_USERNAME:elastic} + password: ${ES_PASSWORD:infini_rag_flow} + +# 禁用 ES 健康检查(启动时不检查 ES 连接) +management: + health: + elasticsearch: + enabled: false # 开发环境 MinIO minio: @@ -75,9 +89,19 @@ all-docs: file-path: sensitive-file: sensitive.txt +# 开发环境 HMAC +hmac: + secret: + key: ${HMAC_SECRET_KEY:dev-hmac-secret-key} + # 异步线程池 async: core-pool-size: 5 max-pool-size: 20 queue-capacity: 100 keep-alive-seconds: 30 + +# JWT 配置 +jwt: + secret: ${JWT_SECRET:dev-jwt-secret-key-change-in-production} + expiration: 172800000 diff --git a/all-docs-bootstrap/src/main/resources/application-prod.yml b/all-docs-bootstrap/src/main/resources/application-prod.yml index 2f88f70..6029148 100644 --- a/all-docs-bootstrap/src/main/resources/application-prod.yml +++ b/all-docs-bootstrap/src/main/resources/application-prod.yml @@ -1,7 +1,7 @@ spring: # 生产环境 MySQL(必须通过环境变量注入) datasource: - url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:3306}/${MYSQL_DATABASE}?useSSL=true&serverTimezone=UTC&characterEncoding=utf8mb4&rewriteBatchedStatements=true + url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:3306}/${MYSQL_DATABASE}?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8&rewriteBatchedStatements=true username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver @@ -39,6 +39,21 @@ cloud: elasticsearch: host: ${ES_HOST} port: ${ES_PORT:9200} + username: ${ES_USERNAME:elastic} + password: ${ES_PASSWORD:infini_rag_flow} + +# 禁用 ES 健康检查 +management: + health: + elasticsearch: + enabled: false + +# Spring Data Elasticsearch 配置 +spring: + elasticsearch: + uris: http://${ES_HOST}:${ES_PORT:9200} + username: ${ES_USERNAME:elastic} + password: ${ES_PASSWORD:infini_rag_flow} # 生产环境 MinIO minio: @@ -85,3 +100,8 @@ async: max-pool-size: 100 queue-capacity: 500 keep-alive-seconds: 60 + +# JWT 配置(生产环境必须设置环境变量) +jwt: + secret: ${JWT_SECRET} + expiration: 172800000 diff --git a/all-docs-bootstrap/src/main/resources/i18n/messages.properties b/all-docs-bootstrap/src/main/resources/i18n/messages.properties index c2b34cd..823ba22 100644 --- a/all-docs-bootstrap/src/main/resources/i18n/messages.properties +++ b/all-docs-bootstrap/src/main/resources/i18n/messages.properties @@ -46,6 +46,9 @@ error-code.permission-denied=Permission denied error-code.invalid-permission=Invalid permission error-code.role-not-found=Role not found +# Collect Related +error-code.document-already-collected=Document already collected + # Category & Tag Related error-code.category-not-found=Category not found error-code.tag-not-found=Tag not found diff --git a/all-docs-bootstrap/src/main/resources/i18n/messages_en_US.properties b/all-docs-bootstrap/src/main/resources/i18n/messages_en_US.properties index dddb8e5..4829a68 100644 --- a/all-docs-bootstrap/src/main/resources/i18n/messages_en_US.properties +++ b/all-docs-bootstrap/src/main/resources/i18n/messages_en_US.properties @@ -46,6 +46,9 @@ error-code.permission-denied=Permission denied error-code.invalid-permission=Invalid permission error-code.role-not-found=Role not found +# Collect Related +error-code.document-already-collected=Document already collected + # Category & Tag Related error-code.category-not-found=Category not found error-code.tag-not-found=Tag not found diff --git a/all-docs-bootstrap/src/main/resources/i18n/messages_ja.properties b/all-docs-bootstrap/src/main/resources/i18n/messages_ja.properties index d6175cb..4e45795 100644 --- a/all-docs-bootstrap/src/main/resources/i18n/messages_ja.properties +++ b/all-docs-bootstrap/src/main/resources/i18n/messages_ja.properties @@ -46,6 +46,9 @@ error-code.permission-denied=権限がありません error-code.invalid-permission=無効な権限 error-code.role-not-found=ロールが見つかりません +# 收藏関連 +error-code.document-already-collected=ドキュメントは既にブックマークされています + # カテゴリ・タグ関連 error-code.category-not-found=カテゴリが見つかりません error-code.tag-not-found=タグが見つかりません diff --git a/all-docs-bootstrap/src/main/resources/i18n/messages_ko.properties b/all-docs-bootstrap/src/main/resources/i18n/messages_ko.properties index 5d42156..bd4f24c 100644 --- a/all-docs-bootstrap/src/main/resources/i18n/messages_ko.properties +++ b/all-docs-bootstrap/src/main/resources/i18n/messages_ko.properties @@ -46,6 +46,9 @@ error-code.permission-denied=권한 거부 error-code.invalid-permission=잘못된 권한 error-code.role-not-found=역할을 찾을 수 없음 +#收藏関連 +error-code.document-already-collected=문서가 이미 북마크되었습니다 + # 카테고리 및 태그 관련 error-code.category-not-found=카테고리를 찾을 수 없음 error-code.tag-not-found=태그를 찾을 수 없음 diff --git a/all-docs-bootstrap/src/main/resources/i18n/messages_zh_CN.properties b/all-docs-bootstrap/src/main/resources/i18n/messages_zh_CN.properties index b66399c..db4ff57 100644 --- a/all-docs-bootstrap/src/main/resources/i18n/messages_zh_CN.properties +++ b/all-docs-bootstrap/src/main/resources/i18n/messages_zh_CN.properties @@ -46,6 +46,9 @@ error-code.permission-denied=权限被拒绝 error-code.invalid-permission=无效权限 error-code.role-not-found=角色未找到 +# 收藏相关 +error-code.document-already-collected=文档已收藏 + # 分类和标签相关 error-code.category-not-found=分类未找到 error-code.tag-not-found=标签未找到 diff --git a/all-docs-bootstrap/src/main/resources/sql/init.sql b/all-docs-bootstrap/src/main/resources/sql/init.sql index 13a1e7f..0bab123 100644 --- a/all-docs-bootstrap/src/main/resources/sql/init.sql +++ b/all-docs-bootstrap/src/main/resources/sql/init.sql @@ -87,6 +87,7 @@ CREATE TABLE `comment` ( `user_name` VARCHAR(100) DEFAULT NULL COMMENT '用户名', `content` VARCHAR(500) NOT NULL COMMENT '评论内容', `doc_id` VARCHAR(64) NOT NULL COMMENT '文档ID', + `doc_name` VARCHAR(255) DEFAULT NULL COMMENT '文档名称', `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_date` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX `idx_doc_id` (`doc_id`), diff --git a/all-docs-common/pom.xml b/all-docs-common/pom.xml index d092bea..c9a44fd 100644 --- a/all-docs-common/pom.xml +++ b/all-docs-common/pom.xml @@ -59,5 +59,9 @@ spring-boot-starter-test test + + jakarta.persistence + jakarta.persistence-api + diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java b/all-docs-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java index 47c2309..d6775d9 100644 --- a/all-docs-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java @@ -5,10 +5,10 @@ * 统一管理存储路径前缀 * * 存储结构: - * - documents/{uniqueKey} - 用户上传的文档原文 + * - documents/{md5}_{filename} - 用户上传的文档原文 + * - document-texts/{md5}_{filename}.txt - 文档提取的文本内容 * - thumbs/{uniqueKey} - 文档缩略图 * - previews/{uniqueKey} - 文档预览图 - * - texts/{uniqueKey} - 文档提取的文本内容 * - avatars/{username}/{filename} - 用户头像 */ public final class StorageConstants { @@ -17,15 +17,21 @@ private StorageConstants() {} /** * 文档存储路径前缀 - * 完整路径格式: documents/{uniqueKey} + * 完整路径格式: documents/{md5}_{filename} */ public static final String DOCUMENTS = "documents/"; + /** + * 文档文本存储路径前缀 + * 完整路径格式: document-texts/{md5}_{filename}.txt + */ + public static final String DOCUMENT_TEXTS = "document-texts/"; + /** * 缩略图存储路径前缀 - * 完整路径格式: thumbs/{uniqueKey} + * 完整路径格式: thumbnails/{uniqueKey} */ - public static final String THUMBS = "thumbs/"; + public static final String THUMBNAILS = "thumbnails/"; /** * 头像存储路径前缀 @@ -47,20 +53,39 @@ private StorageConstants() {} /** * 生成文档存储路径 - * @param uniqueKey 唯一标识符(UUID) - * @return documents/{uniqueKey} + * @param objectKey 存储对象key (md5_filename) + * @return documents/{objectKey} */ - public static String documentPath(String uniqueKey) { - return DOCUMENTS + uniqueKey; + public static String documentPath(String objectKey) { + return DOCUMENTS + objectKey; + } + + /** + * 生成文档文本存储路径 + * @param objectKey 存储对象key (md5_filename) + * @return document-texts/{objectKey}.txt + */ + public static String documentTextPath(String objectKey) { + return DOCUMENT_TEXTS + objectKey + ".txt"; } /** * 生成缩略图存储路径 * @param uniqueKey 唯一标识符(UUID) - * @return thumbs/{uniqueKey} + * @return thumbnails/{uniqueKey} */ public static String thumbPath(String uniqueKey) { - return THUMBS + uniqueKey; + return THUMBNAILS + uniqueKey; + } + + /** + * 生成缩略图存储路径,带文件扩展名 + * @param uniqueKey 唯一标识符(UUID) + * @param format 文件格式,如 png、jpg、jpeg + * @return thumbnails/{uniqueKey}.{format} + */ + public static String thumbPath(String uniqueKey, String format) { + return THUMBNAILS + uniqueKey + "." + format; } /** @@ -90,4 +115,23 @@ public static String textPath(String uniqueKey) { public static String previewPath(String uniqueKey) { return PREVIEWS + uniqueKey; } + + /** + * 生成预览文件存储路径 + * @param objectKey 存储对象key (md5_filename) + * @return previews/{objectKey} + */ + public static String previewPathWithName(String objectKey) { + return PREVIEWS + objectKey; + } + + /** + * 生成预览文件存储路径,带文件扩展名 + * @param objectKey 存储对象key (md5_filename) + * @param format 文件格式,如 png、jpg、jpeg + * @return previews/{objectKey}.{format} + */ + public static String previewPath(String objectKey, String format) { + return PREVIEWS + objectKey + "." + format; + } } diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/DocStateEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/DocStateEnumConverter.java new file mode 100644 index 0000000..04bbdf7 --- /dev/null +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/DocStateEnumConverter.java @@ -0,0 +1,34 @@ +package com.jiaruiblog.common.converter; + +import com.jiaruiblog.common.enums.DocStateEnum; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA Converter for DocStateEnum + * Converts between Integer (database) and DocStateEnum (Java) + */ +@Converter(autoApply = true) +public class DocStateEnumConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(DocStateEnum attribute) { + if (attribute == null) { + return null; + } + return attribute.getCode(); + } + + @Override + public DocStateEnum convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + for (DocStateEnum enumValue : DocStateEnum.values()) { + if (enumValue.getCode().equals(dbData)) { + return enumValue; + } + } + return null; + } +} diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/PermissionEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/PermissionEnumConverter.java new file mode 100644 index 0000000..4656386 --- /dev/null +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/PermissionEnumConverter.java @@ -0,0 +1,34 @@ +package com.jiaruiblog.common.converter; + +import com.jiaruiblog.common.enums.PermissionEnum; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA Converter for PermissionEnum + * Converts between Integer (database) and PermissionEnum (Java) + */ +@Converter(autoApply = true) +public class PermissionEnumConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(PermissionEnum attribute) { + if (attribute == null) { + return null; + } + return attribute.getCode(); + } + + @Override + public PermissionEnum convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + for (PermissionEnum enumValue : PermissionEnum.values()) { + if (enumValue.getCode().equals(dbData)) { + return enumValue; + } + } + return null; + } +} diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/RedisActionEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/RedisActionEnumConverter.java new file mode 100644 index 0000000..71e4a0f --- /dev/null +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/RedisActionEnumConverter.java @@ -0,0 +1,34 @@ +package com.jiaruiblog.common.converter; + +import com.jiaruiblog.common.enums.RedisActionEnum; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA Converter for RedisActionEnum + * Converts between Integer (database) and RedisActionEnum (Java) + */ +@Converter(autoApply = true) +public class RedisActionEnumConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(RedisActionEnum attribute) { + if (attribute == null) { + return null; + } + return attribute.getCode(); + } + + @Override + public RedisActionEnum convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + for (RedisActionEnum enumValue : RedisActionEnum.values()) { + if (enumValue.getCode().equals(dbData)) { + return enumValue; + } + } + return null; + } +} diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringCodeToEnumConverterFactory.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringCodeToEnumConverterFactory.java index 8b54f8b..4664501 100644 --- a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringCodeToEnumConverterFactory.java +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringCodeToEnumConverterFactory.java @@ -14,9 +14,9 @@ * @Date 2022/6/19 5:08 下午 * @Version 1.0 **/ -public class StringCodeToEnumConverterFactory implements ConverterFactory { +public class StringCodeToEnumConverterFactory implements ConverterFactory> { private static final Map CONVERTERS = - Collections.unmodifiableMap(new ConcurrentHashMap<>()); + new ConcurrentHashMap<>(); /** * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现 @@ -25,10 +25,11 @@ public class StringCodeToEnumConverterFactory implements ConverterFactory Converter getConverter(Class targetType) { + @SuppressWarnings("unchecked") + public > Converter getConverter(Class targetType) { Converter converter = CONVERTERS.get(targetType); if (converter == null) { - converter = new StringToEnumConverter<>(targetType); + converter = (Converter) new StringToEnumConverter(targetType); CONVERTERS.put(targetType, converter); } return converter; diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringToEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringToEnumConverter.java index 3978ab5..1e83cdb 100644 --- a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringToEnumConverter.java +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/StringToEnumConverter.java @@ -13,13 +13,16 @@ * @Date 2022/6/19 5:07 下午 * @Version 1.0 **/ -public class StringToEnumConverter implements Converter { +public class StringToEnumConverter & BaseEnum> implements Converter { private Map enumMap = new ConcurrentHashMap<>(); public StringToEnumConverter(Class enumType) { T[] enums = enumType.getEnumConstants(); for (T e : enums) { - enumMap.put(e.getCode().toString(), e); + enumMap.put(e.name(), e); + if (e.getCode() != null) { + enumMap.put(e.getCode().toString(), e); + } } } diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbSizeEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbSizeEnumConverter.java new file mode 100644 index 0000000..3e8e040 --- /dev/null +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbSizeEnumConverter.java @@ -0,0 +1,34 @@ +package com.jiaruiblog.common.converter; + +import com.jiaruiblog.common.enums.ThumbSizeEnum; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA Converter for ThumbSizeEnum + * Converts between Integer (database) and ThumbSizeEnum (Java) + */ +@Converter(autoApply = true) +public class ThumbSizeEnumConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(ThumbSizeEnum attribute) { + if (attribute == null) { + return null; + } + return attribute.getCode(); + } + + @Override + public ThumbSizeEnum convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + for (ThumbSizeEnum enumValue : ThumbSizeEnum.values()) { + if (enumValue.getCode().equals(dbData)) { + return enumValue; + } + } + return null; + } +} diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbnailEnumConverter.java b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbnailEnumConverter.java new file mode 100644 index 0000000..27e6ea6 --- /dev/null +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbnailEnumConverter.java @@ -0,0 +1,34 @@ +package com.jiaruiblog.common.converter; + +import com.jiaruiblog.common.enums.ThumbnailEnum; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +/** + * JPA Converter for ThumbnailEnum + * Converts between Integer (database) and ThumbnailEnum (Java) + */ +@Converter(autoApply = true) +public class ThumbnailEnumConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(ThumbnailEnum attribute) { + if (attribute == null) { + return null; + } + return attribute.getCode(); + } + + @Override + public ThumbnailEnum convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + for (ThumbnailEnum enumValue : ThumbnailEnum.values()) { + if (enumValue.getCode().equals(dbData)) { + return enumValue; + } + } + return null; + } +} diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/enums/PermissionEnum.java b/all-docs-common/src/main/java/com/jiaruiblog/common/enums/PermissionEnum.java index 62a9055..fa5a0d8 100644 --- a/all-docs-common/src/main/java/com/jiaruiblog/common/enums/PermissionEnum.java +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/enums/PermissionEnum.java @@ -27,10 +27,6 @@ public Integer getCode() { return code; } - public String getMsg() { - return msg; - } - public static PermissionEnum getRoleByName(String name) { if (StringUtils.isEmpty(name)) { return null; diff --git a/all-docs-common/src/main/java/com/jiaruiblog/common/exception/ErrorCode.java b/all-docs-common/src/main/java/com/jiaruiblog/common/exception/ErrorCode.java index eae5a8f..a0a3f69 100644 --- a/all-docs-common/src/main/java/com/jiaruiblog/common/exception/ErrorCode.java +++ b/all-docs-common/src/main/java/com/jiaruiblog/common/exception/ErrorCode.java @@ -37,6 +37,9 @@ public enum ErrorCode { OPERATE_FAILED(1203, "error-code.operate-failed"), DATA_IS_EMPTY(1204, "error-code.data-is-empty"), + // Collect Related + DOCUMENT_ALREADY_COLLECTED(1205, "error-code.document-already-collected"), + // File Related FILE_NOT_FOUND(2001, "error-code.file-not-found"), FILE_UPLOAD_FAILED(2002, "error-code.file-upload-failed"), @@ -116,4 +119,4 @@ public String getMessage(MessageSource messageSource, Locale locale, Object... a this.messageKey, locale)); } -} \ No newline at end of file +} diff --git a/all-docs-domain/pom.xml b/all-docs-domain/pom.xml index b1ce3bb..0600cc2 100644 --- a/all-docs-domain/pom.xml +++ b/all-docs-domain/pom.xml @@ -75,7 +75,6 @@ org.springframework.security spring-security-crypto - provided diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/bo/UserBO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/bo/UserBO.java index 55389bd..0b6a652 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/bo/UserBO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/bo/UserBO.java @@ -27,6 +27,8 @@ public class UserBO { private String mail; + private String nickname; + private Boolean male = false; private String description; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/data/Event.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/data/Event.java index 1679acd..64c2c7f 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/data/Event.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/data/Event.java @@ -1,6 +1,7 @@ package com.jiaruiblog.domain.entity.data; import lombok.Builder; +import lombok.Builder.Default; import lombok.Getter; import java.util.HashMap; @@ -26,6 +27,7 @@ public class Event { private String entityType; // 事件发生在哪种类型上 private String entityId; // 事件发生在的实体的id private String entityUserId; //事件发生的实体对应的作者的id + @Default private Map data = new HashMap<>(); public String getTopic() { diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/BasicRegistryDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/BasicRegistryDTO.java new file mode 100644 index 0000000..2725eaf --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/BasicRegistryDTO.java @@ -0,0 +1,48 @@ +package com.jiaruiblog.domain.entity.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jiaruiblog.common.MessageConstant; +import com.jiaruiblog.common.RegexConstant; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * @ClassName BasicRegistryDTO + * @Description 用户注册实体类(简化版 - 注册时仅需账号密码) + * @author luojiarui + * @Date 2026/4/25 + * @Version 1.0 + **/ +@Schema(name = "用户注册对象(简化版)") +@Data +public class BasicRegistryDTO { + + @Schema(description = "用户名", minLength = 3, maxLength = 32, required = true) + @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) + @Size(min = 3, max = 32, message = MessageConstant.PARAMS_LENGTH_REQUIRED) + @Pattern(regexp = RegexConstant.NUM_WORD_REG, message = MessageConstant.PARAMS_FORMAT_ERROR) + String username; + + @Schema(description = "用户密码", minLength = 3, maxLength = 32, required = true) + @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) + @Size(min = 3, max = 32, message = MessageConstant.PARAMS_LENGTH_REQUIRED) + @Pattern(regexp = RegexConstant.NUM_WORD_REG, message = MessageConstant.PARAMS_FORMAT_ERROR) + String password; + + @Autowired + @JsonIgnore + @Schema(hidden = true) + private BCryptPasswordEncoder passwordEncoder; + + public String getEncodePassword() { + if (password == null) { + return ""; + } + return passwordEncoder.encode(password); + } +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentListDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentListDTO.java index 7e6a29e..7d8b17e 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentListDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentListDTO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.EqualsAndHashCode; /** @@ -15,6 +16,7 @@ **/ @Schema(name = "根据文档信息查询所属的文档评论") @Data +@EqualsAndHashCode(callSuper = false) public class CommentListDTO extends BasePageDTO { @Schema(description = "文档主键", required = true) diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentWithUserDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentWithUserDTO.java index b4c47c5..9ef2ab0 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentWithUserDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/CommentWithUserDTO.java @@ -1,6 +1,7 @@ package com.jiaruiblog.domain.entity.dto; import lombok.Data; +import lombok.EqualsAndHashCode; import java.util.Date; @@ -12,6 +13,7 @@ * @Version 1.0 **/ @Data +@EqualsAndHashCode(callSuper = false) public class CommentWithUserDTO extends CommentDTO { private String id; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/DocumentDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/DocumentDTO.java index bc7c74e..2c93849 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/DocumentDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/DocumentDTO.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.EqualsAndHashCode; /** @@ -16,6 +17,7 @@ **/ @Schema(description = "文档查询对象") @Data +@EqualsAndHashCode(callSuper = false) public class DocumentDTO extends BasePageDTO{ @Schema(description = "过滤类型", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/FileDocumentDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/FileDocumentDTO.java index a52ea9a..71ed9a8 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/FileDocumentDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/FileDocumentDTO.java @@ -38,7 +38,7 @@ public class FileDocumentDTO { // true 正在审核;false 审核完毕 - private boolean reviewing = true; + private Boolean reviewing = true; private String userId; } \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/QueryDocByTagCateDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/QueryDocByTagCateDTO.java index 77cade7..b03eccb 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/QueryDocByTagCateDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/QueryDocByTagCateDTO.java @@ -1,6 +1,7 @@ package com.jiaruiblog.domain.entity.dto; import lombok.Data; +import lombok.EqualsAndHashCode; /** * @ClassName QueryDocByTagCateDTO @@ -10,6 +11,7 @@ * @Version 1.0 **/ @Data +@EqualsAndHashCode(callSuper = false) public class QueryDocByTagCateDTO extends BasePageDTO{ String cateId; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RefuseBatchDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RefuseBatchDTO.java index 5384ab7..5debe22 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RefuseBatchDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RefuseBatchDTO.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; +import lombok.EqualsAndHashCode; /** @@ -16,6 +17,7 @@ * @Version 1.0 **/ @Data +@EqualsAndHashCode(callSuper = false) public class RefuseBatchDTO extends BatchIdDTO{ @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RegistryUserDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RegistryUserDTO.java index b6debdc..9a4b444 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RegistryUserDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/RegistryUserDTO.java @@ -1,5 +1,6 @@ package com.jiaruiblog.domain.entity.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.jiaruiblog.common.MessageConstant; import com.jiaruiblog.common.RegexConstant; import io.swagger.v3.oas.annotations.media.Schema; @@ -50,6 +51,8 @@ public class RegistryUserDTO { String nickname; @Autowired + @JsonIgnore + @Schema(hidden = true) private BCryptPasswordEncoder passwordEncoder; public String getEncodePassword() { diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchQuery.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchQuery.java new file mode 100644 index 0000000..f69ad56 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchQuery.java @@ -0,0 +1,48 @@ +package com.jiaruiblog.domain.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @ClassName SearchQuery + * @Description 文档搜索请求参数类 + * @author luojiarui + * @Date 2026/4/27 + * @Version 1.0 + **/ +@Schema(name = "SearchQuery", description = "文档搜索请求参数") +@Data +public class SearchQuery { + + @Schema(description = "搜索关键词,默认空字符串", example = "") + private String keyword = ""; + + @Schema(description = "是否全文检索", example = "false") + private Boolean fullText = false; + + @Schema(description = "是否分词", example = "false") + private Boolean segment = false; + + @Schema(description = "搜索类型:all/name/description", example = "all") + private String searchType = "all"; + + @Schema(description = "标签筛选数组", example = "[\"pdf\", \"docx\"]") + private List tags; + + @Schema(description = "分类名称筛选", example = "") + private String category = ""; + + @Schema(description = "排序字段:name/size/type/category/createTime", example = "createTime") + private String sortField = "createTime"; + + @Schema(description = "排序方向:asc/desc", example = "desc") + private String sortOrder = "desc"; + + @Schema(description = "页码,从1开始", example = "1") + private Integer page = 1; + + @Schema(description = "每页条数", example = "20") + private Integer pageSize = 20; +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchResultItem.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchResultItem.java new file mode 100644 index 0000000..033a352 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchResultItem.java @@ -0,0 +1,37 @@ +package com.jiaruiblog.domain.entity.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @ClassName SearchResultItem + * @Description 搜索结果项,包含ID和高亮片段 + * @author luojiarui + * @Date 2026/4/28 + * @Version 1.0 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResultItem { + + /** + * 文档ID + */ + private String id; + + /** + * 高亮片段列表(最多3段,可能是content、name、tagNames或categoryName中的匹配内容) + */ + private List highlightFragments; + + /** + * 高亮来源:content/name/tagNames/categoryName + */ + private String highlightSource; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/UserDTO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/UserDTO.java index 260bb2a..133bfca 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/UserDTO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/UserDTO.java @@ -1,13 +1,19 @@ package com.jiaruiblog.domain.entity.dto; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jiaruiblog.common.MessageConstant; +import com.jiaruiblog.common.RegexConstant; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import lombok.Data; import java.util.Date; /** * @ClassName UserDTO - * @Description 用户DTO + * @Description 用户DTO(更新资料时使用) * @author luojiarui **/ @Data @@ -18,10 +24,21 @@ public class UserDTO { @JsonIgnore private String password; + @Schema(description = "用户手机号", required = true) + @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) + @Pattern(regexp = RegexConstant.PHONE_REG, message = MessageConstant.PARAMS_FORMAT_ERROR) private String phone; + @Schema(description = "用户邮箱", required = true) + @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) + @Pattern(regexp = RegexConstant.MAIL_REG, message = MessageConstant.PARAMS_FORMAT_ERROR) private String mail; + @Schema(description = "用户昵称", required = true) + @NotNull(message = MessageConstant.PARAMS_IS_NOT_NULL) + @Size(min = 3, max = 32, message = MessageConstant.PARAMS_LENGTH_REQUIRED) + private String nickname; + private boolean male = false; private String description; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/CollectDocRelationship.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/CollectDocRelationship.java index c6f780c..627d175 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/CollectDocRelationship.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/CollectDocRelationship.java @@ -1,18 +1,17 @@ package com.jiaruiblog.domain.entity.po; import com.jiaruiblog.common.enums.RedisActionEnum; -import jakarta.persistence.Id; import lombok.Data; import java.util.Date; -/**用户收藏文档的关系表 +/** + * 用户收藏文档的关系表 * @author luojiarui **/ @Data public class CollectDocRelationship { - @Id private String id; private RedisActionEnum redisActionEnum; @@ -25,4 +24,4 @@ public class CollectDocRelationship { private Date updateDate; -} \ No newline at end of file +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Comment.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Comment.java index 4dcb8e4..996b35b 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Comment.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Comment.java @@ -1,7 +1,5 @@ package com.jiaruiblog.domain.entity.po; -import com.jiaruiblog.common.MessageConstant; -import jakarta.validation.constraints.Size; import lombok.Data; import java.util.Date; @@ -14,17 +12,18 @@ public class Comment { private String id; - private Long createUser; + private String createUser; private String userId; private String userName; - @Size(min = 1, max = 140, message = MessageConstant.PARAMS_LENGTH_REQUIRED) private String content; private String docId; + private String docName; + private Date createDate; private Date updateDate; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocLog.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocLog.java index 9486e81..41ad24c 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocLog.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocLog.java @@ -1,7 +1,5 @@ package com.jiaruiblog.domain.entity.po; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import lombok.Data; import java.util.Date; @@ -10,10 +8,8 @@ * @author luojiarui **/ @Data -@Table(name = "doc_log") public class DocLog { - @Id private String id; private String userId; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocReview.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocReview.java index 3e1c732..b227a39 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocReview.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/DocReview.java @@ -40,22 +40,22 @@ public class DocReview { /** * 评审是否通过的状态 */ - private boolean checkState; + private Boolean checkState; /** * 用户是否已读的状态 */ - private boolean readState; + private Boolean readState; /** * 用户是否删除的状态 */ - private boolean userRemove; + private Boolean userRemove; /** * 管理员删除评审意见 **/ - private boolean adminRemove; + private Boolean adminRemove; /** * 评审意见 diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/FileDocument.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/FileDocument.java index f866c26..3538323 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/FileDocument.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/FileDocument.java @@ -1,9 +1,6 @@ package com.jiaruiblog.domain.entity.po; import com.jiaruiblog.common.enums.DocStateEnum; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; import lombok.Data; import java.util.Date; @@ -17,7 +14,6 @@ public class FileDocument { /** * 主键 */ - @Id private String id; /** @@ -80,7 +76,6 @@ public class FileDocument { /** * 文档的状态 **/ - @Enumerated(EnumType.STRING) private DocStateEnum docState = DocStateEnum.WAIT; /** @@ -89,7 +84,7 @@ public class FileDocument { private String errorMsg; // true 正在审核;false 审核完毕 - private boolean reviewing = true; + private Boolean reviewing = true; private String userId; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/LikeDocRelationship.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/LikeDocRelationship.java index 8435185..8a9e354 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/LikeDocRelationship.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/LikeDocRelationship.java @@ -1,5 +1,6 @@ package com.jiaruiblog.domain.entity.po; +import com.jiaruiblog.common.enums.RedisActionEnum; import lombok.Data; import java.util.Date; @@ -37,10 +38,10 @@ public class LikeDocRelationship { private Date createDate; /** - * 实体类型常量 + * 实体类型常量(委托给 RedisActionEnum 保持一致) */ - public static final int TYPE_LIKE = 0; - public static final int TYPE_COLLECT = 1; + public static final int TYPE_LIKE = RedisActionEnum.LIKE.getCode(); + public static final int TYPE_COLLECT = RedisActionEnum.COLLECT.getCode(); /** * 获取文档ID(兼容方法) @@ -50,11 +51,4 @@ public String getDocId() { return this.entityId; } - public String getUserId() { - return this.userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } } diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/SearchDocument.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/SearchDocument.java index 8cc090e..91e2930 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/SearchDocument.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/SearchDocument.java @@ -7,6 +7,8 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import java.util.List; + /** * Elasticsearch 文档实体,用于全文检索索引 * @@ -46,4 +48,10 @@ public class SearchDocument { */ @Field(type = FieldType.Text, analyzer = "ik_smart") private String content; + + @Field(type = FieldType.Text, analyzer = "ik_smart") + private List tagNames; + + @Field(type = FieldType.Text, analyzer = "ik_smart") + private String categoryName; } diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Tag.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Tag.java index 3a94491..db84c31 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Tag.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Tag.java @@ -13,6 +13,8 @@ public class Tag { private String name; + private String color; + private Date createDate; private Date updateDate; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Thumbnail.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Thumbnail.java index 34db119..d830c87 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Thumbnail.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Thumbnail.java @@ -2,10 +2,10 @@ import com.jiaruiblog.common.enums.ThumbSizeEnum; import com.jiaruiblog.common.enums.ThumbnailEnum; -import jakarta.persistence.Id; import lombok.Data; -/**缩略图相关的类 +/** + * 缩略图相关的类 **/ @Data public class Thumbnail { @@ -13,7 +13,6 @@ public class Thumbnail { /** * 缩略图id */ - @Id private String id; /** @@ -37,4 +36,4 @@ public class Thumbnail { private ThumbSizeEnum thumbSizeEnum; -} \ No newline at end of file +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/User.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/User.java index 162c31c..f0d4544 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/User.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/User.java @@ -2,12 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.jiaruiblog.common.enums.PermissionEnum; -import jakarta.persistence.Column; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -18,16 +12,13 @@ /** * @author luojiarui **/ -@Table(name = "user") @Data @AllArgsConstructor @NoArgsConstructor public class User { - @Id private String id; - @NotBlank(message = "非空") private String username; @JsonIgnore @@ -49,8 +40,6 @@ public class User { // 封禁状态 private Boolean banning = false; - @Enumerated(EnumType.STRING) - @Column(name = "permission_enum") private PermissionEnum permissionEnum; private String nickname; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/CategoryDistVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/CategoryDistVO.java new file mode 100644 index 0000000..e170b9e --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/CategoryDistVO.java @@ -0,0 +1,9 @@ +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class CategoryDistVO { + private String category; + private Long count; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocLogVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocLogVO.java index acd8c7a..77e7b14 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocLogVO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocLogVO.java @@ -16,6 +16,8 @@ public class DocLogVO { private String id; + private String userId; + private String userName; private String action; diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocSearchVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocSearchVO.java new file mode 100644 index 0000000..e9c93ac --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocSearchVO.java @@ -0,0 +1,55 @@ +package com.jiaruiblog.domain.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * @ClassName DocSearchVO + * @Description 文档搜索响应VO类 + * @author luojiarui + * @Date 2026/4/27 + * @Version 1.0 + **/ +@Schema(name = "DocSearchVO", description = "文档搜索响应VO") +@Data +public class DocSearchVO { + + @Schema(description = "文档ID") + private String id; + + @Schema(description = "文档名称(带后缀)", example = "项目需求文档.pdf") + private String name; + + @Schema(description = "文档类型(pdf/docx/xlsx/pptx/image/zip)", example = "pdf") + private String type; + + @Schema(description = "文档大小(字节)", example = "2048576") + private Long size; + + @Schema(description = "文档大小(格式化,如 \"2 MB\")", example = "2 MB") + private String sizeDisplay; + + @Schema(description = "文档描述", example = "2024年Q1项目需求文档") + private String description; + + @Schema(description = "所属分类名称", example = "需求文档") + private String category; + + @Schema(description = "标签列表") + private List tags; + + @Schema(description = "当前用户是否已点赞", example = "false") + private Boolean liked; + + @Schema(description = "当前用户是否已收藏", example = "true") + private Boolean collected; + + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocTypeDistVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocTypeDistVO.java new file mode 100644 index 0000000..3421a4a --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocTypeDistVO.java @@ -0,0 +1,9 @@ +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class DocTypeDistVO { + private String type; + private Long count; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java index d9ffdd7..2be9ad7 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java @@ -27,6 +27,8 @@ public class DocumentVO { private Long commentNum; + private Long likeNum; + private CategoryVO categoryVO; private List tagVOList; @@ -45,6 +47,6 @@ public class DocumentVO { private java.util.Date createTime; - private List pageVOList; +// private List pageVOList; } \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/HotDocVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/HotDocVO.java new file mode 100644 index 0000000..f7bf47c --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/HotDocVO.java @@ -0,0 +1,10 @@ +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class HotDocVO { + private String id; + private String title; + private Long viewCount; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchHotWordVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchHotWordVO.java new file mode 100644 index 0000000..e1292d9 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchHotWordVO.java @@ -0,0 +1,9 @@ +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class SearchHotWordVO { + private String keyword; + private Long count; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchResultVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchResultVO.java new file mode 100644 index 0000000..cedf330 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchResultVO.java @@ -0,0 +1,33 @@ +package com.jiaruiblog.domain.entity.vo; + +import com.jiaruiblog.domain.entity.dto.SearchResultItem; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @ClassName SearchResultVO + * @Description 搜索结果 VO,包含总数和结果列表 + * @author luojiarui + * @Date 2026/4/28 + * @Version 1.0 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResultVO { + + /** + * 搜索结果总数 + */ + private long total; + + /** + * 搜索结果列表 + */ + private List items; +} \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/StatsVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/StatsVO.java index dabba14..f91538c 100644 --- a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/StatsVO.java +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/StatsVO.java @@ -20,5 +20,12 @@ public class StatsVO { private Long commentNum; + private Long userNum; + + private Long downloadNum; + + private Long searchNum; + + private Long viewNum; } \ No newline at end of file diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/TagColorVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/TagColorVO.java new file mode 100644 index 0000000..4e5b706 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/TagColorVO.java @@ -0,0 +1,22 @@ +package com.jiaruiblog.domain.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @ClassName TagColorVO + * @Description 标签颜色VO类 + * @author luojiarui + * @Date 2026/4/27 + * @Version 1.0 + **/ +@Schema(name = "TagColorVO", description = "标签颜色VO") +@Data +public class TagColorVO { + + @Schema(description = "标签名称", example = "重要") + private String name; + + @Schema(description = "标签颜色", example = "red") + private String color; +} diff --git a/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/UserActivityVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/UserActivityVO.java new file mode 100644 index 0000000..e50b7e5 --- /dev/null +++ b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/UserActivityVO.java @@ -0,0 +1,10 @@ +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class UserActivityVO { + private String month; + private Long activeUsers; + private Long totalUsers; +} \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/ElasticSearchConfig.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/ElasticSearchConfig.java index e56459a..6e8911f 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/ElasticSearchConfig.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/ElasticSearchConfig.java @@ -1,32 +1,51 @@ -package com.jiaruiblog.config; +package com.jiaruiblog.infrastructure.config; +import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Configuration; /** * @ClassName ElasticSearchConfig - * @Description ES的配置信息 - * https://blog.csdn.net/wdz985721191/article/details/122866091 + * @Description ES配置信息,支持账号密码认证 * @author luojiarui * @Date 2022/7/12 10:50 下午 * @Version 1.0 **/ -@Component +@Slf4j +@Configuration public class ElasticSearchConfig { - @Value("${cloud.elasticsearch.host}") + @Value("${cloud.elasticsearch.host:192.168.1.29}") private String esHost; - @Value("${cloud.elasticsearch.port}") + @Value("${cloud.elasticsearch.port:1200}") private int esPort; - @Bean(destroyMethod = "close") + @Value("${cloud.elasticsearch.username:elastic}") + private String esUsername; + + @Value("${cloud.elasticsearch.password:infini_rag_flow}") + private String esPassword; + + @Bean public RestClient restClient() { - return RestClient.builder( - new HttpHost(esHost, esPort) - ).build(); + log.info("[ES Config] Creating RestClient: host={}, port={}, username={}", esHost, esPort, esUsername); + RestClientBuilder builder = RestClient.builder(new HttpHost(esHost, esPort)); + if (esUsername != null && !esUsername.isEmpty()) { + log.info("[ES Config] Applying basic auth for user: {}", esUsername); + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(esUsername, esPassword)); + builder.setHttpClientConfigCallback(httpClientBuilder -> + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + return builder.build(); } -} \ No newline at end of file +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/I18nConfig.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/I18nConfig.java index fbba640..fbef411 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/I18nConfig.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/I18nConfig.java @@ -12,7 +12,7 @@ public class I18nConfig { public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); - messageSource.setBasename("classpath:messages"); + messageSource.setBasename("classpath:i18n/messages"); messageSource.setDefaultEncoding("UTF-8"); messageSource.setCacheSeconds(3600); // 1小时刷新 return messageSource; diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.java index ba9e35e..0697157 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.java @@ -1,10 +1,16 @@ package com.jiaruiblog.infrastructure.config.datasource; +import com.jiaruiblog.common.enums.DocStateEnum; +import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.common.enums.ThumbSizeEnum; +import com.jiaruiblog.common.enums.ThumbnailEnum; +import com.jiaruiblog.infrastructure.config.mybatis.BooleanTypeHandler; +import com.jiaruiblog.infrastructure.config.mybatis.DocStateEnumTypeHandler; +import com.jiaruiblog.infrastructure.config.mybatis.EnumTypeHandler; +import com.jiaruiblog.infrastructure.config.mybatis.RedisActionEnumTypeHandler; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; @@ -15,12 +21,6 @@ @MapperScan("com.jiaruiblog.infrastructure.repository.mysql") public class MyBatisConfig { - @Bean - @ConfigurationProperties(prefix = "spring.datasource") - public DataSource dataSource() { - return DataSourceBuilder.create().build(); - } - @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); @@ -28,6 +28,14 @@ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Excepti factory.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml") ); + factory.setTypeHandlers( + new BooleanTypeHandler(), + new EnumTypeHandler<>(PermissionEnum.class), + new DocStateEnumTypeHandler(), + new RedisActionEnumTypeHandler(), + new EnumTypeHandler<>(ThumbnailEnum.class), + new EnumTypeHandler<>(ThumbSizeEnum.class) + ); return factory.getObject(); } -} \ No newline at end of file +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/minio/MinioConfig.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/minio/MinioConfig.java index 8199672..54bd48b 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/minio/MinioConfig.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/minio/MinioConfig.java @@ -17,10 +17,10 @@ public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; - @Value("${minio.accessKey}") + @Value("${minio.access-key}") private String accessKey; - @Value("${minio.secretKey}") + @Value("${minio.secret-key}") private String secretKey; @Bean diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/BooleanTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/BooleanTypeHandler.java new file mode 100644 index 0000000..d596eb7 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/BooleanTypeHandler.java @@ -0,0 +1,43 @@ +package com.jiaruiblog.infrastructure.config.mybatis; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * MyBatis Type Handler for Boolean fields mapped to MySQL TINYINT(1) + * MySQL JDBC driver returns TINYINT as java.lang.Integer, not java.lang.Boolean + * This handler converts Integer (0/1) to Boolean + * + * @author Jarrett Luo + * @version 1.0 + */ +public class BooleanTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException { + ps.setInt(i, parameter ? 1 : 0); + } + + @Override + public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException { + int value = rs.getInt(columnName); + return value != 0; + } + + @Override + public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + int value = rs.getInt(columnIndex); + return value != 0; + } + + @Override + public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + int value = cs.getInt(columnIndex); + return value != 0; + } +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/DocStateEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/DocStateEnumTypeHandler.java new file mode 100644 index 0000000..d63d173 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/DocStateEnumTypeHandler.java @@ -0,0 +1,16 @@ +package com.jiaruiblog.infrastructure.config.mybatis; + +import com.jiaruiblog.common.enums.DocStateEnum; + +/** + * DocStateEnum 的 MyBatis TypeHandler + * + * @author Jarrett Luo + * @version 1.0 + */ +public class DocStateEnumTypeHandler extends EnumTypeHandler { + + public DocStateEnumTypeHandler() { + super(DocStateEnum.class); + } +} \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/EnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/EnumTypeHandler.java index e077af7..66e6e6b 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/EnumTypeHandler.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/EnumTypeHandler.java @@ -16,7 +16,7 @@ * @author Jarrett Luo * @version 1.0 */ -public class EnumTypeHandler> extends BaseTypeHandler { +public class EnumTypeHandler & BaseEnum> extends BaseTypeHandler { private final Class enumClass; @@ -25,33 +25,33 @@ public EnumTypeHandler(Class enumClass) { } @Override - public void setNonNullParameter(PreparedStatement ps, int i, BaseEnum parameter, JdbcType jdbcType) throws SQLException { + public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override - public BaseEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { + public E getNullableResult(ResultSet rs, String columnName) throws SQLException { int code = rs.getInt(columnName); return code == 0 && rs.wasNull() ? null : toEnum(code); } @Override - public BaseEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int code = rs.getInt(columnIndex); return code == 0 && rs.wasNull() ? null : toEnum(code); } @Override - public BaseEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int code = cs.getInt(columnIndex); return code == 0 && cs.wasNull() ? null : toEnum(code); } - private BaseEnum toEnum(int code) { + private E toEnum(int code) { E[] constants = enumClass.getEnumConstants(); for (E constant : constants) { - if (((BaseEnum) constant).getCode().equals(code)) { - return (BaseEnum) constant; + if (constant.getCode().equals(code)) { + return constant; } } return null; diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/PermissionEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/PermissionEnumTypeHandler.java new file mode 100644 index 0000000..866e609 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/PermissionEnumTypeHandler.java @@ -0,0 +1,9 @@ +package com.jiaruiblog.infrastructure.config.mybatis; + +import com.jiaruiblog.common.enums.PermissionEnum; + +public class PermissionEnumTypeHandler extends EnumTypeHandler { + public PermissionEnumTypeHandler() { + super(PermissionEnum.class); + } +} \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/RedisActionEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/RedisActionEnumTypeHandler.java new file mode 100644 index 0000000..689b9d4 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/RedisActionEnumTypeHandler.java @@ -0,0 +1,16 @@ +package com.jiaruiblog.infrastructure.config.mybatis; + +import com.jiaruiblog.common.enums.RedisActionEnum; + +/** + * RedisActionEnum 的 MyBatis TypeHandler + * + * @author Jarrett Luo + * @version 1.0 + */ +public class RedisActionEnumTypeHandler extends EnumTypeHandler { + + public RedisActionEnumTypeHandler() { + super(RedisActionEnum.class); + } +} \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/ThumbSizeEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/ThumbSizeEnumTypeHandler.java new file mode 100644 index 0000000..e9a6045 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/ThumbSizeEnumTypeHandler.java @@ -0,0 +1,9 @@ +package com.jiaruiblog.infrastructure.config.mybatis; + +import com.jiaruiblog.common.enums.ThumbSizeEnum; + +public class ThumbSizeEnumTypeHandler extends EnumTypeHandler { + public ThumbSizeEnumTypeHandler() { + super(ThumbSizeEnum.class); + } +} \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/CommentRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/CommentRepository.java index 4add197..957fa34 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/CommentRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/CommentRepository.java @@ -7,7 +7,7 @@ public interface CommentRepository { - Comment save(Comment comment); + int save(Comment comment); Optional findById(String id); @@ -15,6 +15,8 @@ public interface CommentRepository { List findByUserId(String userId); + List findAll(); + List findByContentContaining(String keyword); long countByDocId(String docId); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocLogRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocLogRepository.java index 18df8cc..90157a5 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocLogRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocLogRepository.java @@ -7,7 +7,7 @@ public interface DocLogRepository { - DocLog save(DocLog docLog); + int save(DocLog docLog); Optional findById(String id); @@ -15,6 +15,8 @@ public interface DocLogRepository { List findByUserId(String userId); + List findAll(); + List findByAction(String action); long count(); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java index 92961b7..8d91827 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java @@ -6,6 +6,7 @@ import java.util.Date; import java.util.List; +import java.util.Map; /** *

@@ -22,6 +23,8 @@ public interface DocumentRepository { long count(); + long countWithFilter(String filterWord); + FileDocument findById(String fileDocumentId); List findByIdList(List docIdList); @@ -32,6 +35,8 @@ public interface DocumentRepository { List findByPageWithFussySearch(Integer pageNum, Integer pageSize, Sort sort, String keyWord); + List findByPageWithFilter(Integer pageNum, Integer pageSize, Sort sort, String filterWord); + List findByUserId(String userId, Integer pageNum, Integer pageSize, Sort sort); List findByUserIdAndNameContaining(String userId, String name, Integer pageNum, Integer pageSize, Sort sort); @@ -43,4 +48,16 @@ public interface DocumentRepository { List stats(Date startDate, Date endDate); List trend(Date startDate, Date endDate); + + List findByPageByTag(String tagId, int pageNum, int pageSize); + + List findByPageByCategory(String categoryId, int pageNum, int pageSize); + + long countByTagId(String tagId); + + long countByCategoryId(String categoryId); + + List> countByDocType(); + + List> countByCategory(); } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CateDocRelationshipMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CateDocRelationshipMapper.java index e3689cb..b985cdf 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CateDocRelationshipMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CateDocRelationshipMapper.java @@ -49,4 +49,6 @@ public interface CateDocRelationshipMapper { List findFileIdsByCategoryIds(@Param("categoryIds") List categoryIds); List findCategoryIdsByFileId(@Param("fileId") String fileId); + + List findByCategoryIdAndFileIds(@Param("categoryId") String categoryId, @Param("fileIds") List fileIds); } diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CategoryMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CategoryMybatisRepository.java index 3c25571..383dc2b 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CategoryMybatisRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CategoryMybatisRepository.java @@ -19,6 +19,9 @@ public class CategoryMybatisRepository implements CategoryRepository { @Autowired private CategoryMapper categoryMapper; + @Autowired + private CateDocRelationshipMapper cateDocRelationshipMapper; + @Override public void save(Category category) { categoryMapper.save(category); @@ -26,7 +29,7 @@ public void save(Category category) { @Override public void saveRelationship(CateDocRelationship relationship) { - // Relationship is handled via CateDocRelationshipMapper + cateDocRelationshipMapper.save(relationship); } @Override @@ -61,8 +64,7 @@ public List findAll(Sort sort) { @Override public List findRelationshipsByCategoryId(String categoryId, Sort sort) { - // Implemented via CateDocRelationshipMapper - return null; + return cateDocRelationshipMapper.findByCategoryId(categoryId); } @Override @@ -87,6 +89,6 @@ public List findByDocIdIn(List docIds) { @Override public List findByCategoryIdAndDocIdIn(String categoryId, List docIds) { - return null; + return cateDocRelationshipMapper.findByCategoryIdAndFileIds(categoryId, docIds); } } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMapper.java index 6ec2a0c..fd7b307 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMapper.java @@ -9,7 +9,7 @@ @Mapper public interface CommentMapper { - Comment save(Comment comment); + int save(Comment comment); Comment findById(@Param("id") String id); @@ -17,9 +17,17 @@ public interface CommentMapper { List findByUserId(@Param("userId") String userId); + List findAll(); + long countByDocId(@Param("docId") String docId); void deleteById(@Param("id") String id); void deleteByDocId(@Param("docId") String docId); + + List findByContentContaining(@Param("keyword") String keyword); + + void deleteAllByIdIn(@Param("idList") List idList); + + long count(); } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java new file mode 100644 index 0000000..2b9a9a3 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java @@ -0,0 +1,76 @@ +package com.jiaruiblog.infrastructure.repository.mysql; + +import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.infrastructure.repository.CommentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * MyBatis Comment Repository Implementation + * + * @author luojiarui + */ +@Repository +public class CommentMybatisRepository implements CommentRepository { + + @Autowired + private CommentMapper commentMapper; + + @Override + public int save(Comment comment) { + return commentMapper.save(comment); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(commentMapper.findById(id)); + } + + @Override + public List findByDocId(String docId) { + return commentMapper.findByDocId(docId); + } + + @Override + public List findByUserId(String userId) { + return commentMapper.findByUserId(userId); + } + + @Override + public List findAll() { + return commentMapper.findAll(); + } + + @Override + public List findByContentContaining(String keyword) { + return commentMapper.findByContentContaining(keyword); + } + + @Override + public long countByDocId(String docId) { + return commentMapper.countByDocId(docId); + } + + @Override + public void deleteById(String id) { + commentMapper.deleteById(id); + } + + @Override + public void deleteByDocId(String docId) { + commentMapper.deleteByDocId(docId); + } + + @Override + public void deleteAllByIdIn(List ids) { + commentMapper.deleteAllByIdIn(ids); + } + + @Override + public long count() { + return commentMapper.count(); + } +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMapper.java index a9c81da..5468936 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMapper.java @@ -9,7 +9,7 @@ @Mapper public interface DocLogMapper { - DocLog save(DocLog docLog); + int save(DocLog docLog); DocLog findById(@Param("id") String id); @@ -17,6 +17,8 @@ public interface DocLogMapper { List findByUserId(@Param("userId") String userId); + List findAll(); + List findByAction(@Param("action") String action); long count(); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java new file mode 100644 index 0000000..f9eef38 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java @@ -0,0 +1,66 @@ +package com.jiaruiblog.infrastructure.repository.mysql; + +import com.jiaruiblog.domain.entity.po.DocLog; +import com.jiaruiblog.infrastructure.repository.DocLogRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * MyBatis DocLog Repository Implementation + * + * @author luojiarui + */ +@Repository +public class DocLogMybatisRepository implements DocLogRepository { + + @Autowired + private DocLogMapper docLogMapper; + + @Override + public int save(DocLog docLog) { + return docLogMapper.save(docLog); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(docLogMapper.findById(id)); + } + + @Override + public List findByDocId(String docId) { + return docLogMapper.findByDocId(docId); + } + + @Override + public List findByUserId(String userId) { + return docLogMapper.findByUserId(userId); + } + + @Override + public List findAll() { + return docLogMapper.findAll(); + } + + @Override + public List findByAction(String action) { + return docLogMapper.findByAction(action); + } + + @Override + public long count() { + return docLogMapper.count(); + } + + @Override + public void deleteById(String id) { + docLogMapper.deleteById(id); + } + + @Override + public void deleteAllByIdIn(List ids) { + docLogMapper.deleteAllByIdIn(ids); + } +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMapper.java index 51e9ea6..bfb2025 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMapper.java @@ -9,14 +9,20 @@ @Mapper public interface DocReviewMapper { - DocReview save(DocReview docReview); + int save(DocReview docReview); void saveAll(@Param("list") List docReviews); long countByUserId(@Param("userId") String userId); + long countByUserIdWithAdmin(@Param("userId") String userId, @Param("isAdmin") boolean isAdmin); + List findByPage(@Param("offset") int offset, @Param("limit") int limit, @Param("userId") String userId); + List findByPageWithAdmin(@Param("offset") int offset, @Param("limit") int limit, @Param("userId") String userId, @Param("isAdmin") boolean isAdmin); + + long deleteByQuery(); + void deleteByIdList(@Param("idList") List idList); boolean existsByDocIdIn(@Param("docIdList") List docIdList); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMybatisRepository.java new file mode 100644 index 0000000..5d836dd --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMybatisRepository.java @@ -0,0 +1,63 @@ +package com.jiaruiblog.infrastructure.repository.mysql; + +import com.jiaruiblog.domain.entity.po.DocReview; +import com.jiaruiblog.infrastructure.repository.DocReviewRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * MyBatis DocReview Repository Implementation + * + * @author luojiarui + */ +@Repository +public class DocReviewMybatisRepository implements DocReviewRepository { + + @Autowired + private DocReviewMapper docReviewMapper; + + @Override + public DocReview save(DocReview docReview) { + docReviewMapper.save(docReview); + return docReview; + } + + @Override + public void saveAll(List docReviews) { + docReviewMapper.saveAll(docReviews); + } + + @Override + public long countByUserId(String userId) { + return docReviewMapper.countByUserId(userId); + } + + @Override + public long countByUserId(String userId, boolean isAdmin) { + return docReviewMapper.countByUserIdWithAdmin(userId, isAdmin); + } + + @Override + public List findByPage(Integer pageNum, Integer pageRows, String userId, boolean isAdmin) { + int offset = (pageNum != null && pageNum >= 0) ? pageNum : 0; + int limit = (pageRows != null && pageRows > 0) ? pageRows : 10; + return docReviewMapper.findByPageWithAdmin(offset, limit, userId, isAdmin); + } + + @Override + public long deleteByQuery() { + return docReviewMapper.deleteByQuery(); + } + + @Override + public void deleteByIdList(List docIds) { + docReviewMapper.deleteByIdList(docIds); + } + + @Override + public boolean existsByDocIdIn(List docIds) { + return docReviewMapper.existsByDocIdIn(docIds); + } +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java index 27a1076..d21e268 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java @@ -7,6 +7,7 @@ import java.util.Date; import java.util.List; +import java.util.Map; @Mapper public interface DocumentMapper { @@ -17,6 +18,8 @@ public interface DocumentMapper { long count(); + long countWithFilter(@Param("filterWord") String filterWord); + FileDocument findById(@Param("id") String id); List findByIdList(@Param("idList") List idList); @@ -27,6 +30,8 @@ public interface DocumentMapper { List findByPageWithFussySearch(@Param("offset") long offset, @Param("limit") int limit, @Param("keyWord") String keyWord); + List findByPageWithFilter(@Param("offset") long offset, @Param("limit") int limit, @Param("filterWord") String filterWord); + List findByUserId(@Param("userId") String userId, @Param("offset") long offset, @Param("limit") int limit); List findByUserIdAndNameContaining(@Param("userId") String userId, @Param("name") String name, @Param("offset") long offset, @Param("limit") int limit); @@ -38,4 +43,16 @@ public interface DocumentMapper { List stats(@Param("startDate") Date startDate, @Param("endDate") Date endDate); List trend(@Param("startDate") Date startDate, @Param("endDate") Date endDate); + + List findByPageByTag(@Param("tagId") String tagId, @Param("offset") long offset, @Param("limit") int limit); + + List findByPageByCategory(@Param("categoryId") String categoryId, @Param("offset") long offset, @Param("limit") int limit); + + long countByTagId(@Param("tagId") String tagId); + + long countByCategoryId(@Param("categoryId") String categoryId); + + List> countByDocType(); + + List> countByCategory(); } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMybatisRepository.java index ae8de56..763865d 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMybatisRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMybatisRepository.java @@ -9,6 +9,7 @@ import java.util.Date; import java.util.List; +import java.util.Map; /** * MyBatis Document Repository Implementation @@ -37,6 +38,11 @@ public long count() { return documentMapper.count(); } + @Override + public long countWithFilter(String filterWord) { + return documentMapper.countWithFilter(filterWord); + } + @Override public FileDocument findById(String fileDocumentId) { return documentMapper.findById(fileDocumentId); @@ -64,6 +70,12 @@ public List findByPageWithFussySearch(Integer pageNum, Integer pag return documentMapper.findByPageWithFussySearch(offset, pageSize, keyWord); } + @Override + public List findByPageWithFilter(Integer pageNum, Integer pageSize, Sort sort, String filterWord) { + long offset = (long) pageNum * pageSize; + return documentMapper.findByPageWithFilter(offset, pageSize, filterWord); + } + @Override public List findByUserId(String userId, Integer pageNum, Integer pageSize, Sort sort) { long offset = (long) pageNum * pageSize; @@ -95,4 +107,42 @@ public List stats(Date startDate, Date endDate) { public List trend(Date startDate, Date endDate) { return documentMapper.trend(startDate, endDate); } + + @Override + public List findByPageByTag(String tagId, int pageNum, int pageSize) { + if (pageNum < 1) { + pageNum = 1; + } + long offset = (long) (pageNum - 1) * pageSize; + return documentMapper.findByPageByTag(tagId, offset, pageSize); + } + + @Override + public List findByPageByCategory(String categoryId, int pageNum, int pageSize) { + if (pageNum < 1) { + pageNum = 1; + } + long offset = (long) (pageNum - 1) * pageSize; + return documentMapper.findByPageByCategory(categoryId, offset, pageSize); + } + + @Override + public long countByTagId(String tagId) { + return documentMapper.countByTagId(tagId); + } + + @Override + public long countByCategoryId(String categoryId) { + return documentMapper.countByCategoryId(categoryId); + } + + @Override + public List> countByDocType() { + return documentMapper.countByDocType(); + } + + @Override + public List> countByCategory() { + return documentMapper.countByCategory(); + } } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/LikeDocRelationshipMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/LikeDocRelationshipMapper.java index c8d5d8a..21291ca 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/LikeDocRelationshipMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/LikeDocRelationshipMapper.java @@ -28,6 +28,8 @@ public interface LikeDocRelationshipMapper { long countByEntityId(@Param("entityId") String entityId); + long countByEntityIdAndEntityType(@Param("entityId") String entityId, @Param("entityType") Integer entityType); + void deleteById(@Param("id") String id); void deleteByUserId(@Param("userId") String userId); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagDocRelationshipMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagDocRelationshipMapper.java index b956984..0e175e5 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagDocRelationshipMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagDocRelationshipMapper.java @@ -47,4 +47,6 @@ public interface TagDocRelationshipMapper { List findFileIdsByTagId(@Param("tagId") String tagId); List findFileIdsByTagIds(@Param("tagIds") List tagIds); + + List findByTagIdAndFileIds(@Param("tagId") String tagId, @Param("fileIds") List fileIds); } diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMapper.java index 9f4c6c8..961ecbd 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMapper.java @@ -9,7 +9,7 @@ @Mapper public interface TagMapper { - Tag save(Tag tag); + int save(Tag tag); Tag findById(@Param("id") String id); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMybatisRepository.java index 15365a3..d89bf9b 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMybatisRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/TagMybatisRepository.java @@ -19,6 +19,9 @@ public class TagMybatisRepository implements TagRepository { @Autowired private TagMapper tagMapper; + @Autowired + private TagDocRelationshipMapper tagDocRelationshipMapper; + @Override public Tag save(Tag tag) { tagMapper.save(tag); @@ -105,7 +108,7 @@ public boolean relationshipExists() { @Override public List findRelationshipsByTagId(String tagId) { - return null; + return tagDocRelationshipMapper.findByTagId(tagId); } @Override diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMapper.java index 32e1966..08bf0b3 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMapper.java @@ -16,4 +16,8 @@ public interface ThumbnailMapper { List findAllByObjectId(@Param("objectId") String objectId); void deleteByObjectId(@Param("objectId") String objectId); + + Thumbnail findByObjectIdAndType(@Param("objectId") String objectId, @Param("thumbnailEnum") String thumbnailEnum); + + Thumbnail findByObjectIdAndSize(@Param("objectId") String objectId, @Param("thumbSizeEnum") String thumbSizeEnum); } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMybatisRepository.java new file mode 100644 index 0000000..6c44f81 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMybatisRepository.java @@ -0,0 +1,50 @@ +package com.jiaruiblog.infrastructure.repository.mysql; + +import com.jiaruiblog.domain.entity.po.Thumbnail; +import com.jiaruiblog.infrastructure.repository.ThumbnailRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * MyBatis Thumbnail Repository Implementation + * + * @author luojiarui + */ +@Repository +public class ThumbnailMybatisRepository implements ThumbnailRepository { + + @Autowired + private ThumbnailMapper thumbnailMapper; + + @Override + public void save(Thumbnail thumbnail) { + thumbnailMapper.save(thumbnail); + } + + @Override + public Thumbnail findByObjectId(String objectId) { + return thumbnailMapper.findByObjectId(objectId); + } + + @Override + public List findAllByObjectId(String objectId) { + return thumbnailMapper.findAllByObjectId(objectId); + } + + @Override + public void deleteByObjectId(String objectId) { + thumbnailMapper.deleteByObjectId(objectId); + } + + @Override + public Thumbnail findByObjectIdAndType(String objectId, String thumbnailEnum) { + return thumbnailMapper.findByObjectIdAndType(objectId, thumbnailEnum); + } + + @Override + public Thumbnail findByObjectIdAndSize(String objectId, String thumbSizeEnum) { + return thumbnailMapper.findByObjectIdAndSize(objectId, thumbSizeEnum); + } +} diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMapper.java index eca31b3..de3b88e 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMapper.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMapper.java @@ -23,7 +23,7 @@ public interface UserMapper { int deleteById(@Param("id") String id); - User save(User user); + int save(User user); long count(); diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMybatisRepository.java index 90a94b2..865bf8c 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMybatisRepository.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/UserMybatisRepository.java @@ -68,7 +68,7 @@ public long count() { @Override public List findByPage(int pageNum, int pageSize, Sort sort) { - int offset = pageNum * pageSize; + int offset = (pageNum - 1) * pageSize; return userMapper.findByPage(offset, pageSize); } } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/storage/MinioStorageStrategy.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/storage/MinioStorageStrategy.java index 482384f..2186dd4 100644 --- a/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/storage/MinioStorageStrategy.java +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/storage/MinioStorageStrategy.java @@ -30,7 +30,7 @@ public class MinioStorageStrategy implements StorageStrategy { @Autowired private MinioClient minioClient; - @Value("${minio.bucket-name:alldocs}") + @Value("${minio.bucket:all-docs-bucket}") private String bucketName; /** diff --git a/all-docs-infrastructure/src/main/resources/mapper/CateDocRelationshipMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CateDocRelationshipMapper.xml index acabff3..05cc892 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CateDocRelationshipMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CateDocRelationshipMapper.xml @@ -99,4 +99,12 @@ SELECT category_id FROM cate_doc_relationship WHERE file_id = #{fileId} + + diff --git a/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml index 88c3df2..42ac4b8 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml @@ -4,7 +4,7 @@ - + @@ -13,7 +13,7 @@ INSERT INTO collect_doc_relationship (id, redis_action_enum, user_id, doc_id, create_date, update_date) - VALUES (#{id}, #{redisActionEnum}, #{userId}, #{docId}, #{createDate}, #{updateDate}) + VALUES (#{id}, #{redisActionEnum.code}, #{userId}, #{docId}, #{createDate}, #{updateDate}) diff --git a/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml index 26079ea..ccd71d2 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml @@ -9,13 +9,14 @@ + - INSERT INTO comment (id, create_user, user_id, user_name, content, doc_id, create_date, update_date) - VALUES (#{id}, #{createUser}, #{userId}, #{userName}, #{content}, #{docId}, #{createDate}, #{updateDate}) + INSERT INTO comment (id, create_user, user_id, user_name, content, doc_id, doc_name, create_date, update_date) + VALUES (#{id}, #{createUser}, #{userId}, #{userName}, #{content}, #{docId}, #{docName}, #{createDate}, #{updateDate}) + + @@ -42,4 +47,19 @@ DELETE FROM comment WHERE doc_id = #{docId} - \ No newline at end of file + + + + DELETE FROM comment WHERE id IN + + #{id} + + + + + + diff --git a/all-docs-infrastructure/src/main/resources/mapper/DocLogMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocLogMapper.xml index 3574afe..70228f0 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocLogMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocLogMapper.xml @@ -30,6 +30,10 @@ SELECT * FROM doc_log WHERE user_id = #{userId} ORDER BY create_date DESC + + diff --git a/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml index 8136192..dd194cf 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml @@ -8,10 +8,10 @@ - - - - + + + + @@ -39,6 +39,13 @@ SELECT COUNT(*) FROM doc_review WHERE user_id = #{userId} + + + + + + DELETE FROM doc_review + + DELETE FROM doc_review WHERE id IN diff --git a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml index d5ca0dc..a2f5ff1 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml @@ -16,9 +16,9 @@ - + - + @@ -31,7 +31,7 @@ error_msg, reviewing, user_id, user_name, create_date, update_date) VALUES (#{id}, #{name}, #{size}, #{uploadDate}, #{md5}, #{content}, #{contentType}, #{suffix}, #{description}, #{gridfsId}, #{thumbId}, #{textFileId}, #{previewFileId}, - #{docState}, #{errorMsg}, #{reviewing}, #{userId}, #{userName}, #{createDate}, #{updateDate}) + #{docState,typeHandler=com.jiaruiblog.infrastructure.config.mybatis.DocStateEnumTypeHandler}, #{errorMsg}, #{reviewing}, #{userId}, #{userName}, #{createDate}, #{updateDate}) @@ -41,14 +41,22 @@ text_file_id = #{textFileId}, thumb_id = #{thumbId}, preview_file_id = #{previewFileId}, + doc_state = #{docState,typeHandler=com.jiaruiblog.infrastructure.config.mybatis.DocStateEnumTypeHandler}, + error_msg = #{errorMsg}, + reviewing = #{reviewing}, update_date = NOW() WHERE id = #{id} - SELECT COUNT(*) FROM file_document WHERE reviewing = false + + @@ -72,7 +80,14 @@ + + @@ -118,4 +133,46 @@ ORDER BY date ASC + + + + + + + + + + + + \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/resources/mapper/LikeDocRelationshipMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/LikeDocRelationshipMapper.xml index b117324..e4b7782 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/LikeDocRelationshipMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/LikeDocRelationshipMapper.xml @@ -44,6 +44,10 @@ SELECT COUNT(*) FROM like_relationship WHERE entity_id = #{entityId} + + DELETE FROM like_relationship WHERE id = #{id} diff --git a/all-docs-infrastructure/src/main/resources/mapper/TagDocRelationshipMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/TagDocRelationshipMapper.xml index 310c28e..79bdab5 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/TagDocRelationshipMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/TagDocRelationshipMapper.xml @@ -99,4 +99,12 @@ + + diff --git a/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml index b45e08e..231fb5c 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml @@ -5,9 +5,9 @@ - + - + @@ -27,4 +27,12 @@ DELETE FROM thumbnail WHERE object_id = #{objectId} + + + + \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml index 9200c20..dae53f4 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml @@ -8,12 +8,12 @@ - + - - + + @@ -47,7 +47,7 @@ avatar = #{avatar}, birthtime = #{birthtime}, banning = #{banning}, - permission_enum = #{permissionEnum}, + permission_enum = #{permissionEnum, typeHandler=com.jiaruiblog.infrastructure.config.mybatis.PermissionEnumTypeHandler}, nickname = #{nickname}, update_date = NOW() WHERE id = #{id} diff --git a/docs/superpowers/plans/2026-04-27-comment-queryallcomments-plan.md b/docs/superpowers/plans/2026-04-27-comment-queryallcomments-plan.md new file mode 100644 index 0000000..2a81423 --- /dev/null +++ b/docs/superpowers/plans/2026-04-27-comment-queryallcomments-plan.md @@ -0,0 +1,218 @@ +# queryAllComments 重构实现计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 重构 `queryAllComments` 方法,Service 层返回 `PageVO`,Controller 层负责组装 `CommentWithUserVO` + +**Architecture:** Service 层不再构建 VO,Controller 层负责调用 DocumentRepository 批量查询文档名称并使用 CommentConverter 组装 VO + +**Tech Stack:** Spring, CommentRepository, DocumentRepository + +--- + +## Chunk 1: Service 层接口修改 + +**Files:** +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java` +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java` + +- [ ] **Step 1: 修改 ICommentService 接口返回类型** + +修改 `ICommentService.java` 第 128 行: +```java +// 修改前 +PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin); + +// 修改后 +PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin); +``` + +- [ ] **Step 2: 修改 CommentServiceImpl 实现** + +修改 `CommentServiceImpl.java` 第 128-162 行: +```java +@Override +public PageVO queryAllComments(BasePageDTO page, String userId, Boolean isAdmin) { + log.info("查询的参数是:{}, {}", page, userId); + List comments; + if (Boolean.TRUE.equals(isAdmin)) { + comments = commentRepository.findAll(); + } else { + comments = commentRepository.findByUserId(userId); + } + + long count = comments.size(); + + int pageNum = page.getPage(); + int pageSize = page.getRows(); + int skip = (pageNum - 1) * pageSize; + comments = comments.stream() + .skip(skip) + .limit(pageSize) + .toList(); + + return PageVO.builder() + .total((int) count) + .list(comments) + .pageNum(pageNum) + .pageSize(pageSize) + .build(); +} +``` + +- [ ] **Step 3: 提交** + +```bash +git add all-docs-application/src/main/java/com/jiaruiblog/application/service/ICommentService.java +git add all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java +git commit -m "refactor: queryAllComments返回PageVO" +``` + +--- + +## Chunk 2: 新增 CommentConverter + +**Files:** +- Create: `all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java` + +- [ ] **Step 1: 创建 CommentConverter** + +创建文件 `all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java`: + +```java +package com.jiaruiblog.application.service.converter; + +import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.domain.entity.vo.CommentWithUserVO; +import org.springframework.stereotype.Component; + +@Component +public class CommentConverter { + + public CommentWithUserVO toVO(Comment comment, String docName) { + CommentWithUserVO vo = new CommentWithUserVO(); + vo.setId(comment.getId()); + vo.setUserId(comment.getUserId()); + vo.setUserName(comment.getUserName()); + vo.setContent(comment.getContent()); + vo.setDocId(comment.getDocId()); + vo.setDocName(docName); + vo.setCreateDate(comment.getCreateDate()); + vo.setUpdateDate(comment.getUpdateDate()); + return vo; + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java +git commit -m "feat: add CommentConverter手写字段映射" +``` + +--- + +## Chunk 3: Controller 层 VO 组装 + +**Files:** +- Modify: `all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java` + +- [ ] **Step 1: 修改 CommentController 注入依赖并组装 VO** + +修改 `CommentController.java`: + +1. 新增注入: +```java +@Resource +CommentConverter commentConverter; + +@Resource +DocumentRepository documentRepository; +``` + +2. 修改 `queryMyComments` 方法: +```java +@Operation(summary = "查询用户评论", description = "查询当前用户的评论列表") +@PostMapping(value = "/auth/myComments") +public ApiResult> queryMyComments(@RequestBody BasePageDTO pageDTO, HttpServletRequest request) { + String userId = (String) request.getAttribute("id"); + PageVO commentPage = commentService.queryAllComments(pageDTO, userId, false); + PageVO result = buildCommentWithUserVO(commentPage); + return ApiResult.success(result); +} +``` + +3. 修改 `queryAllComments` 方法: +```java +@Operation(summary = "查询所有评论", description = "管理员查询所有用户的评论列表") +@Permission(PermissionEnum.ADMIN) +@PostMapping(value = "/auth/allComments") +public ApiResult> queryAllComments(@RequestBody BasePageDTO pageDTO) { + PageVO commentPage = commentService.queryAllComments(pageDTO, null, true); + PageVO result = buildCommentWithUserVO(commentPage); + return ApiResult.success(result); +} +``` + +4. 新增私有方法: +```java +private PageVO buildCommentWithUserVO(PageVO commentPage) { + if (commentPage == null || commentPage.getList() == null || commentPage.getList().isEmpty()) { + return PageVO.builder() + .total(0) + .list(new java.util.ArrayList<>()) + .pageNum(commentPage != null ? commentPage.getPageNum() : 1) + .pageSize(commentPage != null ? commentPage.getPageSize() : 10) + .build(); + } + + // 收集所有docId + List docIdList = commentPage.getList().stream() + .map(Comment::getDocId) + .collect(Collectors.toList()); + + // 批量查询文档 + List documents = documentRepository.findByIdList(docIdList); + Map docNameMap = documents.stream() + .collect(Collectors.toMap(FileDocument::getId, FileDocument::getName)); + + // 组装VO + List voList = commentPage.getList().stream() + .map(comment -> commentConverter.toVO(comment, docNameMap.get(comment.getDocId()))) + .collect(Collectors.toList()); + + return PageVO.builder() + .total(commentPage.getTotal()) + .list(voList) + .pageNum(commentPage.getPageNum()) + .pageSize(commentPage.getPageSize()) + .build(); +} +``` + +5. 添加必要的 import: +```java +import com.jiaruiblog.application.service.converter.CommentConverter; +import com.jiaruiblog.domain.entity.po.Comment; +import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.infrastructure.repository.DocumentRepository; +import java.util.Map; +import java.util.stream.Collectors; +``` + +- [ ] **Step 2: 提交** + +```bash +git add all-docs-api/src/main/java/com/jiaruiblog/api/controller/CommentController.java +git commit -m "refactor: Controller层组装CommentWithUserVO" +``` + +--- + +## 验收标准 + +1. `ICommentService.queryAllComments` 返回 `PageVO` +2. `CommentConverter.toVO` 手写字段映射,无 BeanUtils +3. Controller 层调用 `DocumentRepository.findByIdList` 批量查询文档名称 +4. `CommentWithUserVO.docName` 有值 diff --git a/docs/superpowers/plans/2026-04-28-document-list-fulltext-search-plan.md b/docs/superpowers/plans/2026-04-28-document-list-fulltext-search-plan.md new file mode 100644 index 0000000..ad83c1e --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-document-list-fulltext-search-plan.md @@ -0,0 +1,304 @@ +# Document List 全文检索实现计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 `/api/v1/document/list` 接口添加全文检索能力,当 `filterWord` 不为空时使用 ES 检索,高亮片段分段存入 `description` 字段 + +**Architecture:** 基于现有 ES 检索能力,改造 `ElasticServiceImpl` 添加支持 total 返回的方法,改造 `DocumentServiceImpl.list()` 根据条件分流 + +**Tech Stack:** Spring Data Elasticsearch, MyBatis, Redis + +--- + +## Chunk 1: ElasticServiceImpl 新增分页检索方法 + +**Files:** +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/ElasticServiceImpl.java` +- Test: `all-docs-application/src/test/java/com/jiaruiblog/application/service/impl/ElasticServiceImplTest.java` + +- [ ] **Step 1: 添加 SearchResultItem 多高亮支持** + +修改 `SearchResultItem.java`,将 `highlightFragment` 改为 `highlightFragments` (List): + +```java +// File: all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchResultItem.java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchResultItem { + private String id; + private List highlightFragments; // 改为 List + private String highlightSource; +} +``` + +- [ ] **Step 2: 新增 ES 分页检索方法** + +在 `ElasticServiceImpl.java` 新增方法 `searchDocumentsFullText(DocumentDTO dto)`: + +```java +/** + * 全文检索 + 分页 + 多高亮片段 + * @param filterWord 关键词 + * @param tagId 标签ID(可选) + * @param categoryId 分类ID(可选) + * @param page 页码 + * @param rows 每页条数 + * @return SearchResultVO 含 total 和 list + */ +public SearchResultVO searchDocumentsFullText(String filterWord, String tagId, String categoryId, int page, int rows) { + try { + // 1. 构建基础条件 + Criteria criteria = new Criteria("name").matches(filterWord) + .or("content").matches(filterWord) + .or("tagNames").matches(filterWord) + .or("categoryName").matches(filterWord); + + // 2. 处理 tagId -> tagNames 过滤 + if (StringUtils.hasText(tagId)) { + Tag tag = tagRepository.findById(tagId); + if (tag != null) { + criteria = criteria.and("tagNames").contains(tag.getName()); + } + } + + // 3. 处理 categoryId -> categoryName 过滤 + if (StringUtils.hasText(categoryId)) { + Category category = categoryRepository.findById(categoryId); + if (category != null) { + criteria = criteria.and("categoryName").contains(category.getName()); + } + } + + // 4. ES 分页查询 + Query esQuery = new CriteriaQuery(criteria) + .setPageable(PageRequest.of(page, rows)); + SearchHits searchHits = elasticsearchOperations.search(esQuery, SearchDocument.class); + + // 5. 构建结果 + List items = new ArrayList<>(); + for (SearchHit hit : searchHits.getSearchHits()) { + SearchDocument doc = hit.getContent(); + String keyword = filterWord; + List fragments = new ArrayList<>(); + + // 从 content 中提取多个高亮 + if (doc.getContent() != null) { + int lastIndex = 0; + while (true) { + int idx = doc.getContent().toLowerCase().indexOf(keyword.toLowerCase(), lastIndex); + if (idx < 0) break; + String frag = extractHighlightFragment(doc.getContent(), keyword, idx); + if (frag != null) fragments.add(frag); + lastIndex = idx + 1; + if (fragments.size() >= 3) break; // 最多3段 + } + } + + // 如果 content 没匹配,尝试 name + if (fragments.isEmpty() && doc.getName() != null && doc.getName().contains(keyword)) { + fragments.add(wrapWithHighlight(doc.getName(), keyword)); + } + + items.add(SearchResultItem.builder() + .id(doc.getId()) + .highlightFragments(fragments) + .highlightSource(fragments.isEmpty() ? null : "content") + .build()); + } + + long total = searchHits.getTotalHits(); + return SearchResultVO.builder().total(total).items(items).build(); + + } catch (Exception e) { + log.error("searchDocumentsFullText failed", e); + return SearchResultVO.builder().total(0).items(new ArrayList<>()).build(); + } +} +``` + +- [ ] **Step 3: 新增 SearchResultVO 类** + +```java +// File: all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchResultVO.java +@Data +@Builder +public class SearchResultVO { + private long total; + private List items; +} +``` + +- [ ] **Step 4: 修改 extractHighlightFragment 支持指定位置** + +```java +private String extractHighlightFragment(String content, String keyword, int index) { + int start = Math.max(0, index - 30); + int end = Math.min(content.length(), index + keyword.length() + 50); + String fragment = content.substring(start, end); + if (start > 0) fragment = "..." + fragment; + if (end < content.length()) fragment = fragment + "..."; + return wrapWithHighlight(fragment, keyword); +} +``` + +- [ ] **Step 5: 运行测试** + +```bash +cd all-docs-application && mvn test -Dtest=ElasticServiceImplTest -v +``` + +--- + +## Chunk 2: DocumentServiceImpl 改造 list 方法 + +**Files:** +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java` +- Test: `all-docs-application/src/test/java/com/jiaruiblog/application/service/impl/DocumentServiceImplTest.java` + +- [ ] **Step 1: 修改 DocumentServiceImpl.list() 入口逻辑** + +在 `list(DocumentDTO documentDTO)` 方法开头添加判断: + +```java +@Override +public PageVO list(DocumentDTO documentDTO) { + if (documentDTO == null) { + return PageVO.builder().build(); + } + + String filterWord = documentDTO.getFilterWord(); + + // filterWord 不为空时走 ES 全文检索 + if (StringUtils.hasText(filterWord)) { + return searchFullText(documentDTO); + } + + // 否则走现有 MySQL 逻辑 + // ... 现有代码保持不变 +} +``` + +- [ ] **Step 2: 新增 searchFullText() 方法** + +```java +/** + * ES 全文检索 + */ +private PageVO searchFullText(DocumentDTO documentDTO) { + String filterWord = documentDTO.getFilterWord(); + String tagId = documentDTO.getTagId(); + String categoryId = documentDTO.getCategoryId(); + int page = documentDTO.getPage(); + int rows = documentDTO.getRows(); + + // 1. ES 检索 + SearchResultVO searchResult = elasticService.searchDocumentsFullText( + filterWord, tagId, categoryId, page, rows); + + if (searchResult.getItems().isEmpty()) { + return PageVO.builder() + .pageNum(page) + .pageSize(rows) + .total(0) + .list(new ArrayList<>()) + .build(); + } + + // 2. 获取文档 ID 列表 + List docIds = searchResult.getItems().stream() + .map(SearchResultItem::getId) + .toList(); + + // 3. 批量查询 MySQL 获取文档详情 + List documents = documentMybatisRepository.findByIdList(docIds); + + // 4. 构建 ID -> 高亮片段 的映射 + Map> highlightMap = searchResult.getItems().stream() + .collect(Collectors.toMap( + SearchResultItem::getId, + SearchResultItem::getHighlightFragments, + (existing, replacement) -> replacement + )); + + // 5. 转换为 DocumentVO,高亮片段存入 description + List voList = documents.stream() + .map(doc -> { + DocumentVO vo = convertDocument(new DocumentVO(), doc); + List highlights = highlightMap.get(doc.getId()); + if (highlights != null && !highlights.isEmpty()) { + vo.setDescription(String.join("\n---\n", highlights)); + } + return vo; + }) + .toList(); + + return PageVO.builder() + .pageNum(page) + .pageSize(rows) + .total(searchResult.getTotal()) + .list(voList) + .build(); +} +``` + +- [ ] **Step 3: 注入 RedisService(如果尚未注入)** + +确认 `ElasticServiceImpl` 已注入 `TagRepository` 和 `CategoryRepository`。如果构造方法需要调整,修改构造方法注入。 + +- [ ] **Step 4: 运行测试** + +```bash +cd all-docs-application && mvn test -Dtest=DocumentServiceImplTest -v +``` + +--- + +## Chunk 3: 集成测试 + +**Files:** +- Test: `all-docs-api/src/test/java/com/jiaruiblog/api/controller/DocumentControllerTest.java` + +- [ ] **Step 1: 启动应用测试** + +```bash +cd all-docs-bootstrap && mvn spring-boot:run +``` + +- [ ] **Step 2: 测试全文检索** + +```bash +curl -X POST http://localhost:8082/api/v1/document/list \ + -H "Content-Type: application/json" \ + -d '{"filterWord":"mcp","page":0,"rows":6,"type":"FILTER","userId":"ac92d2e5-d092-45eb-a8db-4cb7d9ae74c1"}' +``` + +预期返回:documents 中 description 字段包含高亮片段,格式为 `"第一段\n---\n第二段"` + +- [ ] **Step 3: 测试 category/tag 过滤(不走 ES)** + +```bash +curl -X POST http://localhost:8082/api/v1/document/list \ + -H "Content-Type: application/json" \ + -d '{"categoryId":"xxx","page":0,"rows":6,"type":"CATEGORY"}' +``` + +预期返回:description 为空(不走 ES),正常返回文档列表 + +--- + +## 关键文件清单 + +| 文件 | 改动 | +|------|------| +| `SearchResultItem.java` | `highlightFragment` → `highlightFragments List` | +| `SearchResultVO.java` | 新增,含 `total` 和 `items` | +| `ElasticServiceImpl.java` | 新增 `searchDocumentsFullText()` 方法 | +| `DocumentServiceImpl.java` | `list()` 加分流,新增 `searchFullText()` | + +## 注意事项 + +1. ES 检索时 tagId/categoryId 需要先查 MySQL 获取 name +2. 高亮片段用 `\n---\n` 分隔,前端可通过 `split("\n---\n")` 解析 +3. `filterWord` 为空时保持现有 MySQL 逻辑不变 \ No newline at end of file diff --git a/docs/superpowers/plans/2026-04-29-stats-api-plan.md b/docs/superpowers/plans/2026-04-29-stats-api-plan.md new file mode 100644 index 0000000..665845c --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-stats-api-plan.md @@ -0,0 +1,408 @@ +# Statistics API 扩展实现计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 扩展统计后端 API,完成统计卡片数据扩展及 5 个新增接口 + +**Architecture:** 在现有 `StatisticsService` / `StatisticsController` 基础上扩展,新增 5 个 VO 类,新增 MyBatis mapper 查询,复用 Redis ZSet 获取热词和热门文档数据 + +**Tech Stack:** Spring Boot, MyBatis, Redis (StringRedisTemplate ZSet), Java + +--- + +## Chunk 1: Domain 层 - 新增 VO 类 + +**Files:** +- Create: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocTypeDistVO.java` +- Create: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/CategoryDistVO.java` +- Create: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/HotDocVO.java` +- Create: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchHotWordVO.java` +- Create: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/UserActivityVO.java` +- Modify: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/StatsVO.java` + +- [ ] **Step 1: 创建 DocTypeDistVO** + +```java +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class DocTypeDistVO { + private String type; + private Long count; +} +``` + +- [ ] **Step 2: 创建 CategoryDistVO** + +```java +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class CategoryDistVO { + private String category; + private Long count; +} +``` + +- [ ] **Step 3: 创建 HotDocVO** + +```java +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class HotDocVO { + private String id; + private String title; + private Long viewCount; +} +``` + +- [ ] **Step 4: 创建 SearchHotWordVO** + +```java +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class SearchHotWordVO { + private String keyword; + private Long count; +} +``` + +- [ ] **Step 5: 创建 UserActivityVO** + +```java +package com.jiaruiblog.domain.entity.vo; + +import lombok.Data; + +@Data +public class UserActivityVO { + private String month; + private Long activeUsers; + private Long totalUsers; +} +``` + +- [ ] **Step 6: 扩展 StatsVO** + +在 `StatsVO.java` 中新增 4 个字段: + +```java +private Long userNum; +private Long downloadNum; +private Long searchNum; +private Long viewNum; +``` + +--- + +## Chunk 2: Infrastructure 层 - MyBatis Mapper 扩展 + +**Files:** +- Modify: `all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java` +- Modify: `all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml` +- Modify: `all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMybatisRepository.java` +- Modify: `all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java` + +- [ ] **Step 1: 在 DocumentMapper.java 新增方法声明** + +```java +List> countByDocType(); +List> countByCategory(); +``` + +- [ ] **Step 2: 在 DocumentMapper.xml 新增 SQL** + +在 `` 中添加: + +```xml + + + +``` + +- [ ] **Step 3: 在 DocumentMybatisRepository.java 实现方法** + +```java +@Override +public List> countByDocType() { + return documentMapper.countByDocType(); +} + +@Override +public List> countByCategory() { + return documentMapper.countByCategory(); +} +``` + +- [ ] **Step 4: 在 DocumentRepository.java 接口新增方法声明** + +```java +List> countByDocType(); +List> countByCategory(); +``` + +--- + +## Chunk 3: Application 层 - Service 接口与实现 + +**Files:** +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/StatisticsService.java` +- Modify: `all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/StatisticsServiceImpl.java` + +- [ ] **Step 1: 在 StatisticsService.java 新增方法声明** + +```java +List docTypeDist(); +List categoryDist(); +List hotDocs(); +List searchHotWords(); +List userActivity(); +``` + +- [ ] **Step 2: 在 StatisticsServiceImpl.java 实现扩展的 all() 方法** + +修改 `all()` 方法,新增字段: + +```java +@Override +public StatsVO all() { + StatsVO vo = new StatsVO(); + vo.setDocNum(countDocument()); + vo.setUserNum(userRepository.count()); // 新增 + vo.setCategoryNum(countCategory()); + vo.setTagNum(countTag()); + vo.setCommentNum(commentRepository.count()); + // 以下暂无数据源,返回 0 + vo.setDownloadNum(0L); + vo.setSearchNum(0L); + vo.setViewNum(0L); + return vo; +} +``` + +- [ ] **Step 3: 实现 docTypeDist()** + +```java +@Override +public List docTypeDist() { + List> rawList = documentRepository.countByDocType(); + List result = new ArrayList<>(); + for (Map map : rawList) { + DocTypeDistVO vo = new DocTypeDistVO(); + vo.setType((String) map.get("type")); + vo.setCount(((Number) map.get("count")).longValue()); + result.add(vo); + } + return result; +} +``` + +- [ ] **Step 4: 实现 categoryDist()** + +```java +@Override +public List categoryDist() { + List> rawList = documentRepository.countByCategory(); + List result = new ArrayList<>(); + for (Map map : rawList) { + CategoryDistVO vo = new CategoryDistVO(); + vo.setCategory((String) map.get("category")); + Object countObj = map.get("count"); + vo.setCount(countObj != null ? ((Number) countObj).longValue() : 0L); + result.add(vo); + } + return result; +} +``` + +- [ ] **Step 5: 实现 hotDocs()** + +```java +@Override +public List hotDocs() { + List docIdList = redisService.getHotList(null, RedisServiceImpl.DOC_KEY); + List result = new ArrayList<>(); + if (docIdList == null || docIdList.isEmpty()) { + return result; + } + int limit = Math.min(docIdList.size(), 10); + for (int i = 0; i < limit; i++) { + String docId = docIdList.get(i); + FileDocument doc = documentRepository.queryById(docId); + if (doc == null) { + continue; + } + HotDocVO vo = new HotDocVO(); + vo.setId(docId); + vo.setTitle(doc.getName()); + vo.setViewCount((long) redisService.score(RedisServiceImpl.DOC_KEY, docId)); + result.add(vo); + } + return result; +} +``` + +- [ ] **Step 6: 实现 searchHotWords()** + +```java +@Override +public List searchHotWords() { + List hotList = redisService.getHotList(null, RedisServiceImpl.SEARCH_KEY); + List result = new ArrayList<>(); + if (hotList == null || hotList.isEmpty()) { + return result; + } + int limit = Math.min(hotList.size(), 10); + for (int i = 0; i < limit; i++) { + String keyword = hotList.get(i); + SearchHotWordVO vo = new SearchHotWordVO(); + vo.setKeyword(keyword); + vo.setCount((long) redisService.score(RedisServiceImpl.SEARCH_KEY, keyword)); + result.add(vo); + } + return result; +} +``` + +- [ ] **Step 7: 实现 userActivity()** + +```java +@Override +public List userActivity() { + // 暂无数据源,返回空列表 + return new ArrayList<>(); +} +``` + +--- + +## Chunk 4: API 层 - Controller 端点 + +**Files:** +- Modify: `all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java` + +- [ ] **Step 1: 新增 docTypeDist 端点** + +```java +@Operation(summary = "文档类型分布", description = "查询各文档类型的数量分布") +@GetMapping("/docTypeDist") +public ApiResult> docTypeDist() { + return ApiResult.success(statisticsService.docTypeDist()); +} +``` + +- [ ] **Step 2: 新增 categoryDist 端点** + +```java +@Operation(summary = "分类文档分布", description = "查询各分类下的文档数量") +@GetMapping("/categoryDist") +public ApiResult> categoryDist() { + return ApiResult.success(statisticsService.categoryDist()); +} +``` + +- [ ] **Step 3: 新增 hotDocs 端点** + +```java +@Operation(summary = "热门文档 TOP 10", description = "查询热门文档排行") +@GetMapping("/hotDocs") +public ApiResult> hotDocs() { + return ApiResult.success(statisticsService.hotDocs()); +} +``` + +- [ ] **Step 4: 新增 searchHotWords 端点** + +```java +@Operation(summary = "搜索热词排行", description = "查询搜索热词排行") +@GetMapping("/searchHotWords") +public ApiResult> searchHotWords() { + return ApiResult.success(statisticsService.searchHotWords()); +} +``` + +- [ ] **Step 5: 新增 userActivity 端点** + +```java +@Operation(summary = "用户活跃度趋势", description = "查询用户活跃度趋势") +@GetMapping("/userActivity") +public ApiResult> userActivity() { + return ApiResult.success(statisticsService.userActivity()); +} +``` + +--- + +## Chunk 5: 创建 TODO 文档 + +**Files:** +- Create: `C:\Project\java\all-docs\TODO.md` + +- [ ] **Step 1: 创建 TODO.md** + +```markdown +# 待补充功能 + +以下功能在 Statistics API 扩展中暂未实现,后续补充: + +## 1. 浏览量统计 (viewNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的文档浏览次数 + +**方案:** 在文档浏览接口中增加 Redis ZSet 计数,key 为 `doc:hot` + +**待办:** +- [ ] 在文档浏览接口中调用 `redisService.incrementDocScore(docId, 1)` +- [ ] 验证 `doc:hot` ZSet 是否正常更新 + +## 2. 下载次数统计 (downloadNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的文档下载次数 + +**方案:** 在文档下载接口中增加 Redis 计数 + +**待办:** +- [ ] 确定下载接口位置 +- [ ] 增加下载计数逻辑 +- [ ] 在 `all()` 方法中聚合下载次数 + +## 3. 搜索次数统计 (searchNum) + +**目标:** 在 `/statistics/all` 接口中返回真实的搜索次数 + +**方案:** 使用 Redis ZSet `search:hot` 的总分数作为搜索次数 + +**待办:** +- [ ] 在 `all()` 方法中聚合 `search:hot` ZSet 的总分 + +## 4. 用户活跃度统计 (userActivity) + +**目标:** 实现 `/statistics/userActivity` 接口 + +**方案:** 在用户登录/关键操作时记录活跃状态,按月聚合 + +**待办:** +- [ ] 设计用户活跃度记录机制(Redis 或数据库) +- [ ] 实现按月统计活跃用户数 +- [ ] 实现 `userActivity()` 方法 +``` diff --git a/docs/superpowers/specs/2026-04-26-getHotTrend-fix-design.md b/docs/superpowers/specs/2026-04-26-getHotTrend-fix-design.md new file mode 100644 index 0000000..402d3a1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-getHotTrend-fix-design.md @@ -0,0 +1,89 @@ +# getHotTrend 接口修复设计 + +## 背景 + +`GET /api/v1/statistics/getHotTrend` 接口存在三个问题需要修复。 + +## 问题清单 + +| # | 问题 | 严重程度 | 位置 | +|---|------|----------|------| +| 1 | `DOC_KEY` (ZSet `doc:hot`) 从未被写入数据,接口始终返回空 | 严重 | 全局 | +| 2 | `deleteKey(s)` 误删整个 Redis key,而非从 ZSet 移除成员 | 高 | StatisticsController:137 | +| 3 | `hit` 排名值从 10 开始递减,逻辑错误 | 中 | StatisticsController:156-163 | + +## 设计方案 + +### 1. 新增 Redis 方法 + +**文件**: `RedisServiceImpl.java` + +```java +public void incrementDocScore(String docId, double delta) { + redisSearchTemplate.opsForZSet().incrementScore(DOC_KEY, docId, delta); +} +``` + +**接口**: `RedisService.java` + +```java +void incrementDocScore(String docId, double delta); +``` + +**说明**: 当 `delta > 0` 时点赞加分,`delta < 0` 时取消点赞减分。 + +--- + +### 2. 点赞时同步更新 DOC_KEY + +**文件**: `LikeServiceImpl.java` + +- 点赞成功时: `redisService.incrementDocScore(entityId, 1)` +- 取消点赞时: `redisService.incrementDocScore(entityId, -1)` + +--- + +### 3. Bug 修复 + +#### 问题2修复 + +```java +// 错误 +redisService.deleteKey(s); + +// 正确 +redisService.removeByDocId(s); +``` + +#### 问题3修复 + +```java +// 错误: count 从 10 开始 +int count = 10; + +// 正确: 从 2 开始 (top1 是第1名,others 从第2名起) +int count = 2; +``` + +--- + +## 数据流 + +``` +用户点赞 → LikeServiceImpl.like() → incrementDocScore(docId, +1) + → Redis ZSet DOC_KEY 分数 +1 + +用户取消点赞 → LikeServiceImpl.like() → incrementDocScore(docId, -1) + → Redis ZSet DOC_KEY 分数 -1 + +getHotTrend → Redis ZSet DOC_KEY → 返回热度排名前10文档 +``` + +--- + +## 测试要点 + +1. 点赞后 `getHotTrend` 返回的 `likeNum` 应增加 +2. 取消点赞后 `likeNum` 应减少 +3. `top1` 的 `hit` 值应为 1,`others` 每个的 `hit` 应从 2 开始 +4. 无效文档 ID 正确从 ZSet 移除而非删除整个 key diff --git a/docs/superpowers/specs/2026-04-27-comment-queryallcomments-design.md b/docs/superpowers/specs/2026-04-27-comment-queryallcomments-design.md new file mode 100644 index 0000000..1c47bb3 --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-comment-queryallcomments-design.md @@ -0,0 +1,54 @@ +# queryAllComments 重构设计 + +## 问题 + +1. `CommentWithUserVO` 在 Service 层构建,但 Controller 层也需要使用 +2. `CommentWithUserVO.docName` 为空,Comment 和 FileDocument 需要关联查询 +3. 使用了 `BeanUtils.copyProperties` 不够清晰 + +## 解决方案 + +### 1. Service 层 - 返回 `PageVO` + +修改 `ICommentService.queryAllComments` 返回类型为 `PageVO`,Service 层不再构建 VO + +### 2. Controller 层 - 负责 VO 组装 + +`CommentController` 注入 `DocumentRepository`,根据返回的 `docId` 批量查询文档名称,组装 `CommentWithUserVO` + +### 3. 新增 `CommentConverter` - 手写转换替代 BeanUtils + +```java +@Component +public class CommentConverter { + public CommentWithUserVO toVO(Comment comment, String docName) { + CommentWithUserVO vo = new CommentWithUserVO(); + vo.setId(comment.getId()); + vo.setUserId(comment.getUserId()); + vo.setUserName(comment.getUserName()); + vo.setContent(comment.getContent()); + vo.setDocId(comment.getDocId()); + vo.setDocName(docName); + vo.setCreateDate(comment.getCreateDate()); + vo.setUpdateDate(comment.getUpdateDate()); + return vo; + } +} +``` + +## 改动文件 + +| 文件 | 改动 | +|------|------| +| `ICommentService.java` | `queryAllComments` 返回 `PageVO` | +| `CommentServiceImpl.java` | 移除 VO 构建逻辑,返回 `PageVO` | +| `CommentConverter.java` | 新增,手写字段映射 | +| `CommentController.java` | 注入 `DocumentRepository`,调用 `CommentConverter` 组装 VO | + +## 数据流 + +``` +Controller -> Service (PageVO) -> Controller +Controller -> DocumentRepository (batch query docName) +Controller -> CommentConverter (to CommentWithUserVO) +``` diff --git a/docs/superpowers/specs/2026-04-27-document-search-refactor-design.md b/docs/superpowers/specs/2026-04-27-document-search-refactor-design.md new file mode 100644 index 0000000..224b92d --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-document-search-refactor-design.md @@ -0,0 +1,234 @@ +# 文档搜索接口重构设计方案 + +## 一、背景 + +根据 `接口文档.md` 的规范,改造现有的 `/api/v1/document/search` 接口,支持更丰富的搜索功能。 + +## 二、接口规范 + +### 2.1 接口信息 + +- **接口地址**: `/api/v1/document/search` +- **请求方式**: `POST` +- **Content-Type**: `application/json` +- **认证**: 需要登录(token) + +### 2.2 请求参数 + +```json +{ + "keyword": "项目", + "fullText": true, + "segment": true, + "searchType": "all", + "tags": ["pdf", "docx"], + "category": "技术文档", + "sortField": "createTime", + "sortOrder": "desc", + "page": 1, + "pageSize": 20 +} +``` + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +|-------|------|------|--------|------| +| keyword | string | 否 | "" | 搜索关键词 | +| fullText | boolean | 否 | false | 是否全文检索(true: 匹配名称+描述+分类;false: 只匹配名称) | +| segment | boolean | 否 | false | 是否分词(true: 开启中文分词搜索) | +| searchType | string | 否 | "all" | 搜索类型:`all`(全部)、`name`(仅名称)、`description`(仅描述) | +| tags | string[] | 否 | [] | 标签筛选数组,传入 tag 的 name | +| category | string | 否 | "" | 分类名称筛选,传空字符串表示不限分类 | +| sortField | string | 否 | "createTime" | 排序字段:`name`、`size`、`type`、`category`、`createTime` | +| sortOrder | string | 否 | "desc" | 排序方向:`asc`(升序)、`desc`(降序) | +| page | int | 否 | 1 | 页码,从 1 开始 | +| pageSize | int | 否 | 20 | 每页条数 | + +### 2.3 响应参数 + +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "id": "doc_001", + "name": "项目需求文档.pdf", + "type": "pdf", + "size": 2048576, + "sizeDisplay": "2 MB", + "description": "2024年Q1项目需求文档,包含功能清单和里程碑", + "category": "需求文档", + "tags": [ + { "name": "重要", "color": "red" }, + { "name": "2024", "color": "blue" } + ], + "liked": false, + "collected": true, + "createTime": "2024-01-15 10:30:00", + "updateTime": "2024-01-20 14:22:00" + } + ], + "total": 100, + "page": 1, + "pageSize": 20 + } +} +``` + +| 字段名 | 类型 | 说明 | +|-------|------|------| +| list | array | 文档列表 | +| list[].id | string | 文档ID | +| list[].name | string | 文档名称(带后缀) | +| list[].type | string | 文档类型(pdf/docx/xlsx/pptx/image/zip) | +| list[].size | long | 文档大小(字节) | +| list[].sizeDisplay | string | 文档大小(格式化,如 "2 MB") | +| list[].description | string | 文档描述 | +| list[].category | string | 所属分类名称 | +| list[].tags | array | 标签列表 | +| list[].tags[].name | string | 标签名称 | +| list[].tags[].color | string | 标签颜色 | +| list[].liked | boolean | 当前用户是否已点赞 | +| list[].collected | boolean | 当前用户是否已收藏 | +| list[].createTime | string | 创建时间(yyyy-MM-dd HH:mm:ss) | +| list[].updateTime | string | 更新时间(yyyy-MM-dd HH:mm:ss) | +| total | int | 总记录数 | +| page | int | 当前页码 | +| pageSize | int | 每页条数 | + +## 三、数据模型改动 + +### 3.1 Tag 实体新增 color 字段 + +**文件**: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/Tag.java` + +```java +@Data +public class Tag { + protected String id; + protected String name; + protected String color; // 新增 + protected Date createDate; + protected Date updateDate; +} +``` + +**说明**: +- `color` 字段用于存储标签颜色 +- 新增标签时传入颜色值,不传则使用默认颜色 +- 查询时返回 `name` 和 `color` + +### 3.2 SearchDocument 新增 ES 字段 + +**文件**: `all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/SearchDocument.java` + +```java +@Slf4j +@Data +@Document(indexName = "all_docs_document_index") +public class SearchDocument { + + @Id + @Field(type = FieldType.Keyword) + private String id; + + @Field(type = FieldType.Text, analyzer = "ik_max_word") + private String name; + + @Field(type = FieldType.Keyword) + private String type; + + @Field(type = FieldType.Text, analyzer = "ik_smart") + private String content; + + // 新增字段 + @Field(type = FieldType.Text, analyzer = "ik_smart") + private List tagNames; // 标签名字列表 + + @Field(type = FieldType.Text, analyzer = "ik_smart") + private String categoryName; // 分类名字 +} +``` + +## 四、业务逻辑流程 + +``` +1. 用户登录 → 获取 userId +2. ES 检索: + - fullText=true → 组合条件:name + content + tagNames + categoryName + - fullText=false → 只搜索 name + - segment=true → 使用 ik_max_word 分词器 + - searchType=all → 搜索所有字段 + - searchType=name → 只搜索 name + - searchType=description → 搜索 content +3. MySQL/关系表过滤: + - tags 筛选 → 通过 TagDocRelationship + Tag 查询匹配文档 + - category 筛选 → 通过 CateDocRelationship + Category 查询匹配文档 +4. 排序:sortField + sortOrder +5. 分页:page + pageSize +6. 组装结果: + - liked → 查询 like 表(当前用户是否点赞该文档) + - collected → 查询 collect 表(当前用户是否收藏该文档) + - tags → 查询 TagDocRelationship + Tag,返回 TagVO 列表 (name + color) +``` + +## 五、文件改动清单 + +| 序号 | 文件 | 改动内容 | +|------|------|---------| +| 1 | `Tag.java` | 新增 `color` 字段 | +| 2 | `SearchDocument.java` | 新增 `tagNames` 和 `categoryName` 字段 | +| 3 | `DocumentDTO.java` | 新增搜索参数字段 | +| 4 | `DocumentController.java` | GET→POST,添加认证,调用新逻辑 | +| 5 | `DocumentService.java` | 接口定义扩展 | +| 6 | `DocumentServiceImpl.java` | 实现新搜索逻辑 | +| 7 | `ElasticService.java` | 接口定义扩展 | +| 8 | `ElasticServiceImpl.java` | 实现新 ES 检索逻辑 | +| 9 | `LikeController.java` | 复用已有点赞接口 | +| 10 | `CollectController.java` | 复用已有收藏接口 | + +## 六、依赖关系 + +``` +DocumentController + ↓ +DocumentService.search(SearchQuery) + ↓ +┌─────────────────────────────────────┐ +│ ElasticService.searchIds() │ ← ES 检索文档ID +│ TagRepository.findFileIdsByTags() │ ← 标签过滤 +│ CategoryService.getDocIds() │ ← 分类过滤 +│ LikeService.isLiked() │ ← 点赞状态 +│ CollectService.isCollected() │ ← 收藏状态 +│ TagService.getTagsByDocId() │ ← 标签列表 +└─────────────────────────────────────┘ +``` + +## 七、实现任务分解 + +### 任务 1: 数据模型改动 +- Tag.java 新增 color 字段 +- SearchDocument.java 新增 tagNames 和 categoryName 字段 + +### 任务 2: DTO/VO 改动 +- 新建 SearchQuery.java 请求参数类 +- 新建 DocSearchVO.java 响应VO类 +- DocumentDTO.java 新增字段 + +### 任务 3: ES 检索逻辑 +- ElasticService 新增 searchDocuments 方法 +- ElasticServiceImpl 实现多条件检索 + +### 任务 4: Service 层改动 +- DocumentService.search 扩展支持多条件 +- DocumentServiceImpl 实现完整搜索流程 + +### 任务 5: Controller 层改动 +- DocumentController.search GET→POST +- 添加认证注解 +- 参数解析和响应组装 + +### 任务 6: ES 索引同步 +- 文档上传/更新时同步更新 ES 的 tagNames 和 categoryName +- 文档删除时同步删除 ES 索引 diff --git a/docs/superpowers/specs/2026-04-28-document-list-fulltext-search-design.md b/docs/superpowers/specs/2026-04-28-document-list-fulltext-search-design.md new file mode 100644 index 0000000..723694f --- /dev/null +++ b/docs/superpowers/specs/2026-04-28-document-list-fulltext-search-design.md @@ -0,0 +1,148 @@ +# Document List 全文检索改造设计 + +## 背景 + +`/api/v1/document/list` 接口需要支持全文检索能力: +- 用户进行过滤筛选时默认进行全文检索 +- 检索结果中高亮片段分段存入 `description` 字段 +- 全文检索与 category/tag 过滤分离,互不影响 + +## 现有接口对比 + +| 接口 | 搜索方式 | 分页方式 | 返回类型 | +|------|----------|----------|----------| +| `/document/list` | MySQL LIKE / ES(改造后) | MySQL / ES | `DocumentVO` | +| `/document/searchList` | ES 多条件 | MySQL | `DocSearchVO` | + +## 改造方案 + +### 1. 入口判断逻辑 + +`DocumentServiceImpl.list(DocumentDTO)` 方法入口增加判断: + +``` +filterWord 不为空? + → 调用 searchFullText() 方法 + → 否则调用 filterByCategoryOrTag() 方法 +``` + +### 2. 全文检索方法 `searchFullText()` + +#### 2.1 ES 查询 + +使用现有 `ElasticServiceImpl.searchDocumentsWithHighlight()` 或新增分页版本: + +```java +// 查询条件构建 +Criteria criteria = new Criteria("content").contains(filterWord) + .or("name").contains(filterWord) + .or("tagNames").contains(filterWord) + .or("categoryName").contains(filterWord); + +// ES 分页 +SearchDocument repository.findAll(criteria, PageRequest.of(page, rows)); +``` + +#### 2.2 tag/category 过滤 + +ES 中已有 `tagNames`(List)和 `categoryName`(String)字段,可直接在 ES 查询中附加条件: + +```java +if (tagId != null && !tagId.isEmpty()) { + // 需要先查 Tag 获取 name,再用 name 查询 ES + Tag tag = tagRepository.findById(tagId); + criteria = criteria.and("tagNames").contains(tag.getName()); +} + +if (categoryId != null && !categoryId.isEmpty()) { + // 需要先查 Category 获取 name,再用 name 查询 ES + Category category = categoryRepository.findById(categoryId); + criteria = criteria.and("categoryName").contains(category.getName()); +} +``` + +> 注意:ES 存的是 name,当前请求传的是 ID,所以需要先查 MySQL 获取 name 再查 ES。 + +#### 2.3 高亮片段处理 + +ES 返回高亮片段后,拼接存入 `DocumentVO.description`: + +```java +List highlights = searchResultItem.getHighlightFragments(); +String combinedHighlights = String.join("\n---\n", highlights); +documentVO.setDescription(combinedHighlights); +``` + +前端可通过 `\n---\n` 分割符解析各段高亮。 + +#### 2.4 返回结构 + +返回 `PageVO`: +- `total`: ES 命中总数 +- `pageNum`: 当前页码 +- `pageSize`: 每页条数 +- `list`: 文档列表,`description` 字段含高亮片段 + +### 3. category/tag 过滤方法 `filterByCategoryOrTag()` + +现有 MySQL 查询逻辑不变,作为独立方法存在。 + +### 4. 全文检索优先级 + +当 `filterWord` 和 `categoryId`/`tagId` 同时存在时: +1. 先通过 ES 查询全文(附带 tagNames/categoryName 过滤) +2. 获取匹配的文档 ID 列表 +3. 用 ID 列表查 MySQL 获取文档详情 + +### 5. 数据流图 + +``` +filterWord 非空? + ├── 是: ES分页检索 + tagNames/categoryName过滤 + │ → 获取文档ID列表 + │ → MySQL批量查文档详情 + │ → 填充高亮到description + │ → 返回PageVO + │ + └── 否: MySQL过滤查询(categoryId/tagId) + → 返回PageVO +``` + +## 关键文件改动 + +| 文件 | 改动内容 | +|------|----------| +| `DocumentServiceImpl.java` | 新增 `searchFullText()` 方法,修改 `list()` 入口逻辑 | +| `DocumentController.java` | 可能需要调整参数传递 | +| `DocumentVO.java` | 确认 `description` 用于存储高亮(已有字段) | + +## 分页参数 + +- `page`: 页码(从 0 开始) +- `rows`: 每页条数 + +## 高亮格式 + +``` +第一段匹配内容 +--- +第二段匹配内容 +--- +第三段匹配内容 +``` + +前端可通过 `description.split("\n---\n")` 解析。 + +## 已确认事项 + +1. **`tagId` / `categoryId` 与 `filterWord` 的关系**: + - `filterWord` 独立控制全文检索 + - `tagId` / `categoryId` 独立控制分类/标签过滤 + - 两者可同时存在:先 ES 全文检索(附带 tagNames/categoryName 过滤),再 MySQL 过滤 + - 如果只有 `tagId`/`categoryId` 没有 `filterWord`,走现有 MySQL 逻辑 + +2. **分页统计 `total`**:返回 ES 命中的文档总数(而非 MySQL 查询结果数) + +3. **空值处理**: + - `tagId` 对应的 Tag 不存在时:跳过 tag 过滤条件 + - `categoryId` 对应的 Category 不存在时:跳过 category 过滤条件 \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-29-stats-api-design.md b/docs/superpowers/specs/2026-04-29-stats-api-design.md new file mode 100644 index 0000000..d913907 --- /dev/null +++ b/docs/superpowers/specs/2026-04-29-stats-api-design.md @@ -0,0 +1,180 @@ +# 文档统计页面后端接口扩展设计 + +## 1. 概述 + +为前端统计页面扩展后端 API,补全统计卡片数据,新增文档类型分布、分类文档分布、热门文档、搜索热词等接口。 + +--- + +## 2. 现有接口扩展 + +### 2.1 GET /statistics/all + +**扩展 `StatsVO` 字段:** + +| 字段 | 类型 | 说明 | 数据来源 | +|------|------|------|----------| +| docNum | Long | 文档总数 | `DocumentRepository.count()` | +| userNum | Long | 用户总数 | `UserRepository.count()` | +| downloadNum | Long | 下载次数 | **暂无,返回 0** | +| searchNum | Long | 搜索次数 | **暂无,返回 0** | +| commentNum | Long | 评论总数 | `CommentRepository.count()` | +| tagNum | Long | 标签总数 | `TagRepository.count()` | +| categoryNum | Long | 分类总数 | `CategoryRepository.count()` | +| viewNum | Long | 浏览次数 | **暂无,返回 0** | + +--- + +## 3. 新增接口 + +### 3.1 GET /statistics/docTypeDist + +**说明:** 统计各文档类型的数量分布 + +**响应:** +```json +{ + "code": 200, + "data": [ + { "type": "pdf", "count": 439 }, + { "type": "docx", "count": 352 } + ] +} +``` + +**新增 VO:** `DocTypeDistVO(type, count)` + +**数据来源:** `file_document.suffix` GROUP BY + +--- + +### 3.2 GET /statistics/categoryDist + +**说明:** 统计各分类下的文档数量 + +**响应:** +```json +{ + "code": 200, + "data": [ + { "category": "产品文档", "count": 286 }, + { "category": "技术文档", "count": 342 } + ] +} +``` + +**新增 VO:** `CategoryDistVO(category, count)` + +**数据来源:** `cate_doc_relationship` + `category` JOIN,COUNT GROUP BY category + +--- + +### 3.3 GET /statistics/hotDocs + +**说明:** 返回热门文档 TOP 10 + +**响应:** +```json +{ + "code": 200, + "data": [ + { "id": 1, "title": "2024年产品路线图.pdf", "viewCount": 2456 }, + { "id": 2, "title": "系统架构设计文档.docx", "viewCount": 2134 } + ] +} +``` + +**新增 VO:** `HotDocVO(id, title, viewCount)` + +**数据来源:** Redis `doc:hot` ZSet,取 TOP 10,再根据 docId 查 `file_document` 表获取 title + +--- + +### 3.4 GET /statistics/searchHotWords + +**说明:** 返回搜索热词排行 + +**响应:** +```json +{ + "code": 200, + "data": [ + { "keyword": "架构设计", "count": 3421 }, + { "keyword": "产品路线图", "count": 2876 } + ] +} +``` + +**新增 VO:** `SearchHotWordVO(keyword, count)` + +**数据来源:** Redis `search:hot` ZSet,取 TOP 10 + +--- + +### 3.5 GET /statistics/userActivity + +**说明:** 用户活跃度趋势 + +**响应:** +```json +{ + "code": 200, + "data": [ + { "month": "2024-01", "activeUsers": 156, "totalUsers": 180 } + ] +} +``` + +**新增 VO:** `UserActivityVO(month, activeUsers, totalUsers)` + +**数据来源:** **暂无,返回空列表** + +--- + +## 4. 新增 VO 类清单 + +| 类名 | 包路径 | 字段 | +|------|--------|------| +| DocTypeDistVO | `domain/entity/vo/` | type(String), count(Long) | +| CategoryDistVO | `domain/entity/vo/` | category(String), count(Long) | +| HotDocVO | `domain/entity/vo/` | id(String), title(String), viewCount(Long) | +| SearchHotWordVO | `domain/entity/vo/` | keyword(String), count(Long) | +| UserActivityVO | `domain/entity/vo/` | month(String), activeUsers(Long), totalUsers(Long) | + +--- + +## 5. 修改文件清单 + +### 5.1 Domain 层 +- `StatsVO.java` - 新增 userNum, downloadNum, searchNum, viewNum 字段 + +### 5.2 Application 层 +- `StatisticsService.java` - 新增 5 个方法声明 +- `StatisticsServiceImpl.java` - 实现新增方法 + +### 5.3 API 层 +- `StatisticsController.java` - 新增 5 个端点 + +### 5.4 Infrastructure 层 +- `DocumentMapper.xml` - 新增 SQL 查询(docTypeDist, categoryDist) +- `DocumentMapper.java` - 新增 mapper 方法 +- `DocumentMybatisRepository.java` - 新增 repository 方法 + +--- + +## 6. 待补充功能(记录到 TODO.md) + +以下功能本次不做实现,后续补充: + +1. **浏览量统计(viewNum)** - 需要在文档浏览接口中增加 Redis/DB 计数 +2. **下载次数统计(downloadNum)** - 需要在文档下载接口中增加 Redis/DB 计数 +3. **搜索次数统计(searchNum)** - 需要在搜索接口中增加计数 +4. **用户活跃度统计(userActivity)** - 需要新增用户活跃度记录机制 + +--- + +## 7. 风险与约束 + +- 浏览量、下载次数、搜索次数、用户活跃度目前无数据源,返回 0 或空列表 +- 热门文档依赖 Redis `doc:hot` ZSet,需确认文档浏览时是否更新该 ZSet +- 搜索热词依赖 Redis `search:hot` ZSet,该机制已存在 diff --git a/pom.xml b/pom.xml index 4796f2b..d434877 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,8 @@ 5.8.0 5.1.6 5.5.13.3 + 2.0.27 + 5.2.5 2.5.0 8.5.7 3.0.3 @@ -140,6 +142,20 @@ ${itextpdf.version} + + + org.apache.pdfbox + pdfbox + ${pdfbox.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + org.springdoc diff --git a/sensitive.txt b/sensitive.txt new file mode 100644 index 0000000..dceb588 --- /dev/null +++ b/sensitive.txt @@ -0,0 +1 @@ +敏感词 \ No newline at end of file diff --git a/stats_API.md b/stats_API.md new file mode 100644 index 0000000..1ac2257 --- /dev/null +++ b/stats_API.md @@ -0,0 +1,224 @@ +# 文档统计页面 - 后端接口需求 + +## 概述 + +文档统计页面需要对后端API进行扩展和完善,以下是各模块需要的数据结构说明。 + +--- + +## 1. 统计卡片数据 + +**接口:** `GET /statistics/all` + +**响应:** +```json +{ + "code": 200, + "data": { + "docNum": 1256, + "userNum": 342, + "downloadNum": 8965, + "searchNum": 23589, + "commentNum": 1856, + "tagNum": 89, + "categoryNum": 12, + "viewNum": 45892 + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| docNum | number | 文档总数 | +| userNum | number | 用户总数 | +| downloadNum | number | 下载次数 | +| searchNum | number | 搜索次数 | +| commentNum | number | 评论总数 | +| tagNum | number | 标签总数 | +| categoryNum | number | 分类总数 | +| viewNum | number | 浏览次数 | + +--- + +## 2. 月度文档上传趋势 + +**接口:** `GET /statistics/monthStat` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "date": "2024-01", "count": 156 }, + { "date": "2024-02", "count": 198 }, + { "date": "2024-03", "count": 245 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| date | string | 月份,格式: YYYY-MM | +| count | number | 该月上传文档数量 | + +--- + +## 3. 文档类型分布 + +**接口:** `GET /statistics/docTypeDist` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "type": "pdf", "count": 439 }, + { "type": "docx", "count": 352 }, + { "type": "xlsx", "count": 226 }, + { "type": "pptx", "count": 151 }, + { "type": "other", "count": 88 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| type | string | 文档类型: pdf/docx/xlsx/pptx/other | +| count | number | 该类型文档数量 | + +--- + +## 4. 分类文档分布 + +**接口:** `GET /statistics/categoryDist` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "category": "产品文档", "count": 286 }, + { "category": "技术文档", "count": 342 }, + { "category": "需求文档", "count": 198 }, + { "category": "运维文档", "count": 156 }, + { "category": "测试文档", "count": 124 }, + { "category": "用户手册", "count": 150 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| category | string | 分类名称 | +| count | number | 该分类文档数量 | + +--- + +## 5. 热门文档 TOP 10 + +**接口:** `GET /statistics/hotDocs` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "id": 1, "title": "2024年产品路线图.pdf", "viewCount": 2456 }, + { "id": 2, "title": "系统架构设计文档.docx", "viewCount": 2134 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | number | 文档ID | +| title | string | 文档标题 | +| viewCount | number | 浏览次数 | + +--- + +## 6. 搜索热词排行 + +**接口:** `GET /statistics/searchHotWords` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "keyword": "架构设计", "count": 3421 }, + { "keyword": "产品路线图", "count": 2876 }, + { "keyword": "API文档", "count": 2543 }, + { "keyword": "财务报告", "count": 2234 }, + { "keyword": "用户手册", "count": 1987 }, + { "keyword": "测试用例", "count": 1765 }, + { "keyword": "部署手册", "count": 1543 }, + { "keyword": "需求调研", "count": 1432 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| keyword | string | 搜索关键词 | +| count | number | 搜索次数 | + +--- + +## 7. 用户活跃度趋势 + +**接口:** `GET /statistics/userActivity` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "month": "2024-01", "activeUsers": 156, "totalUsers": 180 }, + { "month": "2024-02", "activeUsers": 178, "totalUsers": 195 } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| month | string | 月份,格式: YYYY-MM | +| activeUsers | number | 活跃用户数 | +| totalUsers | number | 总用户数 | + +--- + +## 8. 最新文档列表 + +**接口:** `GET /statistics/recentDocs` + +**响应:** +```json +{ + "code": 200, + "data": [ + { "id": 1, "name": "2024年产品路线图.pdf", "type": "pdf", "date": "2024-04-28" }, + { "id": 2, "name": "系统架构设计文档.docx", "type": "docx", "date": "2024-04-27" }, + { "id": 3, "name": "Q1财务数据统计.xlsx", "type": "xlsx", "date": "2024-04-26" } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | number | 文档ID | +| name | string | 文档名称 | +| type | string | 文档类型: pdf/docx/xlsx/pptx/image/zip | +| date | string | 上传日期,格式: YYYY-MM-DD | + +--- + +## 9. 新增后端API清单 + +| 接口 | 方法 | 说明 | +|------|------|------| +| /statistics/docTypeDist | GET | 文档类型分布 | +| /statistics/categoryDist | GET | 分类文档分布 | +| /statistics/hotDocs | GET | 热门文档 TOP 10 | +| /statistics/searchHotWords | GET | 搜索热词排行 | +| /statistics/userActivity | GET | 用户活跃度趋势 | diff --git "a/\346\216\245\345\217\243\346\226\207\346\241\243.md" "b/\346\216\245\345\217\243\346\226\207\346\241\243.md" new file mode 100644 index 0000000..3440dae --- /dev/null +++ "b/\346\216\245\345\217\243\346\226\207\346\241\243.md" @@ -0,0 +1,201 @@ +# 全文档管理页面 接口文档 + +--- + +## 一、新增接口 + +### 1.1 文档搜索列表 + +- **接口名称**: 文档搜索列表 +- **接口地址**: `/document/searchList` +- **请求方式**: `POST` +- **Content-Type**: `application/json` +- **认证**: 需要登录(token) + +#### 请求参数 + +```json +{ + "keyword": "项目", + "fullText": true, + "segment": true, + "searchType": "all", + "tags": ["pdf", "docx"], + "category": "技术文档", + "sortField": "createTime", + "sortOrder": "desc", + "page": 1, + "pageSize": 20 +} +``` + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +|-------|------|------|--------|------| +| keyword | string | 否 | "" | 搜索关键词 | +| fullText | boolean | 否 | false | 是否全文检索(true: 匹配名称+描述+分类;false: 只匹配名称) | +| segment | boolean | 否 | false | 是否分词(true: 开启中文分词搜索) | +| searchType | string | 否 | "all" | 搜索类型:`all`(全部)、`name`(仅名称)、`description`(仅描述) | +| tags | string[] | 否 | [] | 标签筛选数组,如 `["pdf", "docx"]`,支持多选 | +| category | string | 否 | "" | 分类名称筛选,传空字符串表示不限分类 | +| sortField | string | 否 | "name" | 排序字段:`name`、`size`、`type`、`category`、`createTime` | +| sortOrder | string | 否 | "asc" | 排序方向:`asc`(升序)、`desc`(降序) | +| page | int | 否 | 1 | 页码,从 1 开始 | +| pageSize | int | 否 | 20 | 每页条数 | + +#### 响应参数 + +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "id": "doc_001", + "name": "项目需求文档.pdf", + "type": "pdf", + "size": 2048576, + "sizeDisplay": "2 MB", + "description": "2024年Q1项目需求文档,包含功能清单和里程碑", + "category": "需求文档", + "tags": [ + { "name": "重要", "color": "red" }, + { "name": "2024", "color": "blue" } + ], + "liked": false, + "collected": true, + "createTime": "2024-01-15 10:30:00", + "updateTime": "2024-01-20 14:22:00" + } + ], + "total": 100, + "page": 1, + "pageSize": 20 + } +} +``` + +| 字段名 | 类型 | 说明 | +|-------|------|------| +| list | array | 文档列表 | +| list[].id | string | 文档ID | +| list[].name | string | 文档名称(带后缀) | +| list[].type | string | 文档类型(pdf/docx/xlsx/pptx/image/zip) | +| list[].size | long | 文档大小(字节) | +| list[].sizeDisplay | string | 文档大小(格式化,如 "2 MB") | +| list[].description | string | 文档描述 | +| list[].category | string | 所属分类名称 | +| list[].tags | array | 标签列表 | +| list[].tags[].name | string | 标签名称 | +| list[].tags[].color | string | 标签颜色 | +| list[].liked | boolean | 当前用户是否已点赞 | +| list[].collected | boolean | 当前用户是否已收藏 | +| list[].createTime | string | 创建时间(yyyy-MM-dd HH:mm:ss) | +| list[].updateTime | string | 更新时间(yyyy-MM-dd HH:mm:ss) | +| total | int | 总记录数 | +| page | int | 当前页码 | +| pageSize | int | 每页条数 | + +--- + +## 二、已存在接口(可直接使用) + +### 2.1 分类列表 + +- **接口地址**: `/category/all` +- **请求方式**: `GET` + +**响应**: +```json +{ + "code": 200, + "data": [ + { "id": "c1", "name": "产品文档" }, + { "id": "c2", "name": "技术文档" }, + { "id": "c3", "name": "需求文档" } + ] +} +``` + +### 2.2 标签列表 + +- **接口地址**: `/category/all` +- **请求方式**: `GET` +- **请求参数**: `type=TAG` + +**响应**: +```json +{ + "code": 200, + "data": [ + { "id": "t1", "name": "PDF", "value": "pdf", "color": "red" }, + { "id": "t2", "name": "Word", "value": "docx", "color": "blue" } + ] +} +``` + +### 2.3 收藏接口 + +| 操作 | 接口地址 | 请求方式 | 请求参数 | +|-----|---------|---------|---------| +| 添加收藏 | `/collect/auth/insert` | POST | `{ "docId": "doc_001" }` | +| 取消收藏 | `/collect/auth/remove` | DELETE | `{ "docId": "doc_001" }` | + +**响应**: +```json +{ + "code": 200, + "message": "success" +} +``` + +### 2.4 点赞接口 + +| 操作 | 接口地址 | 请求方式 | +|-----|---------|---------| +| 点赞 | `/like/{docId}` | POST | +| 取消点赞 | `/like/{docId}` | DELETE | + +**响应**: +```json +{ + "code": 200, + "message": "success" +} +``` + +### 2.5 文档预览 + +- **接口地址**: `/file/view/{docId}` +- **请求方式**: `GET` +- **返回**: 文件流(图片/PDF等可直接预览) + +### 2.6 文档下载 + +- **接口地址**: `/file/download/{docId}` +- **请求方式**: `GET` +- **返回**: 文件流(blob) + +--- + +## 三、标签类型值定义 + +| 标签名 | 标签值 | 颜色 | +|-------|--------|------| +| PDF | pdf | red | +| Word | docx | blue | +| Excel | xlsx | green | +| PPT | pptx | orange | +| 图片 | image | purple | +| 压缩包 | zip | magenta | + +--- + +## 四、错误码 + +| 错误码 | 说明 | +|-------|------| +| 200 | 成功 | +| 401 | 未登录或token过期 | +| 403 | 无权限 | +| 500 | 服务器内部错误 |