From ff972e9978fd4f384427d24c3521d0af1b5fbac3 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sat, 25 Apr 2026 17:51:02 +0800 Subject: [PATCH 01/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 7 ++++++- .../config/minio/MinioConfig.java | 4 ++-- .../repository/mysql/CommentMapper.java | 6 ++++++ .../repository/mysql/DocReviewMapper.java | 6 ++++++ .../repository/mysql/ThumbnailMapper.java | 4 ++++ .../storage/MinioStorageStrategy.java | 2 +- .../main/resources/mapper/CommentMapper.xml | 15 ++++++++++++++ .../main/resources/mapper/DocReviewMapper.xml | 20 +++++++++++++++++++ .../main/resources/mapper/ThumbnailMapper.xml | 8 ++++++++ 9 files changed, 68 insertions(+), 4 deletions(-) diff --git a/all-docs-bootstrap/src/main/resources/application-dev.yml b/all-docs-bootstrap/src/main/resources/application-dev.yml index 0c262f3..94cec40 100644 --- a/all-docs-bootstrap/src/main/resources/application-dev.yml +++ b/all-docs-bootstrap/src/main/resources/application-dev.yml @@ -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: @@ -75,6 +75,11 @@ all-docs: file-path: sensitive-file: sensitive.txt +# 开发环境 HMAC +hmac: + secret: + key: ${HMAC_SECRET_KEY:dev-hmac-secret-key} + # 异步线程池 async: core-pool-size: 5 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/repository/mysql/CommentMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMapper.java index 6ec2a0c..140f934 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 @@ -22,4 +22,10 @@ public interface CommentMapper { 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/DocReviewMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMapper.java index 51e9ea6..a337569 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 @@ -15,8 +15,14 @@ public interface DocReviewMapper { 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/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/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/CommentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml index 26079ea..f52178c 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml @@ -42,4 +42,19 @@ DELETE FROM comment WHERE doc_id = #{docId} + + + + DELETE FROM comment WHERE id IN + + #{id} + + + + + \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml index 8136192..54b40d2 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml @@ -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/ThumbnailMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml index b45e08e..0181772 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml @@ -27,4 +27,12 @@ DELETE FROM thumbnail WHERE object_id = #{objectId} + + + + \ No newline at end of file From 6512292c2aa8848ecef421d32dd78137143ec5c0 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sun, 26 Apr 2026 10:07:13 +0800 Subject: [PATCH 02/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 12 +- .../api/controller/DocLogController.java | 2 +- .../api/controller/DocReviewController.java | 2 +- .../api/controller/FileController.java | 51 +++-- .../api/controller/StatisticsController.java | 5 +- .../controller/SystemConfigController.java | 4 +- .../api/controller/UserController.java | 13 +- .../com/jiaruiblog/api/filter/JwtFilter.java | 12 +- .../java/com/jiaruiblog/api/util/JwtUtil.java | 24 +- all-docs-application/pom.xml | 19 ++ .../application/config/QuartzConfig.java | 36 --- .../application/service/DocumentService.java | 3 +- .../application/service/IUserService.java | 5 +- .../application/service/LikeService.java | 21 +- .../application/service/ThumbnailService.java | 21 ++ .../service/impl/CategoryServiceImpl.java | 3 + .../service/impl/DocReviewServiceImpl.java | 2 + .../service/impl/DocumentServiceImpl.java | 177 +++++++++++++-- .../service/impl/LikeServiceImpl.java | 212 ++++-------------- .../service/impl/TagServiceImpl.java | 2 + .../service/impl/ThumbnailServiceImpl.java | 83 ++++++- .../service/impl/UserServiceImpl.java | 23 +- .../task/executor/PdfWordTaskExecutor.java | 20 +- .../task/executor/PicExecutor.java | 22 +- .../task/executor/TaskExecutor.java | 19 +- .../task/executor/slider/PptExecutor.java | 20 +- .../task/executor/slider/PptxExecutor.java | 21 +- .../application/task/like/LikeTask.java | 29 --- .../application/task/like/UserLikeDetail.java | 24 -- .../application/task/thread/MainTask.java | 2 + .../transformer/DTO2BOConverter.java | 1 + all-docs-bootstrap/pom.xml | 4 + .../src/main/resources/application-dev.yml | 23 +- .../src/main/resources/application-prod.yml | 22 +- .../common/constants/StorageConstants.java | 38 +++- .../StringCodeToEnumConverterFactory.java | 2 +- all-docs-domain/pom.xml | 1 - .../jiaruiblog/domain/entity/bo/UserBO.java | 2 + .../jiaruiblog/domain/entity/data/Event.java | 2 + .../domain/entity/dto/CommentListDTO.java | 2 + .../domain/entity/dto/CommentWithUserDTO.java | 2 + .../domain/entity/dto/DocumentDTO.java | 2 + .../domain/entity/dto/FileDocumentDTO.java | 2 +- .../entity/dto/QueryDocByTagCateDTO.java | 2 + .../domain/entity/dto/RefuseBatchDTO.java | 2 + .../domain/entity/dto/RegistryUserDTO.java | 3 + .../jiaruiblog/domain/entity/dto/UserDTO.java | 19 +- .../domain/entity/po/DocReview.java | 8 +- .../domain/entity/po/FileDocument.java | 2 +- .../domain/entity/po/LikeDocRelationship.java | 7 +- .../config/ElasticSearchConfig.java | 43 +++- .../config/datasource/MyBatisConfig.java | 15 +- .../config/mybatis/EnumTypeHandler.java | 16 +- .../mysql/CategoryMybatisRepository.java | 8 +- .../repository/mysql/DocReviewMapper.java | 2 +- .../repository/mysql/TagMapper.java | 2 +- .../repository/mysql/UserMapper.java | 2 +- .../mysql/UserMybatisRepository.java | 2 +- .../main/resources/mapper/DocReviewMapper.xml | 8 +- .../main/resources/mapper/DocumentMapper.xml | 5 +- pom.xml | 16 ++ 61 files changed, 730 insertions(+), 424 deletions(-) delete mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/config/QuartzConfig.java delete mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/task/like/LikeTask.java delete mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/task/like/UserLikeDetail.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 78e1437..9850c77 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -38,7 +38,17 @@ "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)" ] } } 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/FileController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/FileController.java index 774eafd..6d16ef2 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 { @@ -278,19 +277,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); @@ -552,9 +548,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/StatisticsController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java index c06357e..adb88ce 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 @@ -121,7 +121,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..666b182 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"; @@ -40,6 +41,10 @@ public class JwtFilter implements Filter { "/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 +114,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..f72bee5 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 @@ -8,7 +8,6 @@ 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; @@ -32,7 +31,7 @@ public interface DocumentService { /** * 用户上传文档 */ - void documentUpload(MultipartFile file, String userId, String username) throws AuthenticationException; + void documentUpload(MultipartFile file, String userId, String username); /** * 批量上传 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..bdee79f 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; @@ -25,9 +26,9 @@ public interface IUserService { /** * 注册用户 - * @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/ThumbnailService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/ThumbnailService.java index 09ad4c6..a44349f 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,25 @@ 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); } \ No newline at end of file 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..a80e570 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 @@ -31,6 +31,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -71,6 +72,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 +119,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); 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..cb1b3ef 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 @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.UUID; /** * @author luojiarui @@ -40,6 +41,7 @@ public void insert(FileDocument document) { return; } DocReview review = new DocReview(); + review.setId(UUID.randomUUID().toString()); review.setDocId(document.getId()); review.setUserId(document.getUserId()); review.setCreateDate(new Date()); 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..84b4b45 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,16 +1,18 @@ package com.jiaruiblog.application.service.impl; import cn.hutool.core.util.IdUtil; +import com.jiaruiblog.application.service.CollectService; +import com.jiaruiblog.application.service.DocReviewService; 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.TaskExecuteService; import com.jiaruiblog.common.constants.StorageConstants; import com.jiaruiblog.common.enums.DocStateEnum; -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.document.UpdateInfoDTO; +import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.vo.DocWithCateVO; import com.jiaruiblog.domain.entity.vo.DocumentVO; import com.jiaruiblog.domain.entity.vo.PageVO; @@ -23,9 +25,11 @@ import org.springframework.stereotype.Service; 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; /** * @author jiarui.luo @@ -49,6 +53,12 @@ public class DocumentServiceImpl implements DocumentService { @Resource private ElasticService elasticService; + @Resource + private TaskExecuteService taskExecuteService; + + @Resource + private DocReviewService docReviewService; + private static final String FILE_NAME = "filename"; @Override @@ -57,12 +67,12 @@ 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 @@ -113,7 +123,7 @@ 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) { @@ -256,8 +266,80 @@ public FileDocument saveFile(String md5, MultipartFile file) { @Override public void documentUpload(MultipartFile file, String userId, String username) { - // Implementation for document upload - log.info("Document upload: userId={}, username={}", userId, username); + if (file == null || file.isEmpty()) { + log.warn("Document upload failed: file is empty"); + return; + } + + 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 = documentRepository.findByMd5(md5); + if (existing != null) { + log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); + return; + } + + // 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()); + documentRepository.save(document); + + // 6. Create review record + docReviewService.insert(document); + + // 7. 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()); + } catch (IOException e) { + log.error("Document upload failed: userId={}, username={}, error={}", + userId, username, e.getMessage()); + } + } + + 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 +349,66 @@ 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 = documentRepository.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()); + documentRepository.save(document); + + // 7. Create review record + docReviewService.insert(document); + + // 8. 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); + } catch (Exception e) { + log.error("Upload by URL failed: userId={}, username={}, url={}, error={}", + userId, username, url, e.getMessage()); + } } @Override @@ -324,7 +465,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())); } } } @@ -389,7 +530,7 @@ public PageVO list(DocumentDTO documentDTO) { List documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); List voList = documents.stream() .map(doc -> convertDocument(new DocumentVO(), doc)) - .collect(Collectors.toList()); + .toList(); return PageVO.builder() .pageNum(documentDTO.getPage()) .pageSize(documentDTO.getRows()) @@ -568,8 +709,8 @@ public PageVO search(String keyword, int pageNum, int pageSize) { // Step 2: Query MySQL, filter by reviewing=false AND docState=SUCCESS java.util.List allMatchedDocs = documentRepository.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 +721,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) 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..c453361 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,17 @@ 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 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 +26,36 @@ public class LikeServiceImpl implements LikeService { @Resource private RedisService redisService; - // 对实体进行点赞的类型 - // 0: entityType,1表示点赞;2表示收藏信息 - // 1: 用户信息 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); + 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); + insertRelationShip(like); + log.debug("用户 {} 点赞实体 {}:{}", userId, entityType, entityId); + return true; } } @@ -55,7 +66,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 获取 + return collectService.collectNum(entityId); } catch (Exception e) { log.error("查询点赞数量失败: entityType={}, entityId={}", entityType, entityId, e); return 0L; @@ -82,12 +98,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 +117,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 +134,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,12 +149,10 @@ 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); } catch (Exception e) { log.error("查询点赞数量失败: docId={}", docId, e); @@ -150,156 +166,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/TagServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TagServiceImpl.java index 3137d92..c76fdae 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); @@ -201,6 +202,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..35e23bf 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,6 +7,9 @@ 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.springframework.stereotype.Service; import javax.imageio.ImageIO; @@ -78,6 +81,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,7 +102,9 @@ public String makePreview(InputStream inputStream, String fileName) { ImageIO.write(previewImage, "jpg", baos); byte[] previewBytes = baos.toByteArray(); - String previewId = UUID.randomUUID().toString(); + if (previewId == null || previewId.isEmpty()) { + previewId = UUID.randomUUID().toString(); + } String objectKey = StorageConstants.previewPath(previewId); String result = minioStorageStrategy.upload(new ByteArrayInputStream(previewBytes), objectKey, "image/jpeg"); @@ -111,4 +121,75 @@ 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); + + 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 ""; + } + } } 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..ae63486 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()); @@ -115,7 +117,7 @@ public Map login(RegistryUserDTO userDTO) { } @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()); } 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..4040758 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,8 +1,10 @@ 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.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; import lombok.extern.slf4j.Slf4j; import java.io.*; @@ -38,6 +40,22 @@ protected void makeThumb(InputStream is, String picPath) throws IOException { @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - // 预览文件暂未实现 + if (is == null) { + log.warn("PDF预览图生成失败:输入流为空"); + return; + } + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + FileDocument fileDocument = taskData.getFileDocument(); + // 使用 md5 + filename 作为预览图ID + String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makePreviewForPdf(is, fileDocument.getName(), previewId); + if (result != null && !result.isEmpty()) { + fileDocument.setPreviewFileId(previewId); + log.info("PDF预览图生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); + } + } catch (Exception e) { + log.error("PDF预览图生成异常: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/PicExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/PicExecutor.java index 7a09a29..c9b3611 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,23 @@ protected void makeThumb(InputStream is, String picPath) throws IOException { @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - // do nothing for pic + if (is == null) { + log.warn("图片预览图生成失败:输入流为空"); + return; + } + try { + ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); + FileDocument fileDocument = taskData.getFileDocument(); + // 使用 md5 + filename 作为预览图ID,保持与文档路径一致 + String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); + String result = thumbnailService.makePreview(is, fileDocument.getName(), previewId); + if (result != null && !result.isEmpty()) { + fileDocument.setPreviewFileId(previewId); + log.info("图片预览图生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); + } + } catch (Exception e) { + log.error("图片预览图生成异常:docId={}", taskData.getFileDocument().getId(), e); + } } 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..3a92a5f 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; @@ -92,12 +95,18 @@ 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(); + String textObjectKey = md5 + "_" + originalName + ".txt"; + 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); 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..0721a91 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,11 @@ 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.common.util.SpringApplicationContext; +import com.jiaruiblog.domain.entity.po.FileDocument; import lombok.extern.slf4j.Slf4j; import java.io.*; @@ -39,6 +41,22 @@ protected void makeThumb(InputStream is, String picPath) throws IOException { @Override protected void makePreviewFile(InputStream inStream, TaskData taskData) { - // 预览文件暂未实现 + 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-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 94cec40..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 @@ -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: @@ -86,3 +100,8 @@ async: 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-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..e21d236 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,10 +17,16 @@ 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} @@ -47,11 +53,20 @@ 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"; } /** @@ -90,4 +105,13 @@ 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; + } } 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..2ec50d3 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 @@ -16,7 +16,7 @@ **/ public class StringCodeToEnumConverterFactory implements ConverterFactory { private static final Map CONVERTERS = - Collections.unmodifiableMap(new ConcurrentHashMap<>()); + new ConcurrentHashMap<>(); /** * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现 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/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/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/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..539b43a 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 @@ -89,7 +89,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..20ce5c8 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(兼容方法) 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/datasource/MyBatisConfig.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/datasource/MyBatisConfig.java index ba9e35e..19e5beb 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,11 @@ package com.jiaruiblog.infrastructure.config.datasource; +import com.jiaruiblog.common.enums.DocStateEnum; +import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.infrastructure.config.mybatis.EnumTypeHandler; 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 +16,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 +23,10 @@ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Excepti factory.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml") ); + factory.setTypeHandlers( + new EnumTypeHandler<>(PermissionEnum.class), + new EnumTypeHandler<>(DocStateEnum.class) + ); return factory.getObject(); } } \ 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/repository/mysql/CategoryMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CategoryMybatisRepository.java index 3c25571..e1771f3 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 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 a337569..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,7 +9,7 @@ @Mapper public interface DocReviewMapper { - DocReview save(DocReview docReview); + int save(DocReview docReview); void saveAll(@Param("list") List docReviews); 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/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/resources/mapper/DocReviewMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocReviewMapper.xml index 54b40d2..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 @@ - - - - + + + + diff --git a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml index d5ca0dc..45268da 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml @@ -18,7 +18,7 @@ - + @@ -41,6 +41,9 @@ text_file_id = #{textFileId}, thumb_id = #{thumbId}, preview_file_id = #{previewFileId}, + doc_state = #{docState}, + error_msg = #{errorMsg}, + reviewing = #{reviewing}, update_date = NOW() WHERE id = #{id} 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 From 0e04c2812af412f31fd627eef0f3e99cadc0ebb8 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sun, 26 Apr 2026 10:07:56 +0800 Subject: [PATCH 03/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=813?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/config/JwtFilterConfig.java | 23 ++++++ .../application/service/ITokenService.java | 23 ++++++ .../service/impl/TokenServiceImpl.java | 68 ++++++++++++++++++ .../domain/entity/dto/BasicRegistryDTO.java | 48 +++++++++++++ .../config/mybatis/BooleanTypeHandler.java | 43 +++++++++++ .../mysql/CommentMybatisRepository.java | 71 +++++++++++++++++++ .../mysql/DocLogMybatisRepository.java | 61 ++++++++++++++++ .../mysql/DocReviewMybatisRepository.java | 63 ++++++++++++++++ .../mysql/ThumbnailMybatisRepository.java | 50 +++++++++++++ sensitive.txt | 1 + 10 files changed, 451 insertions(+) create mode 100644 all-docs-api/src/main/java/com/jiaruiblog/api/config/JwtFilterConfig.java create mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/service/ITokenService.java create mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/TokenServiceImpl.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/BasicRegistryDTO.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/BooleanTypeHandler.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocReviewMybatisRepository.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/ThumbnailMybatisRepository.java create mode 100644 sensitive.txt 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-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/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-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-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/repository/mysql/CommentMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java new file mode 100644 index 0000000..2028275 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/CommentMybatisRepository.java @@ -0,0 +1,71 @@ +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 Comment 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 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/DocLogMybatisRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java new file mode 100644 index 0000000..438f784 --- /dev/null +++ b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocLogMybatisRepository.java @@ -0,0 +1,61 @@ +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 DocLog 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 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/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/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/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 From 401f351f01b3199a3c0eacec5138f68ee078df1c Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sun, 26 Apr 2026 20:48:56 +0800 Subject: [PATCH 04/37] docs: add getHotTrend fix design spec Co-Authored-By: Claude Opus 4.6 --- .../mybatis/DocStateEnumTypeHandler.java | 0 .../mybatis/PermissionEnumTypeHandler.java | 0 .../mybatis/ThumbSizeEnumTypeHandler.java | 0 .../2026-04-26-getHotTrend-fix-design.md | 89 +++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/DocStateEnumTypeHandler.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/PermissionEnumTypeHandler.java create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/ThumbSizeEnumTypeHandler.java create mode 100644 docs/superpowers/specs/2026-04-26-getHotTrend-fix-design.md 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..e69de29 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..e69de29 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..e69de29 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 From 2a91f8dbce517e99365b33f90b5e57f3649f17b9 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sun, 26 Apr 2026 21:55:08 +0800 Subject: [PATCH 05/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=814?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- .../api/controller/DocumentController.java | 9 +- .../api/controller/LikeController.java | 2 +- .../api/controller/StatisticsController.java | 5 +- .../application/service/IDocLogService.java | 8 -- .../service/impl/CategoryServiceImpl.java | 4 +- .../service/impl/CommentServiceImpl.java | 28 ++++- .../service/impl/DocLogServiceImpl.java | 34 +++--- .../service/impl/DocReviewServiceImpl.java | 2 + .../service/impl/DocumentServiceImpl.java | 110 ++++++++++++++++-- .../service/impl/RedisServiceImpl.java | 19 +-- .../service/impl/ThumbnailServiceImpl.java | 4 +- .../task/executor/TaskExecutor.java | 3 +- all-docs-common/pom.xml | 4 + .../common/constants/StorageConstants.java | 10 ++ .../converter/DocStateEnumConverter.java | 34 ++++++ .../converter/PermissionEnumConverter.java | 34 ++++++ .../converter/RedisActionEnumConverter.java | 34 ++++++ .../converter/ThumbSizeEnumConverter.java | 34 ++++++ .../converter/ThumbnailEnumConverter.java | 34 ++++++ .../common/enums/PermissionEnum.java | 4 - .../entity/po/CollectDocRelationship.java | 7 +- .../jiaruiblog/domain/entity/po/Comment.java | 5 +- .../jiaruiblog/domain/entity/po/DocLog.java | 4 - .../domain/entity/po/FileDocument.java | 5 - .../domain/entity/po/LikeDocRelationship.java | 7 -- .../domain/entity/po/Thumbnail.java | 7 +- .../com/jiaruiblog/domain/entity/po/User.java | 11 -- .../config/datasource/MyBatisConfig.java | 13 ++- .../mybatis/DocStateEnumTypeHandler.java | 16 +++ .../mybatis/PermissionEnumTypeHandler.java | 9 ++ .../mybatis/ThumbSizeEnumTypeHandler.java | 9 ++ .../repository/CommentRepository.java | 4 +- .../repository/DocLogRepository.java | 4 +- .../repository/DocumentRepository.java | 8 ++ .../repository/mysql/CommentMapper.java | 4 +- .../mysql/CommentMybatisRepository.java | 7 +- .../repository/mysql/DocLogMapper.java | 4 +- .../mysql/DocLogMybatisRepository.java | 7 +- .../repository/mysql/DocumentMapper.java | 8 ++ .../mysql/DocumentMybatisRepository.java | 28 +++++ .../main/resources/mapper/CollectMapper.xml | 2 +- .../main/resources/mapper/CommentMapper.xml | 4 + .../main/resources/mapper/DocLogMapper.xml | 4 + .../main/resources/mapper/DocumentMapper.xml | 34 +++++- .../main/resources/mapper/ThumbnailMapper.xml | 4 +- .../src/main/resources/mapper/UserMapper.xml | 12 +- 47 files changed, 527 insertions(+), 119 deletions(-) create mode 100644 all-docs-common/src/main/java/com/jiaruiblog/common/converter/DocStateEnumConverter.java create mode 100644 all-docs-common/src/main/java/com/jiaruiblog/common/converter/PermissionEnumConverter.java create mode 100644 all-docs-common/src/main/java/com/jiaruiblog/common/converter/RedisActionEnumConverter.java create mode 100644 all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbSizeEnumConverter.java create mode 100644 all-docs-common/src/main/java/com/jiaruiblog/common/converter/ThumbnailEnumConverter.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9850c77..8341ecf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -48,7 +48,8 @@ "Bash(mvn spring-boot:run -pl all-docs-bootstrap)", "Bash(grep:*)", "Bash(ls -la *.md *.sql)", - "Bash(mvn dependency:tree)" + "Bash(mvn dependency:tree)", + "Bash(git:*)" ] } } 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..700c3c7 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 @@ -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); } 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..96251dc 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,7 +28,7 @@ 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()); 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 adb88ce..459eb1a 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 @@ -81,8 +81,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 +93,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); } 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/impl/CategoryServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java index a80e570..c21c2ad 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 @@ -387,10 +387,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<>(); } 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..6df550d 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,5 +1,6 @@ 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.dto.BasePageDTO; @@ -37,6 +38,8 @@ public void insert(Comment comment) { return; } // 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); @@ -125,21 +128,36 @@ public long countAllFile() { 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<>(); + List comments; + if (Boolean.TRUE.equals(isAdmin)) { + comments = commentRepository.findAll(); + } else { + comments = commentRepository.findByUserId(userId); + } + long count = comments.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; + comments = comments.stream() + .skip(skip) + .limit(pageSize) + .toList(); + + List commentWithUserVOList = new ArrayList<>(); for (Comment comment : comments) { CommentWithUserVO vo = new CommentWithUserVO(); BeanUtils.copyProperties(comment, vo); commentWithUserVOList.add(vo); } - long count = commentRepository.count(); return PageVO.builder() .total((int) count) .list(commentWithUserVOList) - .pageNum(page.getPage()) - .pageSize(page.getRows()) + .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 cb1b3ef..c6bce1c 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 @@ -43,7 +43,9 @@ public void insert(FileDocument document) { 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()); 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 84b4b45..4eca145 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,28 +1,31 @@ package com.jiaruiblog.application.service.impl; import cn.hutool.core.util.IdUtil; -import com.jiaruiblog.application.service.CollectService; -import com.jiaruiblog.application.service.DocReviewService; -import com.jiaruiblog.application.service.DocumentService; -import com.jiaruiblog.application.service.ElasticService; -import com.jiaruiblog.application.service.ICommentService; -import com.jiaruiblog.application.service.TaskExecuteService; +import com.jiaruiblog.application.service.*; import com.jiaruiblog.common.constants.StorageConstants; import com.jiaruiblog.common.enums.DocStateEnum; +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.document.UpdateInfoDTO; +import com.jiaruiblog.domain.entity.po.CateDocRelationship; import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.po.TagDocRelationship; +import com.jiaruiblog.domain.entity.vo.CategoryVO; import com.jiaruiblog.domain.entity.vo.DocWithCateVO; import com.jiaruiblog.domain.entity.vo.DocumentVO; import com.jiaruiblog.domain.entity.vo.PageVO; +import com.jiaruiblog.domain.entity.vo.TagVO; import com.jiaruiblog.infrastructure.repository.DocumentRepository; +import com.jiaruiblog.infrastructure.repository.mysql.CateDocRelationshipMapper; +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; @@ -59,6 +62,12 @@ public class DocumentServiceImpl implements DocumentService { @Resource private DocReviewService docReviewService; + @Resource + private CateDocRelationshipMapper cateDocRelationshipMapper; + + @Resource + private TagDocRelationshipMapper tagDocRelationshipMapper; + private static final String FILE_NAME = "filename"; @Override @@ -527,14 +536,31 @@ public PageVO list(DocumentDTO documentDTO) { if (documentDTO == null) { return PageVO.builder().build(); } - List documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); + + List documents; + long total; + + FilterTypeEnum type = documentDTO.getType(); + if (type == FilterTypeEnum.TAG && StringUtils.hasText(documentDTO.getTagId())) { + documents = documentRepository.findByPageByTag(documentDTO.getTagId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentRepository.countByTagId(documentDTO.getTagId()); + } else if (type == FilterTypeEnum.CATEGORY && StringUtils.hasText(documentDTO.getCategoryId())) { + documents = documentRepository.findByPageByCategory(documentDTO.getCategoryId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentRepository.countByCategoryId(documentDTO.getCategoryId()); + } else { + documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); + total = documentRepository.count(); + } + List voList = documents.stream() .map(doc -> convertDocument(new DocumentVO(), doc)) .toList(); return PageVO.builder() .pageNum(documentDTO.getPage()) .pageSize(documentDTO.getRows()) - .total(documentRepository.count()) + .total(total) .list(voList) .build(); } @@ -572,7 +598,73 @@ public void updateInfo(UpdateInfoDTO updateInfoDTO) { @Override public PageVO listWithCategory(DocumentDTO documentDTO) { - return PageVO.builder().build(); + if (documentDTO == null) { + return PageVO.builder().build(); + } + + List documents; + long total; + + FilterTypeEnum type = documentDTO.getType(); + if (type == FilterTypeEnum.TAG && StringUtils.hasText(documentDTO.getTagId())) { + documents = documentRepository.findByPageByTag(documentDTO.getTagId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentRepository.countByTagId(documentDTO.getTagId()); + } else if (type == FilterTypeEnum.CATEGORY && StringUtils.hasText(documentDTO.getCategoryId())) { + documents = documentRepository.findByPageByCategory(documentDTO.getCategoryId(), + documentDTO.getPage(), documentDTO.getRows()); + total = documentRepository.countByCategoryId(documentDTO.getCategoryId()); + } else { + return PageVO.builder().build(); + } + + 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 + if (StringUtils.hasText(categoryId)) { + CategoryVO categoryVO = new CategoryVO(); + categoryVO.setId(categoryId); + // Get category name from relationship if needed + List cateRels = cateDocRelationshipMapper.findByFileId(doc.getId()); + cateRels.stream() + .filter(rel -> rel.getCategoryId().equals(categoryId)) + .findFirst() + .ifPresent(rel -> { + categoryVO.setRelationShipId(rel.getId()); + }); + vo.setCategoryVO(categoryVO); + } + + // Build tag list info + List tagRels = tagDocRelationshipMapper.findByFileId(doc.getId()); + List tagVOList = tagRels.stream() + .map(rel -> { + TagVO tagVO = new TagVO(); + tagVO.setId(rel.getTagId()); + return tagVO; + }) + .toList(); + vo.setTagVOList(tagVOList); + + return vo; } @Override 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..d6901bb 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,6 @@ 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); } } \ No newline at end of file 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 35e23bf..e8cd264 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 @@ -105,7 +105,7 @@ public String makePreview(InputStream inputStream, String fileName, String previ if (previewId == null || previewId.isEmpty()) { previewId = UUID.randomUUID().toString(); } - String objectKey = StorageConstants.previewPath(previewId); + String objectKey = StorageConstants.previewPath(previewId, "jpg"); String result = minioStorageStrategy.upload(new ByteArrayInputStream(previewBytes), objectKey, "image/jpeg"); @@ -144,7 +144,7 @@ public String makePreviewForPdf(InputStream inputStream, String fileName, String if (previewId == null || previewId.isEmpty()) { previewId = UUID.randomUUID().toString(); } - String objectKey = StorageConstants.previewPath(previewId); + String objectKey = StorageConstants.previewPath(previewId, "jpg"); String result = minioStorageStrategy.upload(new ByteArrayInputStream(previewBytes), objectKey, "image/jpeg"); 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 3a92a5f..9ec8838 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 @@ -98,7 +98,8 @@ public void uploadFileToEs(InputStream is, FileDocument fileDocument, TaskData t // 使用 document-texts/{md5}_{originalName}.txt 路径存储文本文件 String originalName = fileDocument.getName(); String md5 = fileDocument.getMd5(); - String textObjectKey = md5 + "_" + originalName + ".txt"; + // textObjectKey 已经包含 .txt 后缀,documentTextPath 会再添加一次,所以传入时不带 .txt + String textObjectKey = md5 + "_" + originalName; String fullPath = StorageConstants.documentTextPath(textObjectKey); DocumentService fileService = SpringApplicationContext.getBean(DocumentService.class); 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 e21d236..ff57c1e 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 @@ -114,4 +114,14 @@ public static String previewPath(String uniqueKey) { 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/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-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..09f8a5f 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,13 +12,12 @@ 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; 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/FileDocument.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/po/FileDocument.java index 539b43a..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; /** 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 20ce5c8..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 @@ -51,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/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-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 19e5beb..f5b306d 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 @@ -2,6 +2,11 @@ import com.jiaruiblog.common.enums.DocStateEnum; import com.jiaruiblog.common.enums.PermissionEnum; +import com.jiaruiblog.common.enums.RedisActionEnum; +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 org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; @@ -24,9 +29,13 @@ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Excepti new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml") ); factory.setTypeHandlers( + new BooleanTypeHandler(), new EnumTypeHandler<>(PermissionEnum.class), - new EnumTypeHandler<>(DocStateEnum.class) + new DocStateEnumTypeHandler(), + new EnumTypeHandler<>(RedisActionEnum.class), + 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/mybatis/DocStateEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/DocStateEnumTypeHandler.java index e69de29..d63d173 100644 --- 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 @@ -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/PermissionEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/PermissionEnumTypeHandler.java index e69de29..866e609 100644 --- 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 @@ -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/ThumbSizeEnumTypeHandler.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/ThumbSizeEnumTypeHandler.java index e69de29..e9a6045 100644 --- 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 @@ -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..d377568 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 @@ -43,4 +43,12 @@ 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); } \ 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 140f934..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,6 +17,8 @@ public interface CommentMapper { List findByUserId(@Param("userId") String userId); + List findAll(); + long countByDocId(@Param("docId") String docId); void deleteById(@Param("id") String id); 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 index 2028275..2b9a9a3 100644 --- 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 @@ -20,7 +20,7 @@ public class CommentMybatisRepository implements CommentRepository { private CommentMapper commentMapper; @Override - public Comment save(Comment comment) { + public int save(Comment comment) { return commentMapper.save(comment); } @@ -39,6 +39,11 @@ 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); 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 index 438f784..f9eef38 100644 --- 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 @@ -20,7 +20,7 @@ public class DocLogMybatisRepository implements DocLogRepository { private DocLogMapper docLogMapper; @Override - public DocLog save(DocLog docLog) { + public int save(DocLog docLog) { return docLogMapper.save(docLog); } @@ -39,6 +39,11 @@ 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); 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..da3422f 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 @@ -38,4 +38,12 @@ 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); } \ 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..7990a82 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 @@ -95,4 +95,32 @@ 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); + } } \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml index 88c3df2..c94f780 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 @@ - + diff --git a/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml index f52178c..eecfe28 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CommentMapper.xml @@ -30,6 +30,10 @@ SELECT * FROM comment WHERE user_id = #{userId} ORDER BY create_date DESC + + 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/DocumentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml index 45268da..70c27c9 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml @@ -16,7 +16,7 @@ - + @@ -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,7 +41,7 @@ text_file_id = #{textFileId}, thumb_id = #{thumbId}, preview_file_id = #{previewFileId}, - doc_state = #{docState}, + doc_state = #{docState,typeHandler=com.jiaruiblog.infrastructure.config.mybatis.DocStateEnumTypeHandler}, error_msg = #{errorMsg}, reviewing = #{reviewing}, update_date = NOW() @@ -121,4 +121,32 @@ ORDER BY date ASC + + + + + + + + \ No newline at end of file diff --git a/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/ThumbnailMapper.xml index 0181772..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 @@ - + - + diff --git a/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml index 9200c20..9a6aa24 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 @@ - + - - + + @@ -32,7 +32,7 @@ INSERT INTO user (id, username, password, phone, mail, male, description, avatar, birthtime, banning, permission_enum, nickname, last_login, create_date, update_date) VALUES (#{id}, #{username}, #{password}, #{phone}, #{mail}, #{male}, #{description}, - #{avatar}, #{birthtime}, #{banning}, #{permissionEnum}, #{nickname}, + #{avatar}, #{birthtime}, #{banning}, #{permissionEnum, jdbcType=INTEGER}, #{nickname}, #{lastLogin}, #{createDate}, #{updateDate}) @@ -47,7 +47,7 @@ avatar = #{avatar}, birthtime = #{birthtime}, banning = #{banning}, - permission_enum = #{permissionEnum}, + permission_enum = #{permissionEnum, jdbcType=INTEGER}, nickname = #{nickname}, update_date = NOW() WHERE id = #{id} @@ -69,7 +69,7 @@ INSERT INTO user (id, username, password, phone, mail, male, description, avatar, birthtime, banning, permission_enum, nickname, last_login, create_date, update_date) VALUES (#{id}, #{username}, #{password}, #{phone}, #{mail}, #{male}, #{description}, - #{avatar}, #{birthtime}, #{banning}, #{permissionEnum}, #{nickname}, + #{avatar}, #{birthtime}, #{banning}, #{permissionEnum, jdbcType=INTEGER}, #{nickname}, #{lastLogin}, #{createDate}, #{updateDate}) ON DUPLICATE KEY UPDATE username = VALUES(username), From 00c5219d70f0f54bd35de3b5022bcd77a30c7df9 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 14:42:47 +0800 Subject: [PATCH 06/37] =?UTF-8?q?docs:=20add=20queryAllComments=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- ...6-04-27-comment-queryallcomments-design.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-27-comment-queryallcomments-design.md 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) +``` From 0437e6d809319f33ee35a06cc2ecf80b8ba573c6 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 14:47:15 +0800 Subject: [PATCH 07/37] =?UTF-8?q?docs:=20add=20queryAllComments=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=AE=9E=E7=8E=B0=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- ...026-04-27-comment-queryallcomments-plan.md | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-27-comment-queryallcomments-plan.md 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` 有值 From ab276c808614486c7a917c6fafac89b4ee809d77 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 15:05:47 +0800 Subject: [PATCH 08/37] =?UTF-8?q?refactor:=20queryAllComments=E8=BF=94?= =?UTF-8?q?=E5=9B=9EPageVO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service层不再构建VO,Controller层负责组装 Co-Authored-By: Claude Opus 4.6 --- .../application/service/ICommentService.java | 3 +-- .../service/impl/CommentServiceImpl.java | 15 +++------------ 2 files changed, 4 insertions(+), 14 deletions(-) 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/impl/CommentServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CommentServiceImpl.java index 6df550d..e80a78b 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 @@ -125,9 +125,8 @@ 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; if (Boolean.TRUE.equals(isAdmin)) { comments = commentRepository.findAll(); @@ -137,7 +136,6 @@ public PageVO queryAllComments(BasePageDTO page, String userI long count = comments.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; @@ -146,16 +144,9 @@ public PageVO queryAllComments(BasePageDTO page, String userI .limit(pageSize) .toList(); - List commentWithUserVOList = new ArrayList<>(); - for (Comment comment : comments) { - CommentWithUserVO vo = new CommentWithUserVO(); - BeanUtils.copyProperties(comment, vo); - commentWithUserVOList.add(vo); - } - - return PageVO.builder() + return PageVO.builder() .total((int) count) - .list(commentWithUserVOList) + .list(comments) .pageNum(pageNum) .pageSize(pageSize) .build(); From 369b1510e5384ee6bbb066d8618d277d8886ac7d Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 20:30:15 +0800 Subject: [PATCH 09/37] =?UTF-8?q?feat:=20add=20CommentConverter=E6=89=8B?= =?UTF-8?q?=E5=86=99=E5=AD=97=E6=AE=B5=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../service/converter/CommentConverter.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java 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..9176df0 --- /dev/null +++ b/all-docs-application/src/main/java/com/jiaruiblog/application/service/converter/CommentConverter.java @@ -0,0 +1,22 @@ +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; + } +} From 814f7929f351f23db7fea5f453d965cc6d43a213 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 20:38:57 +0800 Subject: [PATCH 10/37] =?UTF-8?q?refactor:=20Controller=E5=B1=82=E7=BB=84?= =?UTF-8?q?=E8=A3=85CommentWithUserVO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../api/controller/CommentController.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) 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..ff5ff14 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,38 @@ 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 -> 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(); + } } \ No newline at end of file From f3741e6916bf7f833416cd1b548c47d6d8d49451 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 20:44:35 +0800 Subject: [PATCH 11/37] =?UTF-8?q?fix:=20CommentConverter=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84updateDate=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../application/service/converter/CommentConverter.java | 1 - 1 file changed, 1 deletion(-) 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 index 9176df0..4f7fbf1 100644 --- 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 @@ -16,7 +16,6 @@ public CommentWithUserVO toVO(Comment comment, String docName) { vo.setDocId(comment.getDocId()); vo.setDocName(docName); vo.setCreateDate(comment.getCreateDate()); - vo.setUpdateDate(comment.getUpdateDate()); return vo; } } From a8b490c533c3ba295ba6cec24ab71e46f6a8fe47 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 22:52:15 +0800 Subject: [PATCH 12/37] docs: add document search refactor design spec Co-Authored-By: Claude Opus 4.6 --- ...6-04-27-document-search-refactor-design.md | 234 ++++++++++++++++++ ...45\345\217\243\346\226\207\346\241\243.md" | 201 +++++++++++++++ 2 files changed, 435 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-27-document-search-refactor-design.md create mode 100644 "\346\216\245\345\217\243\346\226\207\346\241\243.md" 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/\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 | 服务器内部错误 | From 73539ee51cc4bb8b876321a2fc3f0557a4d9468c Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 22:56:23 +0800 Subject: [PATCH 13/37] =?UTF-8?q?feat:=20Tag=E6=96=B0=E5=A2=9Ecolor?= =?UTF-8?q?=E5=AD=97=E6=AE=B5,=20SearchDocument=E6=96=B0=E5=A2=9EtagNames?= =?UTF-8?q?=E5=92=8CcategoryName=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tag.java 新增 color (String) 字段 - SearchDocument.java 新增 tagNames (List) 和 categoryName (String) 字段 - 均为 @Field(type = FieldType.Text, analyzer = "ik_smart") Co-Authored-By: Claude Opus 4.6 --- .../com/jiaruiblog/domain/entity/po/SearchDocument.java | 8 ++++++++ .../main/java/com/jiaruiblog/domain/entity/po/Tag.java | 2 ++ 2 files changed, 10 insertions(+) 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; From 32197f3e72c65a5a61437283bd349c0cbfd3b224 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 22:58:38 +0800 Subject: [PATCH 14/37] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9ESearchQuery?= =?UTF-8?q?=E5=92=8CDocSearchVO=E7=94=A8=E4=BA=8E=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增SearchQuery请求参数类(搜索关键词、全文检索、分词、标签筛选等) - 新增DocSearchVO响应VO类(文档ID、名称、大小、描述、标签等) - 新增TagColorVO标签颜色VO类 Co-Authored-By: Claude Opus 4.6 --- .../domain/entity/dto/SearchQuery.java | 48 ++++++++++++++++ .../domain/entity/vo/DocSearchVO.java | 55 +++++++++++++++++++ .../domain/entity/vo/TagColorVO.java | 22 ++++++++ 3 files changed, 125 insertions(+) create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchQuery.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocSearchVO.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/TagColorVO.java 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/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/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; +} From af209dc6693185500401da3d6ad86ae3f7f68261 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:06:21 +0800 Subject: [PATCH 15/37] feat: add searchDocuments method to ElasticService - Add searchDocuments(SearchQuery) method to ElasticService interface - Implement multi-condition search logic in ElasticServiceImpl: - fullText=true: search name + content + tagNames + categoryName - fullText=false: search only name - searchType: all/name/description field selection - Empty keyword returns all documents with pagination - Returns list of matching document IDs Co-Authored-By: Claude Opus 4.6 --- .../application/service/ElasticService.java | 8 ++ .../service/impl/ElasticServiceImpl.java | 91 +++++++++++++++++++ 2 files changed, 99 insertions(+) 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..04fc25f 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,5 +1,6 @@ package com.jiaruiblog.application.service; +import com.jiaruiblog.domain.entity.dto.SearchQuery; import com.jiaruiblog.domain.entity.po.SearchDocument; import com.jiaruiblog.domain.entity.vo.PageVO; @@ -94,4 +95,11 @@ public interface ElasticService { * 获取词云统计数据 */ java.util.List> getWordStat(); + + /** + * 多条件文档检索 + * @param query 搜索参数 + * @return 匹配的文档ID列表 + */ + List searchDocuments(SearchQuery query); } \ 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..097bf76 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,6 +1,7 @@ package com.jiaruiblog.application.service.impl; import com.jiaruiblog.application.service.ElasticService; +import com.jiaruiblog.domain.entity.dto.SearchQuery; import com.jiaruiblog.domain.entity.po.SearchDocument; import com.jiaruiblog.domain.entity.vo.PageVO; import jakarta.annotation.Resource; @@ -216,4 +217,94 @@ 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<>(); + } + } + + /** + * 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; + } } \ No newline at end of file From 1d217bd9250b9f6159b5f701d275128171a9a4be Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:19:21 +0800 Subject: [PATCH 16/37] =?UTF-8?q?feat:=20DocumentService.search=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20SearchQuery=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现完整搜索流程: 1. ES 检索文档ID (using ElasticService.searchDocuments) 2. MySQL 关系表过滤 (tags/category 筛选) 3. 排序: sortField (createTime/size/name) + sortOrder (asc/desc) 4. 分页: page + pageSize 5. 组装结果: liked/collected/tags/sizeDisplay Co-Authored-By: Claude Opus 4.6 --- .../application/service/DocumentService.java | 11 + .../service/impl/DocumentServiceImpl.java | 202 +++++++++++++++++- 2 files changed, 211 insertions(+), 2 deletions(-) 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 f72bee5..b3934a3 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,7 +3,9 @@ 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; @@ -231,4 +233,13 @@ 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); } 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 4eca145..3c42c21 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 @@ -7,8 +7,10 @@ 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.document.UpdateInfoDTO; -import com.jiaruiblog.domain.entity.po.CateDocRelationship; +import com.jiaruiblog.domain.entity.po.Category; +import com.jiaruiblog.domain.entity.po.Tag; import com.jiaruiblog.domain.entity.po.FileDocument; import com.jiaruiblog.domain.entity.po.TagDocRelationship; import com.jiaruiblog.domain.entity.vo.CategoryVO; @@ -16,7 +18,11 @@ import com.jiaruiblog.domain.entity.vo.DocumentVO; import com.jiaruiblog.domain.entity.vo.PageVO; import com.jiaruiblog.domain.entity.vo.TagVO; -import com.jiaruiblog.infrastructure.repository.DocumentRepository; +import com.jiaruiblog.domain.entity.vo.DocSearchVO; +import com.jiaruiblog.domain.entity.vo.TagColorVO; +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.TagDocRelationshipMapper; import com.jiaruiblog.infrastructure.storage.StorageFactory; @@ -68,6 +74,18 @@ public class DocumentServiceImpl implements DocumentService { @Resource private TagDocRelationshipMapper tagDocRelationshipMapper; + @Resource + private TagRepository tagRepository; + + @Resource + private CategoryRepository categoryRepository; + + @Resource + private CollectRepository collectRepository; + + @Resource + private LikeService likeService; + private static final String FILE_NAME = "filename"; @Override @@ -833,4 +851,184 @@ 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 + List esMatchedIds = elasticService.searchDocuments(query); + if (esMatchedIds == null || esMatchedIds.isEmpty()) { + return PageVO.builder() + .pageNum(query.getPage()) + .pageSize(query.getPageSize()) + .total(0) + .list(new ArrayList<>()) + .build(); + } + + Set candidateIds = new HashSet<>(esMatchedIds); + + // 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.findByCategoryIdAndFileIdIn(categoryId, new ArrayList<>(candidateIds)) + .stream() + .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 = documentRepository.findByIdList(new ArrayList<>(candidateIds)); + // Filter by reviewing=false AND docState=SUCCESS + List filteredDocs = documents.stream() + .filter(doc -> !doc.getReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) + .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)) + .toList(); + + return PageVO.builder() + .pageNum(page) + .pageSize(pageSize) + .total(total) + .list(voList) + .build(); + } + + private DocSearchVO convertToDocSearchVO(FileDocument doc, String userId) { + 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())); + vo.setDescription(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)); + } } \ No newline at end of file From 7211254188ee530c72ebda185aa4a322e9c771b2 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:21:18 +0800 Subject: [PATCH 17/37] =?UTF-8?q?feat:=20DocumentController.search=20GET?= =?UTF-8?q?=E8=BD=ACPOST=E5=B9=B6=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 search 接口从 GET 改为 POST - 使用 SearchQuery 作为请求体参数 - 添加 @Permission(PermissionEnum.USER) 注解要求登录 - 从 request 属性获取 userId 并传递给 service Co-Authored-By: Claude Opus 4.6 --- .../api/controller/DocumentController.java | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) 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 700c3c7..2f19375 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 @@ -7,6 +7,7 @@ import com.jiaruiblog.domain.entity.po.User; 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.vo.DocWithCateVO; import com.jiaruiblog.domain.entity.vo.DocumentVO; @@ -149,31 +150,13 @@ public ApiResult hot() { return ApiResult.success(keyList); } - @Operation(summary = "2.4 文档全文搜索", description = "根据关键字搜索已审核且解析成功的文档") - @GetMapping(value = "/search") + @Operation(summary = "文档搜索", description = "根据多条件搜索文档") + @PostMapping(value = "/search") + @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 From 98d747fdc2f6ff2a86c56c5b0e97b1f90b6374cb Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:29:02 +0800 Subject: [PATCH 18/37] =?UTF-8?q?feat:=20ES=E7=B4=A2=E5=BC=95=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20-=20=E6=96=87=E6=A1=A3=E4=B8=8A=E4=BC=A0=E6=97=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0ES=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增indexDocumentToEs方法:文档上传时构建SearchDocument并索引到ES - 新增getTagNamesByDocId方法:获取文档关联的标签名称列表 - 新增getCategoryNameByDocId方法:获取文档关联的分类名称 - 新增updateFileContentToEs方法:文本提取完成后更新ES中的文档内容 - documentUpload方法增加ES索引调用 - uploadByUrl方法增加ES索引调用 - 确认remove方法已有elasticService.deleteById调用 Co-Authored-By: Claude Opus 4.6 --- .../service/impl/DocumentServiceImpl.java | 117 +++++++++++++++++- 1 file changed, 113 insertions(+), 4 deletions(-) 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 3c42c21..b48e472 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 @@ -12,6 +12,7 @@ import com.jiaruiblog.domain.entity.po.Category; import com.jiaruiblog.domain.entity.po.Tag; import com.jiaruiblog.domain.entity.po.FileDocument; +import com.jiaruiblog.domain.entity.po.SearchDocument; import com.jiaruiblog.domain.entity.po.TagDocRelationship; import com.jiaruiblog.domain.entity.vo.CategoryVO; import com.jiaruiblog.domain.entity.vo.DocWithCateVO; @@ -39,6 +40,7 @@ import java.io.InputStream; import java.security.MessageDigest; import java.util.*; +import java.util.stream.Collectors; /** * @author jiarui.luo @@ -333,10 +335,13 @@ public void documentUpload(MultipartFile file, String userId, String username) { document.setCreateDate(new Date()); documentRepository.save(document); - // 6. Create review record + // 6. Index document to ES + indexDocumentToEs(document); + + // 7. Create review record docReviewService.insert(document); - // 7. Submit async task for text extraction and ES indexing + // 8. Submit async task for text extraction and ES indexing taskExecuteService.execute(document); log.info("Document upload success: userId={}, username={}, docId={}, filename={}", @@ -424,10 +429,13 @@ public void uploadByUrl(String category, List tags, String name, String document.setCreateDate(new Date()); documentRepository.save(document); - // 7. Create review record + // 7. Index document to ES + indexDocumentToEs(document); + + // 8. Create review record docReviewService.insert(document); - // 8. Submit async task for text extraction and ES indexing + // 9. Submit async task for text extraction and ES indexing taskExecuteService.execute(document); log.info("Upload by URL success: userId={}, username={}, docId={}, filename={}, url={}", @@ -789,6 +797,9 @@ 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()); @@ -1031,4 +1042,102 @@ private String formatSize(Long size) { 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.getMd5() == null) { + log.warn("Cannot index null or md5-less document to ES"); + return; + } + try { + SearchDocument searchDocument = new SearchDocument(); + searchDocument.setId(document.getMd5()); + 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.getMd5(), document.getName()); + } catch (Exception e) { + log.error("Failed to index document to ES: id={}", document.getMd5(), e); + } + } + + /** + * Get tag names for a document + */ + private 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 + */ + private 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 = documentRepository.findById(docId); + if (document == null || document.getMd5() == null) { + log.warn("Cannot update ES content: document not found for docId={}", docId); + return; + } + try { + SearchDocument searchDocument = new SearchDocument(); + searchDocument.setId(document.getMd5()); + 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={}, md5={}", docId, document.getMd5()); + } catch (Exception e) { + log.error("Failed to update document content in ES: docId={}", docId, e); + } + } } \ No newline at end of file From 59e5d46103e00833cadf2a05620ff64a51dba680 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:32:58 +0800 Subject: [PATCH 19/37] fix: StringToEnumConverter null check for getCode() Co-Authored-By: Claude Opus 4.6 --- .../jiaruiblog/common/converter/StringToEnumConverter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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..69d4bae 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 @@ -19,7 +19,10 @@ public class StringToEnumConverter implements Converter 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); + } } } From 6429cfafae27ccd576846e5f650682ea6ab72096 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:33:38 +0800 Subject: [PATCH 20/37] fix: StringToEnumConverter generic type to Enum & BaseEnum Co-Authored-By: Claude Opus 4.6 --- .../com/jiaruiblog/common/converter/StringToEnumConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 69d4bae..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,7 +13,7 @@ * @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) { From 328ffa791d76e2eb6c1d917e775a62957dfebe25 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:35:33 +0800 Subject: [PATCH 21/37] fix: StringCodeToEnumConverterFactory generic type to Enum for compatibility Co-Authored-By: Claude Opus 4.6 --- .../common/converter/StringCodeToEnumConverterFactory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 2ec50d3..3dc476b 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,7 +14,7 @@ * @Date 2022/6/19 5:08 下午 * @Version 1.0 **/ -public class StringCodeToEnumConverterFactory implements ConverterFactory { +public class StringCodeToEnumConverterFactory implements ConverterFactory> { private static final Map CONVERTERS = new ConcurrentHashMap<>(); @@ -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; From 00fdb2ba371bdce2998ba740febeee436e97a43d Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:38:15 +0800 Subject: [PATCH 22/37] fix: remove diamond operator for StringToEnumConverter type inference Co-Authored-By: Claude Opus 4.6 --- .../common/converter/StringCodeToEnumConverterFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3dc476b..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 @@ -29,7 +29,7 @@ public class StringCodeToEnumConverterFactory implements ConverterFactory> Converter getConverter(Class targetType) { Converter converter = CONVERTERS.get(targetType); if (converter == null) { - converter = (Converter) new StringToEnumConverter<>(targetType); + converter = (Converter) new StringToEnumConverter(targetType); CONVERTERS.put(targetType, converter); } return converter; From 67877e67da8eda5d046722e86ad70f739132166d Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:41:05 +0800 Subject: [PATCH 23/37] fix: use existing findByFileIds method for category filtering Co-Authored-By: Claude Opus 4.6 --- .../service/impl/DocumentServiceImpl.java | 116 ++++++++---------- 1 file changed, 54 insertions(+), 62 deletions(-) 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 b48e472..e5fc963 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 @@ -9,22 +9,13 @@ 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.po.Category; -import com.jiaruiblog.domain.entity.po.Tag; -import com.jiaruiblog.domain.entity.po.FileDocument; -import com.jiaruiblog.domain.entity.po.SearchDocument; -import com.jiaruiblog.domain.entity.po.TagDocRelationship; -import com.jiaruiblog.domain.entity.vo.CategoryVO; -import com.jiaruiblog.domain.entity.vo.DocWithCateVO; -import com.jiaruiblog.domain.entity.vo.DocumentVO; -import com.jiaruiblog.domain.entity.vo.PageVO; -import com.jiaruiblog.domain.entity.vo.TagVO; -import com.jiaruiblog.domain.entity.vo.DocSearchVO; -import com.jiaruiblog.domain.entity.vo.TagColorVO; +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; @@ -50,7 +41,7 @@ public class DocumentServiceImpl implements DocumentService { @Resource - private DocumentRepository documentRepository; + private DocumentMybatisRepository documentMybatisRepository; @Resource private StorageFactory storageFactory; @@ -106,7 +97,7 @@ public String uploadFileToGridFs(String fileName, InputStream inputStream, Strin @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 @@ -114,7 +105,7 @@ public void insert(FileDocument document) { if (document == null) { return; } - documentRepository.save(document); + documentMybatisRepository.save(document); } @Override @@ -137,7 +128,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())); @@ -175,7 +166,7 @@ public FileDocument queryById(String documentId) { if (documentId == null || documentId.isEmpty()) { return null; } - return documentRepository.findById(documentId); + return documentMybatisRepository.findById(documentId); } @Override @@ -183,7 +174,7 @@ public FileDocument queryByMd5(String md5) { if (md5 == null || md5.isEmpty()) { return null; } - return documentRepository.findByMd5(md5); + return documentMybatisRepository.findByMd5(md5); } @Override @@ -199,12 +190,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 @@ -255,14 +246,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) @@ -276,7 +267,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; } @@ -289,7 +280,7 @@ 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; } @@ -308,7 +299,7 @@ public void documentUpload(MultipartFile file, String userId, String username) { String md5 = calculateMd5(fileBytes); // 3. Check for duplicate - FileDocument existing = documentRepository.findByMd5(md5); + FileDocument existing = documentMybatisRepository.findByMd5(md5); if (existing != null) { log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); return; @@ -333,7 +324,7 @@ public void documentUpload(MultipartFile file, String userId, String username) { document.setDocState(DocStateEnum.WAIT); document.setReviewing(true); document.setCreateDate(new Date()); - documentRepository.save(document); + documentMybatisRepository.save(document); // 6. Index document to ES indexDocumentToEs(document); @@ -398,7 +389,7 @@ public void uploadByUrl(String category, List tags, String name, String String md5 = calculateMd5(fileBytes); // 3. Check for duplicate - FileDocument existing = documentRepository.findByMd5(md5); + FileDocument existing = documentMybatisRepository.findByMd5(md5); if (existing != null) { log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); return; @@ -427,7 +418,7 @@ public void uploadByUrl(String category, List tags, String name, String document.setDocState(DocStateEnum.WAIT); document.setReviewing(true); document.setCreateDate(new Date()); - documentRepository.save(document); + documentMybatisRepository.save(document); // 7. Index document to ES indexDocumentToEs(document); @@ -453,7 +444,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; } @@ -462,7 +453,7 @@ public void updateFile(FileDocument fileDocument) { if (fileDocument == null || fileDocument.getId() == null) { return; } - documentRepository.update(fileDocument); + documentMybatisRepository.update(fileDocument); } @Override @@ -474,7 +465,7 @@ public void updateState(FileDocument fileDocument, DocStateEnum state, String er if (errorMsg != null) { fileDocument.setErrorMsg(errorMsg); } - documentRepository.update(fileDocument); + documentMybatisRepository.update(fileDocument); } @Override @@ -482,9 +473,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) { @@ -511,7 +502,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 @@ -521,7 +512,7 @@ public Optional getPreviewById(String id) { @Override public FileDocument getByMd5(String md5) { - return documentRepository.findByMd5(md5); + return documentMybatisRepository.findByMd5(md5); } @Override @@ -531,7 +522,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); } @@ -541,7 +532,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 @@ -549,7 +540,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 @@ -568,16 +559,16 @@ public PageVO list(DocumentDTO documentDTO) { FilterTypeEnum type = documentDTO.getType(); if (type == FilterTypeEnum.TAG && StringUtils.hasText(documentDTO.getTagId())) { - documents = documentRepository.findByPageByTag(documentDTO.getTagId(), + documents = documentMybatisRepository.findByPageByTag(documentDTO.getTagId(), documentDTO.getPage(), documentDTO.getRows()); - total = documentRepository.countByTagId(documentDTO.getTagId()); + total = documentMybatisRepository.countByTagId(documentDTO.getTagId()); } else if (type == FilterTypeEnum.CATEGORY && StringUtils.hasText(documentDTO.getCategoryId())) { - documents = documentRepository.findByPageByCategory(documentDTO.getCategoryId(), + documents = documentMybatisRepository.findByPageByCategory(documentDTO.getCategoryId(), documentDTO.getPage(), documentDTO.getRows()); - total = documentRepository.countByCategoryId(documentDTO.getCategoryId()); + total = documentMybatisRepository.countByCategoryId(documentDTO.getCategoryId()); } else { documents = listFilesByPage(documentDTO.getPage(), documentDTO.getRows()); - total = documentRepository.count(); + total = documentMybatisRepository.count(); } List voList = documents.stream() @@ -610,7 +601,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()); @@ -618,7 +609,7 @@ public void updateInfo(UpdateInfoDTO updateInfoDTO) { if (updateInfoDTO.getDesc() != null) { document.setDescription(updateInfoDTO.getDesc()); } - documentRepository.update(document); + documentMybatisRepository.update(document); } } @@ -633,13 +624,13 @@ public PageVO listWithCategory(DocumentDTO documentDTO) { FilterTypeEnum type = documentDTO.getType(); if (type == FilterTypeEnum.TAG && StringUtils.hasText(documentDTO.getTagId())) { - documents = documentRepository.findByPageByTag(documentDTO.getTagId(), + documents = documentMybatisRepository.findByPageByTag(documentDTO.getTagId(), documentDTO.getPage(), documentDTO.getRows()); - total = documentRepository.countByTagId(documentDTO.getTagId()); + total = documentMybatisRepository.countByTagId(documentDTO.getTagId()); } else if (type == FilterTypeEnum.CATEGORY && StringUtils.hasText(documentDTO.getCategoryId())) { - documents = documentRepository.findByPageByCategory(documentDTO.getCategoryId(), + documents = documentMybatisRepository.findByPageByCategory(documentDTO.getCategoryId(), documentDTO.getPage(), documentDTO.getRows()); - total = documentRepository.countByCategoryId(documentDTO.getCategoryId()); + total = documentMybatisRepository.countByCategoryId(documentDTO.getCategoryId()); } else { return PageVO.builder().build(); } @@ -724,7 +715,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 @@ -732,8 +723,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; } @@ -743,7 +734,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 @@ -753,20 +744,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 @@ -774,7 +765,7 @@ public boolean isExist(String docId) { if (docId == null || docId.isEmpty()) { return false; } - return documentRepository.findById(docId) != null; + return documentMybatisRepository.findById(docId) != null; } @Override @@ -828,7 +819,7 @@ 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.getReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) .toList(); @@ -926,8 +917,9 @@ public PageVO search(SearchQuery query, String userId) { .build(); } String categoryId = categories.get(0).getId(); - List docIdsInCategory = cateDocRelationshipMapper.findByCategoryIdAndFileIdIn(categoryId, new ArrayList<>(candidateIds)) + List docIdsInCategory = cateDocRelationshipMapper.findByFileIds(new ArrayList<>(candidateIds)) .stream() + .filter(rel -> categoryId.equals(rel.getCategoryId())) .map(CateDocRelationship::getFileId) .toList(); candidateIds.retainAll(docIdsInCategory); @@ -942,7 +934,7 @@ public PageVO search(SearchQuery query, String userId) { } // Step 4: Query documents from MySQL - List documents = documentRepository.findByIdList(new ArrayList<>(candidateIds)); + 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) @@ -1121,7 +1113,7 @@ private void updateFileContentToEs(String docId, String content) { if (docId == null || docId.isEmpty()) { return; } - FileDocument document = documentRepository.findById(docId); + FileDocument document = documentMybatisRepository.findById(docId); if (document == null || document.getMd5() == null) { log.warn("Cannot update ES content: document not found for docId={}", docId); return; From b13913e687a0cccb737c9ac0d8a4c64d557a6ca6 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:43:41 +0800 Subject: [PATCH 24/37] fix: use Collectors.toList() instead of toList() for mutable list Co-Authored-By: Claude Opus 4.6 --- .../application/service/impl/DocumentServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e5fc963..27e15e4 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 @@ -938,7 +938,7 @@ public PageVO search(SearchQuery query, String userId) { // Filter by reviewing=false AND docState=SUCCESS List filteredDocs = documents.stream() .filter(doc -> !doc.getReviewing() && doc.getDocState() == DocStateEnum.SUCCESS) - .toList(); + .collect(Collectors.toList()); // Step 5: Sorting String sortField = query.getSortField(); From 5c525ff53c46566168b5ae8629ff835a8e42d44f Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:46:56 +0800 Subject: [PATCH 25/37] fix: use document ID (UUID) as ES document ID instead of MD5 ES documents were indexed with MD5 as ID but MySQL queries used UUID, causing search results to be empty. Now ES and MySQL both use document.getId(). Co-Authored-By: Claude Opus 4.6 --- .../service/impl/DocumentServiceImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 27e15e4..6f5f2fe 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 @@ -146,8 +146,8 @@ public void remove(FileDocument document) { 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()); } @@ -1039,22 +1039,22 @@ private String formatSize(Long size) { * Index document to Elasticsearch */ private void indexDocumentToEs(FileDocument document) { - if (document == null || document.getMd5() == null) { - log.warn("Cannot index null or md5-less document to ES"); + 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.getMd5()); + 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.getMd5(), document.getName()); + 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.getMd5(), e); + log.error("Failed to index document to ES: id={}", document.getId(), e); } } @@ -1114,20 +1114,20 @@ private void updateFileContentToEs(String docId, String content) { return; } FileDocument document = documentMybatisRepository.findById(docId); - if (document == null || document.getMd5() == null) { + 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.getMd5()); + 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={}, md5={}", docId, document.getMd5()); + log.info("Document content updated in ES: docId={}", docId); } catch (Exception e) { log.error("Failed to update document content in ES: docId={}", docId, e); } From a32418bc175d19075dd5fb92db350a501da87b02 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Mon, 27 Apr 2026 23:53:41 +0800 Subject: [PATCH 26/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=815?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 4 +- .../api/controller/CollectController.java | 18 +- .../api/controller/DocumentController.java | 27 ++- .../api/controller/FileController.java | 8 +- .../api/controller/LikeController.java | 13 +- .../application/service/IUserService.java | 1 - .../application/service/ThumbnailService.java | 21 +++ .../service/impl/CollectServiceImpl.java | 5 + .../service/impl/LikeServiceImpl.java | 11 +- .../service/impl/ThumbnailServiceImpl.java | 164 +++++++++++++++++- .../service/impl/UserServiceImpl.java | 4 +- .../application/task/data/TaskData.java | 18 +- .../task/executor/DocxExecutor.java | 43 ++++- .../task/executor/PdfWordTaskExecutor.java | 68 ++++++-- .../task/executor/PicExecutor.java | 20 +-- .../task/executor/TaskExecutor.java | 14 +- .../task/executor/slider/PptExecutor.java | 49 +++++- .../transformer/PO2VOConverter.java | 3 +- .../common/constants/StorageConstants.java | 18 +- .../jiaruiblog/domain/entity/vo/DocLogVO.java | 2 + .../domain/entity/vo/DocumentVO.java | 2 + .../infrastructure/config/I18nConfig.java | 2 +- .../mysql/LikeDocRelationshipMapper.java | 2 + .../mapper/LikeDocRelationshipMapper.xml | 4 + 24 files changed, 424 insertions(+), 97 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8341ecf..f7cdc20 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -49,7 +49,9 @@ "Bash(grep:*)", "Bash(ls -la *.md *.sql)", "Bash(mvn dependency:tree)", - "Bash(git:*)" + "Bash(git:*)", + "Bash(xxd \"C:\\\\Project\\\\java\\\\all-docs\\\\all-docs-bootstrap\\\\src\\\\main\\\\resources\\\\i18n\\\\messages_zh_CN.properties\")", + "Bash(./mvnw compile:*)" ] } } 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/DocumentController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/DocumentController.java index 2f19375..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,26 +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; @@ -151,7 +150,7 @@ public ApiResult hot() { } @Operation(summary = "文档搜索", description = "根据多条件搜索文档") - @PostMapping(value = "/search") + @PostMapping(value = "/searchList") @Permission(value = PermissionEnum.USER) public ApiResult search( @RequestBody @Schema(description = "搜索参数") SearchQuery query, 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 6d16ef2..cc952c2 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 @@ -97,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()) { @@ -105,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下载文件内容 @@ -520,7 +524,7 @@ public byte[] previewThumb2(@PathVariable String thumbId, // } // 设置响应头,缓存 1 小时 response.setHeader("Cache-Control", "max-age=3600, public"); - return fileService.getFileBytes(thumbId, StorageConstants.THUMBS); + return fileService.getFileBytes(thumbId, StorageConstants.THUMBNAILS); } @GetMapping(value = "/text2/{txtId}", produces = MediaType.TEXT_PLAIN_VALUE) 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 96251dc..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 @@ -31,9 +31,16 @@ public class LikeController{ @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-application/src/main/java/com/jiaruiblog/application/service/IUserService.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/IUserService.java index bdee79f..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 @@ -23,7 +23,6 @@ public interface IUserService { Map login(RegistryUserDTO userDTO); - /** * 注册用户 * @param userDTO 用户注册信息(简化版,仅需账号密码) 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 a44349f..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 @@ -56,4 +56,25 @@ public interface ThumbnailService { * @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/impl/CollectServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CollectServiceImpl.java index 79854d7..51a17ce 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; /** * 收藏服务实现类 @@ -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/LikeServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java index c453361..b2ff9cb 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 @@ -6,6 +6,7 @@ import com.jiaruiblog.common.enums.RedisActionEnum; import com.jiaruiblog.domain.entity.po.CollectDocRelationship; import com.jiaruiblog.domain.entity.po.LikeDocRelationship; +import com.jiaruiblog.infrastructure.repository.mysql.LikeDocRelationshipMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -26,6 +27,9 @@ public class LikeServiceImpl implements LikeService { @Resource private RedisService redisService; + @Resource + private LikeDocRelationshipMapper likeDocRelationshipMapper; + public static final String ENTITY_LIKE_KEY_FORMAT = "like:entity:{0}:{1}"; @Override @@ -70,8 +74,8 @@ public Long findEntityLikeCount(Integer entityType, String entityId) { if (redisCount != null && redisCount > 0) { return redisCount; } - // Redis 没有从 DB 获取 - return collectService.collectNum(entityId); + // Redis 没有从 DB 获取,使用正确的 entityType 查询数量 + return likeDocRelationshipMapper.countByEntityIdAndEntityType(entityId, entityType); } catch (Exception e) { log.error("查询点赞数量失败: entityType={}, entityId={}", entityType, entityId, e); return 0L; @@ -153,7 +157,8 @@ public Long likeNum(String docId) { if (redisCount != null && redisCount > 0) { return redisCount; } - return collectService.collectNum(docId); + // Redis 没有从 DB 获取,使用正确的 entityType 查询点赞数量 + return likeDocRelationshipMapper.countByEntityIdAndEntityType(docId, RedisActionEnum.LIKE.getCode()); } catch (Exception e) { log.error("查询点赞数量失败: docId={}", docId, e); return 0L; 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 e8cd264..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 @@ -10,13 +10,19 @@ 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; /** @@ -62,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"); @@ -192,4 +198,160 @@ public String makePreviewForPpt(InputStream inputStream, String fileName, String 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/UserServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/UserServiceImpl.java index ae63486..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 @@ -111,7 +111,7 @@ public Map login(RegistryUserDTO userDTO) { // 更新登录时间 user.setLastLogin(new Date()); - userRepository.update(user); + userRepository.updateLoginTime(user); return result; } @@ -473,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 4040758..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,14 +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 提取文字 @@ -16,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) { @@ -35,27 +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) { - if (is == null) { - log.warn("PDF预览图生成失败:输入流为空"); - return; - } - try { - ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); - FileDocument fileDocument = taskData.getFileDocument(); - // 使用 md5 + filename 作为预览图ID - String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); - String result = thumbnailService.makePreviewForPdf(is, fileDocument.getName(), previewId); - if (result != null && !result.isEmpty()) { - fileDocument.setPreviewFileId(previewId); - log.info("PDF预览图生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); - } - } catch (Exception e) { - log.error("PDF预览图生成异常:docId={}", taskData.getFileDocument().getId(), e); - } + // 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 c9b3611..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 @@ -49,23 +49,9 @@ protected void makeThumb(InputStream is, String picPath) throws IOException { @Override protected void makePreviewFile(InputStream is, TaskData taskData) { - if (is == null) { - log.warn("图片预览图生成失败:输入流为空"); - return; - } - try { - ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); - FileDocument fileDocument = taskData.getFileDocument(); - // 使用 md5 + filename 作为预览图ID,保持与文档路径一致 - String previewId = fileDocument.getMd5() + "_" + fileDocument.getName(); - String result = thumbnailService.makePreview(is, fileDocument.getName(), previewId); - if (result != null && !result.isEmpty()) { - fileDocument.setPreviewFileId(previewId); - log.info("图片预览图生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); - } - } catch (Exception e) { - log.error("图片预览图生成异常:docId={}", taskData.getFileDocument().getId(), e); - } + // 图片文件不需要格式转换,预览图直接使用缩略图即可 + // 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 9ec8838..2f6135c 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 @@ -43,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[]{}; } /** @@ -188,11 +185,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; } @@ -212,7 +210,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 0721a91..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 @@ -4,6 +4,7 @@ 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; @@ -17,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) { @@ -36,27 +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预览图生成失败:输入流为空"); + log.warn("PPT预览文件生成失败:输入流为空"); return; } try { ThumbnailService thumbnailService = SpringApplicationContext.getBean(ThumbnailService.class); FileDocument fileDocument = taskData.getFileDocument(); - // 使用 md5 + filename 作为预览图ID + // 使用 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); + log.info("PPT预览文件生成成功:docId={}, previewId={}", fileDocument.getId(), previewId); } } catch (Exception e) { - log.error("PPT预览图生成异常:docId={}", taskData.getFileDocument().getId(), 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/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-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java b/all-docs-common/src/main/java/com/jiaruiblog/common/constants/StorageConstants.java index ff57c1e..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 @@ -29,9 +29,9 @@ private StorageConstants() {} /** * 缩略图存储路径前缀 - * 完整路径格式: thumbs/{uniqueKey} + * 完整路径格式: thumbnails/{uniqueKey} */ - public static final String THUMBS = "thumbs/"; + public static final String THUMBNAILS = "thumbnails/"; /** * 头像存储路径前缀 @@ -72,10 +72,20 @@ public static String documentTextPath(String objectKey) { /** * 生成缩略图存储路径 * @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; } /** 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/DocumentVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java index d9ffdd7..238aebc 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; 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/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/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} From 18d84ebfcceed4a5e63a55eb29b65014231d1f1f Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 17:45:22 +0800 Subject: [PATCH 27/37] docs: add spec for document list fulltext search Co-Authored-By: Claude Opus 4.6 --- ...28-document-list-fulltext-search-design.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-28-document-list-fulltext-search-design.md 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 From 3710ebb57cf0c99d17cbe1f42800332b94865510 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 17:47:38 +0800 Subject: [PATCH 28/37] docs: add implementation plan for document list fulltext search Co-Authored-By: Claude Opus 4.6 --- ...4-28-document-list-fulltext-search-plan.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-28-document-list-fulltext-search-plan.md 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 From 7659cf636159ea1e2d7755be7c516a4b80be1e1f Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 18:01:00 +0800 Subject: [PATCH 29/37] feat: add paginated full-text search with multi-highlight support - Add SearchResultVO containing total and items list - Change SearchResultItem.highlightFragment to highlightFragments (List) - Add searchDocumentsFullText method supporting tagId/categoryId filtering - Extract up to 3 highlight fragments from content - Add overloaded extractHighlightFragment with position parameter - Update DocumentServiceImpl to use new list-based highlightFragments Co-Authored-By: Claude Opus 4.6 --- .../service/impl/DocumentServiceImpl.java | 56 ++++- .../service/impl/ElasticServiceImpl.java | 222 ++++++++++++++++++ .../domain/entity/dto/SearchResultItem.java | 37 +++ .../domain/entity/vo/SearchResultVO.java | 33 +++ 4 files changed, 335 insertions(+), 13 deletions(-) create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/dto/SearchResultItem.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchResultVO.java 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 6f5f2fe..e49c9a6 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 @@ -2,12 +2,14 @@ import cn.hutool.core.util.IdUtil; 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.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.po.*; import com.jiaruiblog.domain.entity.vo.*; @@ -79,7 +81,8 @@ public class DocumentServiceImpl implements DocumentService { @Resource private LikeService likeService; - private static final String FILE_NAME = "filename"; + @Resource + private IDocLogService docLogService; @Override public String uploadFileToGridFs(String fileName, InputStream inputStream, String contentType, String md5) { @@ -285,10 +288,10 @@ public FileDocument saveFile(String md5, MultipartFile file) { } @Override - public void documentUpload(MultipartFile file, String userId, String username) { + public FileDocument documentUpload(MultipartFile file, String userId, String username) { if (file == null || file.isEmpty()) { log.warn("Document upload failed: file is empty"); - return; + return null; } try { @@ -302,7 +305,7 @@ public void documentUpload(MultipartFile file, String userId, String username) { FileDocument existing = documentMybatisRepository.findByMd5(md5); if (existing != null) { log.info("Document already exists: md5={}, docId={}", md5, existing.getId()); - return; + return existing; } // 4. Upload to MinIO @@ -337,9 +340,18 @@ public void documentUpload(MultipartFile file, String userId, String username) { 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; } } @@ -431,6 +443,12 @@ public void uploadByUrl(String category, List tags, String name, String 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()); @@ -856,9 +874,9 @@ private DocumentVO convertToVO(FileDocument doc) { @Override public PageVO search(SearchQuery query, String userId) { - // Step 1: ES retrieval - get candidate doc IDs - List esMatchedIds = elasticService.searchDocuments(query); - if (esMatchedIds == null || esMatchedIds.isEmpty()) { + // 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()) @@ -867,7 +885,15 @@ public PageVO search(SearchQuery query, String userId) { .build(); } - Set candidateIds = new HashSet<>(esMatchedIds); + // 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()) { @@ -966,7 +992,7 @@ public PageVO search(SearchQuery query, String userId) { // Step 7: Assemble results - get liked/collected status and tags List voList = pagedDocs.stream() - .map(doc -> convertToDocSearchVO(doc, userId)) + .map(doc -> convertToDocSearchVO(doc, userId, highlightMap)) .toList(); return PageVO.builder() @@ -977,14 +1003,16 @@ public PageVO search(SearchQuery query, String userId) { .build(); } - private DocSearchVO convertToDocSearchVO(FileDocument doc, String userId) { + 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())); - vo.setDescription(doc.getDescription()); + // 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()); @@ -1061,7 +1089,8 @@ private void indexDocumentToEs(FileDocument document) { /** * Get tag names for a document */ - private List getTagNamesByDocId(String docId) { + @Override + public List getTagNamesByDocId(String docId) { if (docId == null || docId.isEmpty()) { return Collections.emptyList(); } @@ -1087,7 +1116,8 @@ private List getTagNamesByDocId(String docId) { /** * Get category name for a document */ - private String getCategoryNameByDocId(String docId) { + @Override + public String getCategoryNameByDocId(String docId) { if (docId == null || docId.isEmpty()) { return ""; } 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 097bf76..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 @@ -2,10 +2,17 @@ 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; @@ -13,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; /** @@ -33,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) { @@ -268,6 +283,123 @@ public List searchDocuments(SearchQuery query) { } } + @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 */ @@ -307,4 +439,94 @@ private List buildFieldCriteria(String keyword, Boolean fullText, Stri 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-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/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 From 68e7c91cac02fa1f78617262fd8229af8694087a Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 18:43:45 +0800 Subject: [PATCH 30/37] feat: add fulltext search routing in DocumentServiceImpl.list() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added filterWord check at start of list() method to route to ES - Added searchFullText() private method that: 1. Calls elasticService.searchDocumentsFullText() 2. Fetches document details from MySQL via findByIdList() 3. Builds highlight map and sets description with "\n---\n" delimiter - Added searchDocumentsFullText() to ElasticService interface - Matches plan: ES检索时高亮片段分段存入description字段 Co-Authored-By: Claude Opus 4.6 --- .../application/service/ElasticService.java | 22 +++++- .../service/impl/DocumentServiceImpl.java | 67 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) 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 04fc25f..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,8 +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; @@ -96,10 +98,28 @@ 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/impl/DocumentServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/DocumentServiceImpl.java index e49c9a6..5673bbf 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 @@ -572,6 +572,14 @@ public PageVO list(DocumentDTO documentDTO) { return PageVO.builder().build(); } + String filterWord = documentDTO.getFilterWord(); + + // filterWord 不为空时走 ES 全文检索 + if (StringUtils.hasText(filterWord)) { + return searchFullText(documentDTO); + } + + // 否则走现有 MySQL 逻辑(保留现有代码) List documents; long total; @@ -600,6 +608,65 @@ public PageVO list(DocumentDTO documentDTO) { .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() + .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(); + } + @Override public PageVO listNew(DocumentDTO documentDTO) { return list(documentDTO); From ffff0f009ab755d7f9a9ba036acdd9c33ab4d3da Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 18:57:48 +0800 Subject: [PATCH 31/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=816?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- .../api/controller/CommentController.java | 10 +++- .../api/controller/FileController.java | 15 ++++-- .../com/jiaruiblog/api/filter/JwtFilter.java | 2 + .../application/service/DocumentService.java | 15 +++++- .../application/service/RedisService.java | 7 +++ .../service/impl/CategoryServiceImpl.java | 48 +++++++++++++++++++ .../service/impl/CollectServiceImpl.java | 2 +- .../service/impl/CommentServiceImpl.java | 12 +++++ .../service/impl/DocReviewServiceImpl.java | 22 ++++++--- .../service/impl/LikeServiceImpl.java | 2 + .../service/impl/RedisServiceImpl.java | 9 ++++ .../task/executor/TaskExecutor.java | 6 ++- .../main/resources/i18n/messages.properties | 3 ++ .../resources/i18n/messages_en_US.properties | 3 ++ .../resources/i18n/messages_ja.properties | 3 ++ .../resources/i18n/messages_ko.properties | 3 ++ .../resources/i18n/messages_zh_CN.properties | 3 ++ .../src/main/resources/sql/init.sql | 1 + .../common/exception/ErrorCode.java | 5 +- .../jiaruiblog/domain/entity/po/Comment.java | 2 + .../domain/entity/vo/DocumentVO.java | 2 +- .../config/datasource/MyBatisConfig.java | 4 +- .../mysql/CateDocRelationshipMapper.java | 2 + .../mysql/CategoryMybatisRepository.java | 2 +- .../mysql/TagDocRelationshipMapper.java | 2 + .../mysql/TagMybatisRepository.java | 5 +- .../mapper/CateDocRelationshipMapper.xml | 8 ++++ .../main/resources/mapper/CollectMapper.xml | 2 +- .../main/resources/mapper/CommentMapper.xml | 7 +-- .../mapper/TagDocRelationshipMapper.xml | 8 ++++ .../src/main/resources/mapper/UserMapper.xml | 6 +-- 32 files changed, 192 insertions(+), 32 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f7cdc20..3a11f8b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -51,7 +51,8 @@ "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(./mvnw compile:*)", + "Bash(xxd \"C:\\\\Project\\\\java\\\\all-docs\\\\all-docs-infrastructure\\\\src\\\\main\\\\resources\\\\mapper\\\\CommentMapper.xml\")" ] } } 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 ff5ff14..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 @@ -145,8 +145,14 @@ private PageVO buildCommentWithUserVO(PageVO comment Map docNameMap = documents.stream() .collect(Collectors.toMap(FileDocument::getId, FileDocument::getName)); - List voList = commentPage.getList().stream() - .map(comment -> commentConverter.toVO(comment, docNameMap.get(comment.getDocId()))) +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() 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 cc952c2..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 @@ -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.THUMBNAILS); + // 缩略图存储时带了 .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) 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 666b182..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 @@ -36,7 +36,9 @@ 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" ); 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 b3934a3..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 @@ -30,10 +30,11 @@ public interface DocumentService { */ FileDocument saveFile(String md5, MultipartFile file); - /** +/** * 用户上传文档 + * @return FileDocument 上传后的文档对象 */ - void documentUpload(MultipartFile file, String userId, String username); + FileDocument documentUpload(MultipartFile file, String userId, String username); /** * 批量上传 @@ -242,4 +243,14 @@ void uploadByUrl(String category, List tags, String name, * @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/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/impl/CategoryServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/CategoryServiceImpl.java index c21c2ad..4f3b4e9 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; @@ -59,6 +60,9 @@ public class CategoryServiceImpl implements CategoryService { @Resource CollectRepository collectRepository; + @Resource + TagDocRelationshipMapper tagDocRelationshipMapper; + /** * 新增分类记录 * 注意:需要处理并发插入的事务问题 @@ -482,6 +486,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(); @@ -520,6 +546,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 51a17ce..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 @@ -32,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(); } } 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 e80a78b..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 @@ -3,11 +3,13 @@ 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; @@ -32,11 +34,21 @@ 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()); 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 c6bce1c..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,10 +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.UUID; +import java.util.*; /** * @author luojiarui @@ -170,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<>(); @@ -187,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/LikeServiceImpl.java b/all-docs-application/src/main/java/com/jiaruiblog/application/service/impl/LikeServiceImpl.java index b2ff9cb..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 @@ -51,12 +51,14 @@ public boolean like(String userId, Integer entityType, String entityId) { if (isLiked) { // 已点赞 → 取消点赞 redisService.deleteSetMember(entityLikeKey, userId); + redisService.incrementDocScore(entityId, -1); remove(like); log.debug("用户 {} 取消点赞实体 {}:{}", userId, entityType, entityId); return false; } else { // 未点赞 → 执行点赞 redisService.setSet(entityLikeKey, userId); + redisService.incrementDocScore(entityId, 1); insertRelationShip(like); log.debug("用户 {} 点赞实体 {}:{}", userId, entityType, entityId); return true; 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 d6901bb..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 @@ -250,4 +250,13 @@ public void addSearchHistoryByUserId(String userId, String searchWord) { redisSearchTemplate.expire(key, java.time.Duration.ofDays(30)); 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/task/executor/TaskExecutor.java b/all-docs-application/src/main/java/com/jiaruiblog/application/task/executor/TaskExecutor.java index 2f6135c..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 @@ -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) { 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/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/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 09f8a5f..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 @@ -22,6 +22,8 @@ public class Comment { 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/vo/DocumentVO.java b/all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocumentVO.java index 238aebc..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 @@ -47,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-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 f5b306d..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 @@ -2,12 +2,12 @@ import com.jiaruiblog.common.enums.DocStateEnum; import com.jiaruiblog.common.enums.PermissionEnum; -import com.jiaruiblog.common.enums.RedisActionEnum; 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; @@ -32,7 +32,7 @@ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Excepti new BooleanTypeHandler(), new EnumTypeHandler<>(PermissionEnum.class), new DocStateEnumTypeHandler(), - new EnumTypeHandler<>(RedisActionEnum.class), + new RedisActionEnumTypeHandler(), new EnumTypeHandler<>(ThumbnailEnum.class), new EnumTypeHandler<>(ThumbSizeEnum.class) ); 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 e1771f3..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 @@ -89,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/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/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/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 c94f780..42ac4b8 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/CollectMapper.xml @@ -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 eecfe28..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}) - \ No newline at end of file + 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/UserMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml index 9a6aa24..dae53f4 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/UserMapper.xml @@ -32,7 +32,7 @@ INSERT INTO user (id, username, password, phone, mail, male, description, avatar, birthtime, banning, permission_enum, nickname, last_login, create_date, update_date) VALUES (#{id}, #{username}, #{password}, #{phone}, #{mail}, #{male}, #{description}, - #{avatar}, #{birthtime}, #{banning}, #{permissionEnum, jdbcType=INTEGER}, #{nickname}, + #{avatar}, #{birthtime}, #{banning}, #{permissionEnum}, #{nickname}, #{lastLogin}, #{createDate}, #{updateDate}) @@ -47,7 +47,7 @@ avatar = #{avatar}, birthtime = #{birthtime}, banning = #{banning}, - permission_enum = #{permissionEnum, jdbcType=INTEGER}, + permission_enum = #{permissionEnum, typeHandler=com.jiaruiblog.infrastructure.config.mybatis.PermissionEnumTypeHandler}, nickname = #{nickname}, update_date = NOW() WHERE id = #{id} @@ -69,7 +69,7 @@ INSERT INTO user (id, username, password, phone, mail, male, description, avatar, birthtime, banning, permission_enum, nickname, last_login, create_date, update_date) VALUES (#{id}, #{username}, #{password}, #{phone}, #{mail}, #{male}, #{description}, - #{avatar}, #{birthtime}, #{banning}, #{permissionEnum, jdbcType=INTEGER}, #{nickname}, + #{avatar}, #{birthtime}, #{banning}, #{permissionEnum}, #{nickname}, #{lastLogin}, #{createDate}, #{updateDate}) ON DUPLICATE KEY UPDATE username = VALUES(username), From c7d1fa3b2b4feae064682855e6ad6d81dcfef355 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Tue, 28 Apr 2026 18:57:58 +0800 Subject: [PATCH 32/37] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=817?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mybatis/RedisActionEnumTypeHandler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/config/mybatis/RedisActionEnumTypeHandler.java 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 From 76f9158a6ce0f6b802d324a64b43dc016e1b0e74 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Wed, 29 Apr 2026 17:54:42 +0800 Subject: [PATCH 33/37] docs: add statistics API design spec --- .../specs/2026-04-29-stats-api-design.md | 180 ++++++++++++++ stats_API.md | 224 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-29-stats-api-design.md create mode 100644 stats_API.md 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/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 | 用户活跃度趋势 | From 4d145e8dd3ac9f3c4560525625f49d32e321acc9 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Wed, 29 Apr 2026 17:56:56 +0800 Subject: [PATCH 34/37] docs: add statistics API implementation plan --- .../plans/2026-04-29-stats-api-plan.md | 408 ++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-29-stats-api-plan.md 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()` 方法 +``` From ec170dfd491231122c4d3feed732f0925aba9799 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Wed, 29 Apr 2026 19:47:18 +0800 Subject: [PATCH 35/37] feat: extend statistics API with new endpoints - Add 5 new VO classes for statistics responses - Extend StatsVO with userNum, downloadNum, searchNum, viewNum - Add MyBatis queries for docTypeDist and categoryDist - Add 5 new endpoints: docTypeDist, categoryDist, hotDocs, searchHotWords, userActivity - Fix compilation: change queryById to findById Co-Authored-By: Claude Opus 4.6 --- TODO.md | 44 ++++++++++ .../api/controller/StatisticsController.java | 35 ++++++++ .../service/StatisticsService.java | 30 +++++++ .../service/impl/StatisticsServiceImpl.java | 87 +++++++++++++++++++ .../domain/entity/vo/CategoryDistVO.java | 9 ++ .../domain/entity/vo/DocTypeDistVO.java | 9 ++ .../jiaruiblog/domain/entity/vo/HotDocVO.java | 10 +++ .../domain/entity/vo/SearchHotWordVO.java | 9 ++ .../jiaruiblog/domain/entity/vo/StatsVO.java | 7 ++ .../domain/entity/vo/UserActivityVO.java | 10 +++ .../repository/DocumentRepository.java | 9 ++ .../repository/mysql/DocumentMapper.java | 9 ++ .../mysql/DocumentMybatisRepository.java | 22 +++++ .../main/resources/mapper/DocumentMapper.xml | 30 ++++++- 14 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 TODO.md create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/CategoryDistVO.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/DocTypeDistVO.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/HotDocVO.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/SearchHotWordVO.java create mode 100644 all-docs-domain/src/main/java/com/jiaruiblog/domain/entity/vo/UserActivityVO.java 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/controller/StatisticsController.java b/all-docs-api/src/main/java/com/jiaruiblog/api/controller/StatisticsController.java index 459eb1a..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 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/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-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/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/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/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/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/repository/DocumentRepository.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/DocumentRepository.java index d377568..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); @@ -51,4 +56,8 @@ public interface DocumentRepository { 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/DocumentMapper.java b/all-docs-infrastructure/src/main/java/com/jiaruiblog/infrastructure/repository/mysql/DocumentMapper.java index da3422f..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); @@ -46,4 +51,8 @@ public interface DocumentMapper { 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 7990a82..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; @@ -123,4 +135,14 @@ public long countByTagId(String tagId) { 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/resources/mapper/DocumentMapper.xml b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml index 70c27c9..a2f5ff1 100644 --- a/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml +++ b/all-docs-infrastructure/src/main/resources/mapper/DocumentMapper.xml @@ -48,10 +48,15 @@ WHERE id = #{id} - SELECT COUNT(*) FROM file_document WHERE reviewing = false + + @@ -75,7 +80,14 @@ + + @@ -149,4 +161,18 @@ WHERE fd.reviewing = false AND cdr.category_id = #{categoryId} + + + +
\ No newline at end of file From 4ed67e25f98cc326481d4e5538a6f48648b8a388 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Wed, 29 Apr 2026 21:24:28 +0800 Subject: [PATCH 36/37] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CategoryServiceImpl.java | 2 + .../service/impl/DocumentServiceImpl.java | 93 +++++++++++++------ .../service/impl/TagServiceImpl.java | 1 + 3 files changed, 68 insertions(+), 28 deletions(-) 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 4f3b4e9..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 @@ -204,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()); } @@ -324,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()); 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 5673bbf..f403dcf 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 @@ -704,20 +704,21 @@ public PageVO listWithCategory(DocumentDTO documentDTO) { return PageVO.builder().build(); } + String filterWord = documentDTO.getFilterWord(); 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()); + // 根据是否有过滤词选择不同的查询方法 + if (StringUtils.hasText(filterWord)) { + documents = documentMybatisRepository.findByPageWithFilter( + documentDTO.getPage(), documentDTO.getRows(), + Sort.by(Sort.Direction.DESC, "uploadDate"), filterWord); + total = documentMybatisRepository.countWithFilter(filterWord); } else { - return PageVO.builder().build(); + documents = documentMybatisRepository.findByPage( + documentDTO.getPage(), documentDTO.getRows(), + Sort.by(Sort.Direction.DESC, "uploadDate")); + total = documentMybatisRepository.count(); } List voList = documents.stream() @@ -740,31 +741,41 @@ private DocWithCateVO convertToDocWithCateVO(FileDocument doc, String tagId, Str vo.setCreateTime(doc.getUploadDate()); vo.setChecked(false); - // Build category info + // Build category info and check if already belongs to this category if (StringUtils.hasText(categoryId)) { CategoryVO categoryVO = new CategoryVO(); categoryVO.setId(categoryId); - // Get category name from relationship if needed List cateRels = cateDocRelationshipMapper.findByFileId(doc.getId()); - cateRels.stream() - .filter(rel -> rel.getCategoryId().equals(categoryId)) - .findFirst() - .ifPresent(rel -> { - categoryVO.setRelationShipId(rel.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 - List tagRels = tagDocRelationshipMapper.findByFileId(doc.getId()); - List tagVOList = tagRels.stream() - .map(rel -> { - TagVO tagVO = new TagVO(); - tagVO.setId(rel.getTagId()); - return tagVO; - }) - .toList(); - vo.setTagVOList(tagVOList); + // 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; } @@ -880,6 +891,32 @@ public DocumentVO convertDocument(DocumentVO documentVO, FileDocument fileDocume 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; } 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 c76fdae..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 @@ -161,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()); } From 77875a8efcc72d9fc33965091e3820f1e7e23c32 Mon Sep 17 00:00:00 2001 From: Jarrett Date: Sat, 9 May 2026 15:40:39 +0800 Subject: [PATCH 37/37] =?UTF-8?q?fix(*):=20=E4=BF=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=A3=80=E7=B4=A2=E7=9A=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/impl/DocumentServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f403dcf..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 @@ -641,9 +641,10 @@ private PageVO searchFullText(DocumentDTO documentDTO) { // 4. 构建 ID -> 高亮片段的映射 Map> highlightMap = searchResult.getItems().stream() + .filter(item -> item.getId() != null) .collect(Collectors.toMap( SearchResultItem::getId, - SearchResultItem::getHighlightFragments, + item -> item.getHighlightFragments() != null ? item.getHighlightFragments() : Collections.emptyList(), (existing, replacement) -> replacement ));