feat(reader): 添加文本阅读器模块
- 新增文本文件上传、查询、删除、状态更新等功能 - 实现文件内容读取与编码自动识别 - 添加支持公开接口配置,无需登录访问- 配置数据库表结构与初始菜单权限- 完成前后端接口对接,支持Swagger文档 - 集成Sa-Token权限控制,区分公开与需登录接口- 添加文件存储路径配置与上传大小限制 - 实现批量删除与分页查询功能- 提供用户端公开接口用于文件浏览与阅读
This commit is contained in:
parent
ee41e544f5
commit
618381aa0f
@ -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}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> urls = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 获取公开URL数组
|
||||
*/
|
||||
public String[] getUrlsArray() {
|
||||
return urls.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
33
sql/text_file.sql
Normal file
33
sql/text_file.sql
Normal file
@ -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, '');
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-nacos</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -13,32 +13,44 @@
|
||||
<module>agileboot-system-base</module>
|
||||
<module>wol-module-codegen</module>
|
||||
<module>wol-module-ai</module>
|
||||
<module>wol-module-reader</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-core</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-nacos</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-satoken</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-web</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-mybatis</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-common-redis</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-domain</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
47
wol-modules/wol-module-reader/pom.xml
Normal file
47
wol-modules/wol-module-reader/pom.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.agileboot</groupId>
|
||||
<artifactId>wol-modules</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>wol-module-reader</artifactId>
|
||||
|
||||
<name>wol-module-reader</name>
|
||||
|
||||
<properties>
|
||||
<application.name>wol-module-reader</application.name>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖从父pom继承,无需单独声明 -->
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -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("(♥◠‿◠)ノ゙ 文本阅读器服务启动成功 ლ(´ڡ`ლ)゙ ");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Long> 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<TextFileVO> getFileList(TextFileQueryDTO query) {
|
||||
PageR<TextFileVO> page = textFileService.getFileList(query);
|
||||
return page;
|
||||
}
|
||||
|
||||
@GetMapping("/detail/{fileId}")
|
||||
@ApiOperation("获取文件详情")
|
||||
public R<TextFileVO> getFileDetail(@PathVariable Long fileId) {
|
||||
TextFileVO file = textFileService.getFileDetail(fileId);
|
||||
return R.ok(file);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{fileId}")
|
||||
@ApiOperation("删除文件")
|
||||
public R<Void> deleteFile(@PathVariable Long fileId) {
|
||||
textFileService.deleteFile(fileId);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping("/batch")
|
||||
@ApiOperation("批量删除文件")
|
||||
public R<Void> deleteBatch(@RequestBody List<Long> fileIds) {
|
||||
textFileService.deleteBatch(fileIds);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{fileId}/status/{status}")
|
||||
@ApiOperation("更新文件状态")
|
||||
public R<Void> updateStatus(@PathVariable Long fileId, @PathVariable Integer status) {
|
||||
textFileService.updateStatus(fileId, status);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<List<TextFileVO>> getAllFiles() {
|
||||
List<TextFileVO> files = textFileService.getAllFiles();
|
||||
return R.ok(files);
|
||||
}
|
||||
|
||||
@GetMapping("/file/{fileId}")
|
||||
@ApiOperation("获取文件详情")
|
||||
public R<TextFileVO> getFileDetail(@PathVariable Long fileId) {
|
||||
TextFileVO file = textFileService.getFileDetail(fileId);
|
||||
return R.ok(file);
|
||||
}
|
||||
|
||||
@GetMapping("/read/{fileId}")
|
||||
@ApiOperation("读取文件内容")
|
||||
public R<TextFileContentVO> readFile(@PathVariable Long fileId) {
|
||||
TextFileContentVO content = textFileService.readFileContent(fileId);
|
||||
return R.ok(content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TextFile> {
|
||||
|
||||
/**
|
||||
* 文件名(模糊查询)
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
@Override
|
||||
public LambdaQueryWrapper<TextFile> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package com.agileboot.reader.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文本文件上传DTO
|
||||
*/
|
||||
@Data
|
||||
public class TextFileUploadDTO {
|
||||
|
||||
/**
|
||||
* 文件描述
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package com.agileboot.reader.mapper;
|
||||
|
||||
import com.agileboot.common.mybatis.mapper.BaseMapperDelete;
|
||||
import com.agileboot.reader.entity.TextFile;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 文本文件表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author agileboot
|
||||
* @since 2025-11-01
|
||||
*/
|
||||
public interface TextFileMapper extends BaseMapperDelete<TextFile> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.agileboot.reader.mapper.TextFileMapper">
|
||||
|
||||
</mapper>
|
||||
|
||||
@ -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<TextFileVO> getFileList(TextFileQueryDTO query);
|
||||
|
||||
/**
|
||||
* 获取所有文本文件列表(用户端)
|
||||
*/
|
||||
List<TextFileVO> getAllFiles();
|
||||
|
||||
/**
|
||||
* 获取文件详情
|
||||
*/
|
||||
TextFileVO getFileDetail(Long fileId);
|
||||
|
||||
/**
|
||||
* 读取文件内容
|
||||
*/
|
||||
TextFileContentVO readFileContent(Long fileId);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
boolean deleteFile(Long fileId);
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
*/
|
||||
boolean deleteBatch(List<Long> fileIds);
|
||||
|
||||
/**
|
||||
* 更新文件状态
|
||||
*/
|
||||
boolean updateStatus(Long fileId, Integer status);
|
||||
}
|
||||
|
||||
|
||||
@ -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<TextFileVO> getFileList(TextFileQueryDTO query) {
|
||||
Page<TextFile> page = textFileMapper.selectPage(query.toPage(), query.toQueryWrapper());
|
||||
|
||||
List<TextFileVO> voList = page.getRecords().stream().map(this::convertToVO).collect(Collectors.toList());
|
||||
|
||||
return new PageR<>(page, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TextFileVO> getAllFiles() {
|
||||
LambdaQueryWrapper<TextFile> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(TextFile::getStatus, 0);
|
||||
wrapper.orderByDesc(TextFile::getCreateTime);
|
||||
|
||||
List<TextFile> 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<Long> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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/**
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: @application.name@
|
||||
config:
|
||||
import: classpath:base.yml,classpath:nacos.yml
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user