feat(reader): 添加文本阅读器模块

- 新增文本文件上传、查询、删除、状态更新等功能
- 实现文件内容读取与编码自动识别
- 添加支持公开接口配置,无需登录访问- 配置数据库表结构与初始菜单权限- 完成前后端接口对接,支持Swagger文档
- 集成Sa-Token权限控制,区分公开与需登录接口- 添加文件存储路径配置与上传大小限制
- 实现批量删除与分页查询功能- 提供用户端公开接口用于文件浏览与阅读
This commit is contained in:
cuijiawang 2025-11-04 17:59:57 +08:00
parent ee41e544f5
commit 618381aa0f
26 changed files with 912 additions and 35 deletions

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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]);
}
}

View File

@ -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);

View File

@ -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
View 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, '');

View File

@ -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>

View File

@ -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);
}

View File

@ -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/**

View File

@ -14,6 +14,21 @@ spring:
# 响应式环境GatewayRedisson 的 Bean 和自定义 Bean 同时加载,产生冲突
# 在 WebFluxGateway和 ServletAuth环境中自动配置的策略不同
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

View File

@ -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>

View 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>

View File

@ -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("(♥◠‿◠)ノ゙ 文本阅读器服务启动成功 ლ(´ڡ`ლ)゙ ");
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,17 @@
package com.agileboot.reader.dto;
import lombok.Data;
/**
* 文本文件上传DTO
*/
@Data
public class TextFileUploadDTO {
/**
* 文件描述
*/
private String description;
}

View File

@ -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;
}

View File

@ -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> {
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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());
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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/**

View File

@ -0,0 +1,6 @@
spring:
application:
name: @application.name@
config:
import: classpath:base.yml,classpath:nacos.yml