From 618381aa0f35e13c3f17c2c6b85e018840d0ad60 Mon Sep 17 00:00:00 2001 From: cuijiawang Date: Tue, 4 Nov 2025 17:59:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(reader):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E9=98=85=E8=AF=BB=E5=99=A8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增文本文件上传、查询、删除、状态更新等功能 - 实现文件内容读取与编码自动识别 - 添加支持公开接口配置,无需登录访问- 配置数据库表结构与初始菜单权限- 完成前后端接口对接,支持Swagger文档 - 集成Sa-Token权限控制,区分公开与需登录接口- 添加文件存储路径配置与上传大小限制 - 实现批量删除与分页查询功能- 提供用户端公开接口用于文件浏览与阅读 --- .../src/main/resources/common-mybatis.yml | 2 +- .../satoken/config/PublicUrlsConfig.java | 37 +++ .../satoken/config/PublicUrlsProperties.java | 33 +++ .../config/SaTokenMvcConfiguration.java | 22 +- .../src/main/resources/common-satoken.yml | 11 + sql/text_file.sql | 33 +++ wol-gateway/pom.xml | 1 + .../gateway/config/SaTokenConfig.java | 49 ++-- .../src/main/resources/application-dev.yml | 6 +- .../src/main/resources/application.yml | 15 ++ wol-modules/pom.xml | 12 + wol-modules/wol-module-reader/pom.xml | 47 ++++ .../agileboot/reader/ReaderApplication.java | 25 ++ .../reader/controller/TextFileController.java | 75 ++++++ .../controller/TextReaderController.java | 49 ++++ .../reader/dto/TextFileQueryDTO.java | 38 +++ .../reader/dto/TextFileUploadDTO.java | 17 ++ .../com/agileboot/reader/entity/TextFile.java | 62 +++++ .../reader/mapper/TextFileMapper.java | 18 ++ .../reader/mapper/xml/TextFileMapper.xml | 7 + .../reader/service/ITextFileService.java | 57 +++++ .../service/impl/TextFileServiceImpl.java | 242 ++++++++++++++++++ .../reader/vo/TextFileContentVO.java | 20 ++ .../com/agileboot/reader/vo/TextFileVO.java | 35 +++ .../src/main/resources/application.yml | 28 ++ .../src/main/resources/bootstrap.yml | 6 + 26 files changed, 912 insertions(+), 35 deletions(-) create mode 100644 agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsConfig.java create mode 100644 agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsProperties.java create mode 100644 sql/text_file.sql create mode 100644 wol-modules/wol-module-reader/pom.xml create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/ReaderApplication.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextFileController.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextReaderController.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileQueryDTO.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileUploadDTO.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/entity/TextFile.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/TextFileMapper.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/xml/TextFileMapper.xml create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/ITextFileService.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/impl/TextFileServiceImpl.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileContentVO.java create mode 100644 wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileVO.java create mode 100644 wol-modules/wol-module-reader/src/main/resources/application.yml create mode 100644 wol-modules/wol-module-reader/src/main/resources/bootstrap.yml diff --git a/agileboot-common/wol-common-mybatis/src/main/resources/common-mybatis.yml b/agileboot-common/wol-common-mybatis/src/main/resources/common-mybatis.yml index 9e078ec..0ed395b 100644 --- a/agileboot-common/wol-common-mybatis/src/main/resources/common-mybatis.yml +++ b/agileboot-common/wol-common-mybatis/src/main/resources/common-mybatis.yml @@ -82,6 +82,6 @@ spring: testOnReturn: false datasource: master: - url: jdbc:mysql://${wol.mysql.maser.url}/agileboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true + url: jdbc:mysql://${wol.mysql.maser.url}/${wol.mysql.maser.database}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true username: ${wol.mysql.maser.username} password: ${wol.mysql.maser.password} diff --git a/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsConfig.java b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsConfig.java new file mode 100644 index 0000000..9a55f99 --- /dev/null +++ b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsConfig.java @@ -0,0 +1,37 @@ +package com.agileboot.common.satoken.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +/** + * 公开接口配置 - 统一管理无需登录的接口 + * 支持从yml配置文件读取,更加灵活 + * + * @author agileboot + */ +@Component +@RequiredArgsConstructor +public class PublicUrlsConfig { + + private final PublicUrlsProperties properties; + + /** + * 公开接口列表 - 从yml配置读取 + */ + private static String[] publicUrls; + + @PostConstruct + public void init() { + publicUrls = properties.getUrlsArray(); + } + + /** + * 获取公开URL列表 + */ + public static String[] getPublicUrls() { + return publicUrls; + } +} + diff --git a/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsProperties.java b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsProperties.java new file mode 100644 index 0000000..b677c0f --- /dev/null +++ b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/PublicUrlsProperties.java @@ -0,0 +1,33 @@ +package com.agileboot.common.satoken.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 公开接口配置属性类 - 从yml读取 + * + * @author agileboot + */ +@Data +@Component +@ConfigurationProperties(prefix = "satoken.public") +public class PublicUrlsProperties { + + /** + * 公开接口列表 - 从yml配置文件读取 + */ + private List urls = new ArrayList<>(); + + /** + * 获取公开URL数组 + */ + public String[] getUrlsArray() { + return urls.toArray(new String[0]); + } +} + + diff --git a/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/SaTokenMvcConfiguration.java b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/SaTokenMvcConfiguration.java index ab14f27..86032e5 100644 --- a/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/SaTokenMvcConfiguration.java +++ b/agileboot-common/wol-common-satoken/src/main/java/com/agileboot/common/satoken/config/SaTokenMvcConfiguration.java @@ -7,6 +7,7 @@ import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.agileboot.common.core.constant.HttpStatus; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -15,15 +16,18 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** - * 权限安全配置 + * 权限安全配置 - Servlet环境(适用于普通微服务) * * @author Lion Li */ @Slf4j @AutoConfiguration +@RequiredArgsConstructor @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class SaTokenMvcConfiguration implements WebMvcConfigurer { + private final PublicUrlsProperties publicUrlsProperties; + /** * 注册sa-token的拦截器 */ @@ -35,15 +39,25 @@ public class SaTokenMvcConfiguration implements WebMvcConfigurer { /** * 注册 [Sa-Token全局过滤器] + * 从yml配置文件读取公开接口列表 */ @Bean public SaServletFilter getGlobleSaServletFilter() { + String[] publicUrls = publicUrlsProperties.getUrlsArray(); + return new SaServletFilter() - .addInclude("/**").addExclude("/favicon.ico") - .addExclude("/auth/getConfig", "/captcha/code", "/auth/register") + // 拦截所有路径 + .addInclude("/**") + // 排除公开接口(从yml配置读取) + .addExclude(publicUrls) + // 登录校验 .setAuth(obj -> { - SaRouter.match("/**", "/auth/login", StpUtil::checkLogin); + // 匹配所有路径,排除公开接口,其他需要登录 + SaRouter.match("/**") + .notMatch(publicUrls) + .check(r -> StpUtil.checkLogin()); }) + // 异常处理 .setError(e -> { if (e instanceof NotLoginException) { return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED); diff --git a/agileboot-common/wol-common-satoken/src/main/resources/common-satoken.yml b/agileboot-common/wol-common-satoken/src/main/resources/common-satoken.yml index bc3094c..98f0a70 100644 --- a/agileboot-common/wol-common-satoken/src/main/resources/common-satoken.yml +++ b/agileboot-common/wol-common-satoken/src/main/resources/common-satoken.yml @@ -24,3 +24,14 @@ sa-token: is-log: ${wol.satoken.isLog} # jwt秘钥 jwt-secret-key: ${wol.satoken.jwtSecretKey} + + # 公开接口配置(无需登录即可访问) + # 注意:每个服务可以在自己的application.yml中配置 + public: + urls: + - /favicon.ico + # 认证相关接口(通用) + - /auth/login + - /auth/getConfig + - /auth/register + - /captcha/code \ No newline at end of file diff --git a/sql/text_file.sql b/sql/text_file.sql new file mode 100644 index 0000000..be828bc --- /dev/null +++ b/sql/text_file.sql @@ -0,0 +1,33 @@ +-- 文本文件表 +CREATE TABLE IF NOT EXISTS `text_file` ( + `file_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文件ID', + `file_name` varchar(255) NOT NULL COMMENT '文件名', + `original_file_name` varchar(255) NOT NULL COMMENT '原始文件名', + `file_path` varchar(500) NOT NULL COMMENT '文件路径', + `file_size` bigint(20) DEFAULT NULL COMMENT '文件大小(字节)', + `description` varchar(500) DEFAULT NULL COMMENT '文件描述', + `status` tinyint(1) DEFAULT '0' COMMENT '状态 0=正常 1=禁用', + `upload_user_id` bigint(20) DEFAULT NULL COMMENT '上传用户ID', + `upload_user_name` varchar(50) DEFAULT NULL COMMENT '上传用户名', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志 0=未删除 1=已删除', + PRIMARY KEY (`file_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文本文件表'; + +-- 初始化一些示例菜单权限(可选) +-- 注意:这里的parent_id需要根据实际情况调整 +INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +VALUES +('文本阅读器', 0, 5, 'reader', NULL, 1, 0, 'M', '0', '0', NULL, 'documentation', 'admin', NOW(), NULL, NULL, '文本阅读器菜单'), +('文件管理', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '文本阅读器' AND parent_id = 0) AS tmp), 1, 'file', 'reader/file/index', 1, 0, 'C', '0', '0', 'reader:file:list', 'list', 'admin', NOW(), NULL, NULL, '文本文件管理菜单'), +('文件上传', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '文件管理' AND perms = 'reader:file:list') AS tmp), 1, '', NULL, 1, 0, 'F', '0', '0', 'reader:file:upload', '#', 'admin', NOW(), NULL, NULL, ''), +('文件查询', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '文件管理' AND perms = 'reader:file:list') AS tmp), 2, '', NULL, 1, 0, 'F', '0', '0', 'reader:file:query', '#', 'admin', NOW(), NULL, NULL, ''), +('文件删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '文件管理' AND perms = 'reader:file:list') AS tmp), 3, '', NULL, 1, 0, 'F', '0', '0', 'reader:file:remove', '#', 'admin', NOW(), NULL, NULL, ''), +('文件编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '文件管理' AND perms = 'reader:file:list') AS tmp), 4, '', NULL, 1, 0, 'F', '0', '0', 'reader:file:edit', '#', 'admin', NOW(), NULL, NULL, ''); + + diff --git a/wol-gateway/pom.xml b/wol-gateway/pom.xml index 6303a2b..bc1782b 100644 --- a/wol-gateway/pom.xml +++ b/wol-gateway/pom.xml @@ -20,6 +20,7 @@ com.agileboot wol-common-nacos + 1.0.0 com.agileboot diff --git a/wol-gateway/src/main/java/com/agileboot/gateway/config/SaTokenConfig.java b/wol-gateway/src/main/java/com/agileboot/gateway/config/SaTokenConfig.java index f42bfc9..c8976d6 100644 --- a/wol-gateway/src/main/java/com/agileboot/gateway/config/SaTokenConfig.java +++ b/wol-gateway/src/main/java/com/agileboot/gateway/config/SaTokenConfig.java @@ -1,60 +1,51 @@ package com.agileboot.gateway.config; import cn.dev33.satoken.exception.NotLoginException; -import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.agileboot.common.core.constant.HttpStatus; -import com.agileboot.common.satoken.utils.LoginHelper; +import com.agileboot.common.satoken.config.PublicUrlsProperties; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.server.reactive.ServerHttpRequest; /** - * [Sa-Token 权限认证] 拦截器 + * [Sa-Token 权限认证] 拦截器 - Gateway网关环境(Reactor响应式) * * @author Lion Li */ @Slf4j @Configuration +@RequiredArgsConstructor public class SaTokenConfig { + private final PublicUrlsProperties publicUrlsProperties; + /** * 注册 Sa-Token 全局过滤器 + * 从yml配置文件读取公开接口列表 */ @Bean public SaReactorFilter getSaReactorFilter() { + String[] publicUrls = publicUrlsProperties.getUrlsArray(); + return new SaReactorFilter() - // 拦截地址 + // 拦截所有路径 .addInclude("/**") - .addExclude("/favicon.ico") - .addExclude("/auth/getConfig", "/captcha/code", "/auth/register") - // 鉴权方法:每次访问进入 + // 排除公开接口(从yml配置读取) + .addExclude(publicUrls) + // 登录校验 .setAuth(obj -> { - // 登录校验 -- 拦截所有路由 - SaRouter.match("/**", "/auth/login", StpUtil::checkLogin) -// .check(r -> { -// ServerHttpRequest request = SaReactorSyncHolder.getExchange().getRequest(); -// // 检查是否登录 是否有token -// StpUtil.checkLogin(); -// -// // 检查 header 与 param 里的 clientid 与 token 里的是否一致 -// String headerCid = request.getHeaders().getFirst(LoginHelper.CLIENT_KEY); -// String paramCid = request.getQueryParams().getFirst(LoginHelper.CLIENT_KEY); -// String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString(); -// if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) { -// // token 无效 -// throw NotLoginException.newInstance(StpUtil.getLoginType(), -// "-100", "客户端ID与Token不匹配", -// StpUtil.getTokenValue()); -// } -// }) - ; - }).setError(e -> { + // 匹配所有路径,排除公开接口,其他需要登录 + SaRouter.match("/**") + .notMatch(publicUrls) + .check(r -> StpUtil.checkLogin()); + }) + // 异常处理 + .setError(e -> { if (e instanceof NotLoginException) { return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED); } diff --git a/wol-gateway/src/main/resources/application-dev.yml b/wol-gateway/src/main/resources/application-dev.yml index fad5b3a..95570a5 100644 --- a/wol-gateway/src/main/resources/application-dev.yml +++ b/wol-gateway/src/main/resources/application-dev.yml @@ -8,8 +8,12 @@ spring: - id: wol-auth uri: lb://wol-auth predicates: - - Path=/auth/** + - Path=/auth/**,/system/** - id: wol-module-codegen uri: lb://wol-module-codegen predicates: - Path=/codegen/** + - id: wol-module-reader + uri: lb://wol-module-reader + predicates: + - Path=/reader/** diff --git a/wol-gateway/src/main/resources/application.yml b/wol-gateway/src/main/resources/application.yml index 21f4f17..41806bc 100644 --- a/wol-gateway/src/main/resources/application.yml +++ b/wol-gateway/src/main/resources/application.yml @@ -14,6 +14,21 @@ spring: # 响应式环境(Gateway):Redisson 的 Bean 和自定义 Bean 同时加载,产生冲突 # 在 WebFlux(Gateway)和 Servlet(Auth)环境中,自动配置的策略不同 allow-bean-definition-overriding: true + +# Gateway公开接口配置(统一入口,集中管理所有公开接口) +# 这里配置的接口无需登录即可通过Gateway访问 +sa-token: + public: + urls: + - /favicon.ico + # 认证相关接口 + - /auth/login + - /auth/getConfig + - /auth/register + - /captcha/code + # 文本阅读器公开接口 + - /reader/public/** + # 如需添加新的公开接口,在这里添加即可 #logging: # level: # com.alibaba.cloud.nacos: DEBUG diff --git a/wol-modules/pom.xml b/wol-modules/pom.xml index 29625bc..87aa571 100644 --- a/wol-modules/pom.xml +++ b/wol-modules/pom.xml @@ -13,32 +13,44 @@ agileboot-system-base wol-module-codegen wol-module-ai + wol-module-reader + + com.agileboot + wol-common-core + 1.0.0 + com.agileboot wol-common-nacos + 1.0.0 com.agileboot wol-common-satoken + 1.0.0 com.agileboot wol-common-web + 1.0.0 com.agileboot wol-common-mybatis + 1.0.0 com.agileboot wol-common-redis + 1.0.0 com.agileboot wol-domain + 1.0.0 diff --git a/wol-modules/wol-module-reader/pom.xml b/wol-modules/wol-module-reader/pom.xml new file mode 100644 index 0000000..a8ebac6 --- /dev/null +++ b/wol-modules/wol-module-reader/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + + com.agileboot + wol-modules + 1.0.0 + + + wol-module-reader + + wol-module-reader + + + wol-module-reader + UTF-8 + 17 + 17 + + + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/ReaderApplication.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/ReaderApplication.java new file mode 100644 index 0000000..99a4cbc --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/ReaderApplication.java @@ -0,0 +1,25 @@ +package com.agileboot.reader; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 文本阅读器服务启动类 + */ +@SpringBootApplication(scanBasePackages = {"com.agileboot.reader"}) +public class ReaderApplication { + + public static void main(String[] args) { + SpringApplication.run(ReaderApplication.class, args); + String successMsg = " ____ _ \n" + + " | _ \\ ___ __ _ __| | ___ _ __ \n" + + " | |_) / _ \\/ _` |/ _` |/ _ \\ '__| \n" + + " | _ < __/ (_| | (_| | __/ | \n" + + " |_| \\_\\___|\\__,_|\\__,_|\\___|_| \n" + + " "; + + System.out.println(successMsg); + System.out.println("(♥◠‿◠)ノ゙ 文本阅读器服务启动成功 ლ(´ڡ`ლ)゙ "); + } +} + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextFileController.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextFileController.java new file mode 100644 index 0000000..0c7b5fb --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextFileController.java @@ -0,0 +1,75 @@ +package com.agileboot.reader.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.agileboot.common.core.core.R; +import com.agileboot.common.mybatis.core.page.PageR; +import com.agileboot.reader.dto.TextFileQueryDTO; +import com.agileboot.reader.service.ITextFileService; +import com.agileboot.reader.vo.TextFileContentVO; +import com.agileboot.reader.vo.TextFileVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 文本文件管理Controller + */ +@Slf4j +@RestController +@RequestMapping("/reader/file") +@RequiredArgsConstructor +@Api(tags = "文本文件管理") +public class TextFileController { + + private final ITextFileService textFileService; + + @PostMapping("/upload") + @ApiOperation("上传文本文件") + public R uploadFile( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "description", required = false) String description) { + Long fileId = textFileService.uploadFile(file, description); + return R.ok(fileId); + } + + @GetMapping("/list") + @ApiOperation("获取文本文件列表(后台管理)") + public PageR getFileList(TextFileQueryDTO query) { + PageR page = textFileService.getFileList(query); + return page; + } + + @GetMapping("/detail/{fileId}") + @ApiOperation("获取文件详情") + public R getFileDetail(@PathVariable Long fileId) { + TextFileVO file = textFileService.getFileDetail(fileId); + return R.ok(file); + } + + @DeleteMapping("/{fileId}") + @ApiOperation("删除文件") + public R deleteFile(@PathVariable Long fileId) { + textFileService.deleteFile(fileId); + return R.ok(); + } + + @DeleteMapping("/batch") + @ApiOperation("批量删除文件") + public R deleteBatch(@RequestBody List fileIds) { + textFileService.deleteBatch(fileIds); + return R.ok(); + } + + @PutMapping("/{fileId}/status/{status}") + @ApiOperation("更新文件状态") + public R updateStatus(@PathVariable Long fileId, @PathVariable Integer status) { + textFileService.updateStatus(fileId, status); + return R.ok(); + } +} + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextReaderController.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextReaderController.java new file mode 100644 index 0000000..83f0702 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/controller/TextReaderController.java @@ -0,0 +1,49 @@ +package com.agileboot.reader.controller; + +import com.agileboot.common.core.core.R; +import com.agileboot.reader.service.ITextFileService; +import com.agileboot.reader.vo.TextFileContentVO; +import com.agileboot.reader.vo.TextFileVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 文本阅读器Controller(用户端) + * 注意:公开接口在 PublicUrlsConfig 中统一配置,无需 @SaIgnore 注解 + */ +@Slf4j +@RestController +@RequestMapping("/reader/public") +@RequiredArgsConstructor +@Api(tags = "文本阅读器(用户端)") +public class TextReaderController { + + private final ITextFileService textFileService; + + @GetMapping("/files") + @ApiOperation("获取所有可用文本文件列表") + public R> getAllFiles() { + List files = textFileService.getAllFiles(); + return R.ok(files); + } + + @GetMapping("/file/{fileId}") + @ApiOperation("获取文件详情") + public R getFileDetail(@PathVariable Long fileId) { + TextFileVO file = textFileService.getFileDetail(fileId); + return R.ok(file); + } + + @GetMapping("/read/{fileId}") + @ApiOperation("读取文件内容") + public R readFile(@PathVariable Long fileId) { + TextFileContentVO content = textFileService.readFileContent(fileId); + return R.ok(content); + } +} + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileQueryDTO.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileQueryDTO.java new file mode 100644 index 0000000..5823ca0 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileQueryDTO.java @@ -0,0 +1,38 @@ +package com.agileboot.reader.dto; + +import com.agileboot.common.mybatis.core.page.PageQuery; +import com.agileboot.reader.entity.TextFile; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; + +/** + * 文本文件查询DTO + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class TextFileQueryDTO extends PageQuery { + + /** + * 文件名(模糊查询) + */ + private String fileName; + + /** + * 状态 + */ + private Integer status; + + @Override + public LambdaQueryWrapper toQueryWrapper() { + return Wrappers.lambdaQuery(TextFile.class) + .like(StringUtils.isNotEmpty(fileName), TextFile::getOriginalFileName, fileName) + .or() + .like(StringUtils.isNotEmpty(fileName), TextFile::getDescription, fileName) + .eq(status != null, TextFile::getStatus, status) + .orderByDesc(TextFile::getCreateTime); + } +} + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileUploadDTO.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileUploadDTO.java new file mode 100644 index 0000000..348751f --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/dto/TextFileUploadDTO.java @@ -0,0 +1,17 @@ +package com.agileboot.reader.dto; + +import lombok.Data; + +/** + * 文本文件上传DTO + */ +@Data +public class TextFileUploadDTO { + + /** + * 文件描述 + */ + private String description; +} + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/entity/TextFile.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/entity/TextFile.java new file mode 100644 index 0000000..b93be36 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/entity/TextFile.java @@ -0,0 +1,62 @@ +package com.agileboot.reader.entity; + +import com.agileboot.common.mybatis.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 文本文件实体类 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("text_file") +public class TextFile extends BaseEntity { + + @TableId(type = IdType.AUTO) + private Long fileId; + + /** + * 文件名 + */ + private String fileName; + + /** + * 原始文件名 + */ + private String originalFileName; + + /** + * 文件路径 + */ + private String filePath; + + /** + * 文件大小(字节) + */ + private Long fileSize; + + /** + * 文件描述 + */ + private String description; + + /** + * 状态 0=正常 1=禁用 + */ + private Integer status; + + /** + * 上传用户ID + */ + private Long uploadUserId; + + /** + * 上传用户名 + */ + private String uploadUserName; +} + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/TextFileMapper.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/TextFileMapper.java new file mode 100644 index 0000000..e0b87c9 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/TextFileMapper.java @@ -0,0 +1,18 @@ +package com.agileboot.reader.mapper; + +import com.agileboot.common.mybatis.mapper.BaseMapperDelete; +import com.agileboot.reader.entity.TextFile; + +/** + *

+ * 文本文件表 Mapper 接口 + *

+ * + * @author agileboot + * @since 2025-11-01 + */ +public interface TextFileMapper extends BaseMapperDelete { + +} + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/xml/TextFileMapper.xml b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/xml/TextFileMapper.xml new file mode 100644 index 0000000..d0e486f --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/mapper/xml/TextFileMapper.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/ITextFileService.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/ITextFileService.java new file mode 100644 index 0000000..05632a8 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/ITextFileService.java @@ -0,0 +1,57 @@ +package com.agileboot.reader.service; + +import com.agileboot.common.mybatis.core.page.PageR; +import com.agileboot.reader.dto.TextFileQueryDTO; +import com.agileboot.reader.vo.TextFileContentVO; +import com.agileboot.reader.vo.TextFileVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 文本文件服务接口 + */ +public interface ITextFileService { + + /** + * 上传文本文件 + */ + Long uploadFile(MultipartFile file, String description); + + /** + * 分页查询文本文件列表 + */ + PageR getFileList(TextFileQueryDTO query); + + /** + * 获取所有文本文件列表(用户端) + */ + List getAllFiles(); + + /** + * 获取文件详情 + */ + TextFileVO getFileDetail(Long fileId); + + /** + * 读取文件内容 + */ + TextFileContentVO readFileContent(Long fileId); + + /** + * 删除文件 + */ + boolean deleteFile(Long fileId); + + /** + * 批量删除文件 + */ + boolean deleteBatch(List fileIds); + + /** + * 更新文件状态 + */ + boolean updateStatus(Long fileId, Integer status); +} + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/impl/TextFileServiceImpl.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/impl/TextFileServiceImpl.java new file mode 100644 index 0000000..d5139c8 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/service/impl/TextFileServiceImpl.java @@ -0,0 +1,242 @@ +package com.agileboot.reader.service.impl; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.agileboot.common.core.exception.BizException; +import com.agileboot.common.core.exception.error.ErrorCode; +import com.agileboot.common.mybatis.core.page.PageQuery; +import com.agileboot.common.mybatis.core.page.PageR; +import com.agileboot.common.satoken.utils.LoginHelper; +import com.agileboot.reader.dto.TextFileQueryDTO; +import com.agileboot.reader.entity.TextFile; +import com.agileboot.reader.mapper.TextFileMapper; +import com.agileboot.reader.service.ITextFileService; +import com.agileboot.reader.vo.TextFileContentVO; +import com.agileboot.reader.vo.TextFileVO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文本文件服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TextFileServiceImpl implements ITextFileService { + + private final TextFileMapper textFileMapper; + + @Value("${reader.upload.path:./upload/reader}") + private String uploadPath; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long uploadFile(MultipartFile file, String description) { + if (file == null || file.isEmpty()) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件不能为空"); + } + + String originalFilename = file.getOriginalFilename(); + if (StrUtil.isBlank(originalFilename)) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件名不能为空"); + } + + // 检查文件类型 + String extension = FileUtil.extName(originalFilename); + if (!"txt".equalsIgnoreCase(extension)) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "只支持上传txt文件"); + } + + try { + // 获取绝对路径 + String absoluteUploadPath = FileUtil.getAbsolutePath(uploadPath); + + // 创建上传目录 + File uploadDir = new File(absoluteUploadPath); + if (!uploadDir.exists()) { + boolean created = uploadDir.mkdirs(); + if (!created) { + log.error("创建上传目录失败: {}", absoluteUploadPath); + } + } + + // 生成唯一文件名 + String fileName = IdUtil.fastSimpleUUID() + ".txt"; + String filePath = absoluteUploadPath + File.separator + fileName; + File destFile = new File(filePath); + + // 保存文件 + file.transferTo(destFile); + + // 保存文件信息到数据库 + TextFile textFile = new TextFile(); + textFile.setFileName(fileName); + textFile.setOriginalFileName(originalFilename); + textFile.setFilePath(filePath); + textFile.setFileSize(file.getSize()); + textFile.setDescription(description); + textFile.setStatus(0); + + // 获取当前登录用户信息 + try { + Long userId = LoginHelper.getUserId(); + String username = LoginHelper.getUsername(); + textFile.setUploadUserId(userId); + textFile.setUploadUserName(username); + } catch (Exception e) { + log.warn("获取登录用户信息失败", e); + } + + textFileMapper.insert(textFile); + return textFile.getFileId(); + } catch (IOException e) { + log.error("文件上传失败", e); + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件上传失败:" + e.getMessage()); + } + } + + @Override + public PageR getFileList(TextFileQueryDTO query) { + Page page = textFileMapper.selectPage(query.toPage(), query.toQueryWrapper()); + + List voList = page.getRecords().stream().map(this::convertToVO).collect(Collectors.toList()); + + return new PageR<>(page, voList); + } + + @Override + public List getAllFiles() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(TextFile::getStatus, 0); + wrapper.orderByDesc(TextFile::getCreateTime); + + List files = textFileMapper.selectList(wrapper); + return files.stream().map(this::convertToVO).collect(Collectors.toList()); + } + + @Override + public TextFileVO getFileDetail(Long fileId) { + TextFile textFile = textFileMapper.selectById(fileId); + if (textFile == null) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件不存在"); + } + return convertToVO(textFile); + } + + @Override + public TextFileContentVO readFileContent(Long fileId) { + TextFile textFile = textFileMapper.selectById(fileId); + if (textFile == null) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件不存在"); + } + + File file = new File(textFile.getFilePath()); + if (!file.exists()) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件已被删除"); + } + + try { + // 尝试读取文件内容,自动检测编码 + String content = readFileWithEncoding(file); + + TextFileContentVO vo = new TextFileContentVO(); + vo.setFileId(textFile.getFileId()); + vo.setFileName(textFile.getOriginalFileName()); + vo.setContent(content); + vo.setFileSize(textFile.getFileSize()); + + return vo; + } catch (Exception e) { + log.error("读取文件内容失败", e); + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "读取文件内容失败:" + e.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteFile(Long fileId) { + TextFile textFile = textFileMapper.selectById(fileId); + if (textFile == null) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件不存在"); + } + + // 删除物理文件 + File file = new File(textFile.getFilePath()); + if (file.exists()) { + file.delete(); + } + + // 删除数据库记录 + return textFileMapper.deleteById(fileId) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteBatch(List fileIds) { + if (fileIds == null || fileIds.isEmpty()) { + return false; + } + + for (Long fileId : fileIds) { + deleteFile(fileId); + } + + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateStatus(Long fileId, Integer status) { + TextFile textFile = textFileMapper.selectById(fileId); + if (textFile == null) { + throw new BizException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, "文件不存在"); + } + + textFile.setStatus(status); + return textFileMapper.updateById(textFile) > 0; + } + + /** + * 转换为VO + */ + private TextFileVO convertToVO(TextFile textFile) { + TextFileVO vo = new TextFileVO(); + BeanUtils.copyProperties(textFile, vo); + return vo; + } + + /** + * 读取文件内容,自动检测编码 + */ + private String readFileWithEncoding(File file) { + try { + // 尝试UTF-8 + return FileUtil.readString(file, StandardCharsets.UTF_8); + } catch (Exception e) { + try { + // 尝试GBK + return FileUtil.readString(file, Charset.forName("GBK")); + } catch (Exception ex) { + // 使用系统默认编码 + return FileUtil.readString(file, Charset.defaultCharset()); + } + } + } +} + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileContentVO.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileContentVO.java new file mode 100644 index 0000000..81523a0 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileContentVO.java @@ -0,0 +1,20 @@ +package com.agileboot.reader.vo; + +import lombok.Data; + +/** + * 文本文件内容VO + */ +@Data +public class TextFileContentVO { + + private Long fileId; + + private String fileName; + + private String content; + + private Long fileSize; +} + + diff --git a/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileVO.java b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileVO.java new file mode 100644 index 0000000..6bbc495 --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/java/com/agileboot/reader/vo/TextFileVO.java @@ -0,0 +1,35 @@ +package com.agileboot.reader.vo; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 文本文件VO + */ +@Data +public class TextFileVO { + + private Long fileId; + + private String fileName; + + private String originalFileName; + + private String filePath; + + private Long fileSize; + + private String description; + + private Integer status; + + private Long uploadUserId; + + private String uploadUserName; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/wol-modules/wol-module-reader/src/main/resources/application.yml b/wol-modules/wol-module-reader/src/main/resources/application.yml new file mode 100644 index 0000000..afbf31d --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 9212 + servlet: + context-path: / +spring: + application: + name: @application.name@ + profiles: + active: dev + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + +# Reader文本阅读器配置 +reader: + upload: + path: ./upload/reader + +# Reader服务的公开接口配置 +# 注意:这里只需配置Reader自己的公开接口即可 +# 通用的认证接口会从common-satoken.yml继承 +sa-token: + public: + urls: + # Reader服务特有的公开接口 + - /reader/public/** + diff --git a/wol-modules/wol-module-reader/src/main/resources/bootstrap.yml b/wol-modules/wol-module-reader/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..801fcea --- /dev/null +++ b/wol-modules/wol-module-reader/src/main/resources/bootstrap.yml @@ -0,0 +1,6 @@ +spring: + application: + name: @application.name@ + config: + import: classpath:base.yml,classpath:nacos.yml +