feat(日志): 新增日志文件下载功能
添加日志文件下载相关功能,包括: 1. 新增LogFileDTO用于传输日志文件信息 2. 实现LogFileService接口及其实现类处理日志文件操作 3. 添加日志下载应用服务LogDownloadApplicationService 4. 创建管理后台和客户端API的日志下载控制器 5. 在SecurityConfig中添加日志下载路径权限 6. 新增相关错误码定义
This commit is contained in:
parent
078c18fec9
commit
7cc0674b90
|
|
@ -0,0 +1,79 @@
|
|||
package com.agileboot.admin.controller.system;
|
||||
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import com.agileboot.common.core.base.BaseController;
|
||||
import com.agileboot.common.core.dto.ResponseDTO;
|
||||
import com.agileboot.common.exception.ApiException;
|
||||
import com.agileboot.common.exception.error.ErrorCode.Business;
|
||||
import com.agileboot.common.utils.file.FileUploadUtils;
|
||||
import com.agileboot.domain.system.log.LogDownloadApplicationService;
|
||||
import com.agileboot.domain.system.log.dto.LogFileDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日志下载控制器 (管理后台使用)
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
@Tag(name = "日志下载API", description = "服务器日志下载相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/system/log")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogDownloadController extends BaseController {
|
||||
|
||||
private final LogDownloadApplicationService logDownloadService;
|
||||
|
||||
/**
|
||||
* 获取日志文件列表
|
||||
*/
|
||||
@Operation(summary = "获取日志文件列表")
|
||||
@GetMapping("/files")
|
||||
public ResponseDTO<List<LogFileDTO>> getLogFileList() {
|
||||
List<LogFileDTO> fileList = logDownloadService.getLogFileList();
|
||||
return ResponseDTO.ok(fileList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载日志文件
|
||||
*
|
||||
* @param logType 日志类型: info, error, debug
|
||||
*/
|
||||
@Operation(summary = "下载日志文件")
|
||||
@GetMapping("/download/{logType}")
|
||||
public ResponseEntity<byte[]> downloadLog(@PathVariable String logType) {
|
||||
try {
|
||||
// 1. 获取日志文件内容
|
||||
byte[] fileContent = logDownloadService.getLogFileContent(logType);
|
||||
|
||||
// 2. 获取文件名
|
||||
String fileName = logDownloadService.getLogFileName(logType);
|
||||
|
||||
// 3. 构建下载响应头
|
||||
HttpHeaders headers = FileUploadUtils.getDownloadHeader(fileName);
|
||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
return new ResponseEntity<>(fileContent, headers, org.springframework.http.HttpStatus.OK);
|
||||
} catch (ApiException e) {
|
||||
log.error("下载日志文件失败: logType={}, error={}", logType, e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("下载日志文件失败: logType={}", logType, e);
|
||||
throw new ApiException(Business.LOG_FILE_READ_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -137,7 +137,8 @@ public class SecurityConfig {
|
|||
.antMatchers("/login", "/register", "/captchaImage", "/api/**", "/file/**").anonymous()
|
||||
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js",
|
||||
"/profile/**").permitAll()
|
||||
.antMatchers("/manual/**", "/qywx/**", "/test/**", "/monitor/**", "/getQyUserinfo", "/getConfig").permitAll()
|
||||
.antMatchers("/manual/**", "/qywx/**", "/test/**", "/monitor/**",
|
||||
"/system/log/**", "/getQyUserinfo", "/getConfig").permitAll()
|
||||
// TODO this is danger.
|
||||
.antMatchers("/swagger-ui.html").anonymous()
|
||||
.antMatchers("/swagger-resources/**").anonymous()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.agileboot.api.controller;
|
||||
|
||||
import com.agileboot.common.core.dto.ResponseDTO;
|
||||
import com.agileboot.common.exception.ApiException;
|
||||
import com.agileboot.common.exception.error.ErrorCode.Business;
|
||||
import com.agileboot.common.utils.file.FileUploadUtils;
|
||||
import com.agileboot.domain.system.log.LogDownloadApplicationService;
|
||||
import com.agileboot.domain.system.log.dto.LogFileDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日志下载控制器 (客户端API使用)
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
@Tag(name = "日志下载API", description = "服务器日志下载相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/log")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogDownloadController {
|
||||
|
||||
private final LogDownloadApplicationService logDownloadService;
|
||||
|
||||
/**
|
||||
* 获取日志文件列表
|
||||
*/
|
||||
@Operation(summary = "获取日志文件列表")
|
||||
@GetMapping("/files")
|
||||
public ResponseDTO<List<LogFileDTO>> getLogFileList() {
|
||||
List<LogFileDTO> fileList = logDownloadService.getLogFileList();
|
||||
return ResponseDTO.ok(fileList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载日志文件
|
||||
*
|
||||
* @param logType 日志类型: info, error, debug
|
||||
*/
|
||||
@Operation(summary = "下载日志文件")
|
||||
@GetMapping("/download/{logType}")
|
||||
public ResponseEntity<byte[]> downloadLog(@PathVariable String logType) {
|
||||
try {
|
||||
// 1. 获取日志文件内容
|
||||
byte[] fileContent = logDownloadService.getLogFileContent(logType);
|
||||
|
||||
// 2. 获取文件名
|
||||
String fileName = logDownloadService.getLogFileName(logType);
|
||||
|
||||
// 3. 构建下载响应头
|
||||
HttpHeaders headers = FileUploadUtils.getDownloadHeader(fileName);
|
||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
return new ResponseEntity<>(fileContent, headers, org.springframework.http.HttpStatus.OK);
|
||||
} catch (ApiException e) {
|
||||
log.error("下载日志文件失败: logType={}, error={}", logType, e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("下载日志文件失败: logType={}", logType, e);
|
||||
throw new ApiException(Business.LOG_FILE_READ_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -51,13 +53,13 @@ public class AdminLoginController {
|
|||
SysUserEntity userEntity = userService.getById(userId);
|
||||
if (userEntity == null) {
|
||||
log.warn("用户不存在, userId: {}", userId);
|
||||
return ResponseDTO.ok(List.of()); // 返回空列表而不是错误
|
||||
return ResponseDTO.ok(new ArrayList<>()); // 返回空列表而不是错误
|
||||
}
|
||||
|
||||
// 2. 检查用户状态 (可选,根据需求决定)
|
||||
if (userEntity.getStatus() != 1) { // 1表示正常状态
|
||||
log.warn("用户状态异常, userId: {}, status: {}", userId, userEntity.getStatus());
|
||||
return ResponseDTO.ok(List.of());
|
||||
return ResponseDTO.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
// 4. 构造 SystemLoginUser (isAdmin设置为false)
|
||||
|
|
@ -87,12 +89,12 @@ public class AdminLoginController {
|
|||
SysUserEntity userEntity = userService.getById(userId);
|
||||
if (userEntity == null) {
|
||||
log.warn("用户不存在, userId: {}", userId);
|
||||
return ResponseDTO.ok(List.of());
|
||||
return ResponseDTO.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
if (userEntity.getStatus() != 1) {
|
||||
log.warn("用户状态异常, userId: {}, status: {}", userId, userEntity.getStatus());
|
||||
return ResponseDTO.ok(List.of());
|
||||
return ResponseDTO.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
SystemLoginUser loginUser = new SystemLoginUser(
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ public enum ErrorCode implements ErrorCodeInterface {
|
|||
|
||||
PERMISSION_NOT_ALLOWED_TO_OPERATE(10202, "没有权限进行此操作,请联系管理员", "Business.NO_PERMISSION_TO_OPERATE"),
|
||||
|
||||
// ----------------------------- LOG -------------------------------------------
|
||||
|
||||
LOG_TYPE_NOT_FOUND(10301, "日志类型 {} 不存在", "Business.LOG_TYPE_NOT_FOUND"),
|
||||
|
||||
LOG_PATH_NOT_CONFIGURED(10302, "日志路径未配置", "Business.LOG_PATH_NOT_CONFIGURED"),
|
||||
|
||||
LOG_FILE_READ_FAILED(10303, "读取日志文件失败: {}", "Business.LOG_FILE_READ_FAILED"),
|
||||
|
||||
// ----------------------------- LOGIN -----------------------------------------
|
||||
|
||||
LOGIN_WRONG_USER_PASSWORD(10201, "用户密码错误,请重输", "Business.LOGIN_WRONG_USER_PASSWORD"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.agileboot.domain.system.log;
|
||||
|
||||
import com.agileboot.domain.system.log.dto.LogFileDTO;
|
||||
import com.agileboot.domain.system.log.service.LogFileService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日志下载应用服务
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogDownloadApplicationService {
|
||||
|
||||
private final LogFileService logFileService;
|
||||
|
||||
/**
|
||||
* 获取日志文件列表
|
||||
*/
|
||||
public List<LogFileDTO> getLogFileList() {
|
||||
return logFileService.getLogFileList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件内容
|
||||
*/
|
||||
public byte[] getLogFileContent(String logType) {
|
||||
return logFileService.getLogFileContent(logType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件名
|
||||
*/
|
||||
public String getLogFileName(String logType) {
|
||||
return logFileService.getLogFileName(logType);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.agileboot.domain.system.log.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 日志文件信息DTO
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LogFileDTO {
|
||||
|
||||
/**
|
||||
* 日志类型: info, error, debug
|
||||
*/
|
||||
private String logType;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件是否存在
|
||||
*/
|
||||
private Boolean exists;
|
||||
|
||||
/**
|
||||
* 最后修改时间
|
||||
*/
|
||||
private Long lastModified;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
private Long size;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.agileboot.domain.system.log.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.agileboot.domain.system.log.dto.LogFileDTO;
|
||||
|
||||
/**
|
||||
* 日志文件服务接口
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
public interface LogFileService {
|
||||
|
||||
/**
|
||||
* 获取可下载的日志文件列表
|
||||
*
|
||||
* @return 日志文件信息列表
|
||||
*/
|
||||
List<LogFileDTO> getLogFileList();
|
||||
|
||||
/**
|
||||
* 根据日志类型获取日志文件字节内容
|
||||
*
|
||||
* @param logType 日志类型 (info, error, debug)
|
||||
* @return 文件字节数组
|
||||
*/
|
||||
byte[] getLogFileContent(String logType);
|
||||
|
||||
/**
|
||||
* 获取日志文件名
|
||||
*
|
||||
* @param logType 日志类型
|
||||
* @return 文件名
|
||||
*/
|
||||
String getLogFileName(String logType);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.agileboot.domain.system.log.service;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.agileboot.common.exception.ApiException;
|
||||
import com.agileboot.common.exception.error.ErrorCode.Business;
|
||||
import com.agileboot.domain.system.log.dto.LogFileDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 日志文件服务实现
|
||||
*
|
||||
* @author agileboot
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LogFileServiceImpl implements LogFileService {
|
||||
|
||||
/**
|
||||
* 日志路径
|
||||
*/
|
||||
@Value("${logging.file.path:}")
|
||||
private String logPath;
|
||||
|
||||
/**
|
||||
* 允许的日志类型
|
||||
*/
|
||||
private static final List<String> ALLOWED_LOG_TYPES = Arrays.asList("info", "error", "debug");
|
||||
|
||||
/**
|
||||
* 日志类型到文件名前缀的映射
|
||||
*/
|
||||
private static final String LOG_FILE_PREFIX_INFO = "sys-info";
|
||||
private static final String LOG_FILE_PREFIX_ERROR = "sys-error";
|
||||
private static final String LOG_FILE_PREFIX_DEBUG = "sys-debug";
|
||||
|
||||
@Override
|
||||
public List<LogFileDTO> getLogFileList() {
|
||||
if (StrUtil.isEmpty(logPath)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return ALLOWED_LOG_TYPES.stream()
|
||||
.map(this::buildLogFileDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private LogFileDTO buildLogFileDTO(String logType) {
|
||||
String fileName = getLogFileName(logType);
|
||||
String filePath = logPath + File.separator + fileName;
|
||||
File file = new File(filePath);
|
||||
|
||||
LogFileDTO dto = new LogFileDTO();
|
||||
dto.setLogType(logType);
|
||||
dto.setFileName(fileName);
|
||||
dto.setExists(file.exists());
|
||||
dto.setLastModified(file.exists() ? file.lastModified() : null);
|
||||
dto.setSize(file.exists() ? file.length() : null);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getLogFileContent(String logType) {
|
||||
// 1. 参数校验
|
||||
if (StrUtil.isEmpty(logType)) {
|
||||
throw new ApiException(Business.COMMON_BAD_REQUEST, "日志类型不能为空");
|
||||
}
|
||||
|
||||
// 2. 日志类型校验 - 防止目录遍历攻击
|
||||
String normalizedType = logType.toLowerCase().trim();
|
||||
if (!ALLOWED_LOG_TYPES.contains(normalizedType)) {
|
||||
throw new ApiException(Business.COMMON_OBJECT_NOT_FOUND,
|
||||
"日志类型", normalizedType);
|
||||
}
|
||||
|
||||
// 3. 校验日志路径配置
|
||||
if (StrUtil.isEmpty(logPath)) {
|
||||
throw new ApiException(Business.COMMON_OBJECT_NOT_FOUND,
|
||||
"日志配置", "日志路径未配置");
|
||||
}
|
||||
|
||||
// 4. 构建文件路径
|
||||
String fileName = getLogFileName(normalizedType);
|
||||
String filePath = logPath + File.separator + fileName;
|
||||
|
||||
// 5. 验证文件路径安全 - 确保路径在日志目录下
|
||||
File logDir = new File(logPath);
|
||||
File targetFile = new File(filePath);
|
||||
|
||||
try {
|
||||
String canonicalDirPath = logDir.getCanonicalPath();
|
||||
String canonicalFilePath = targetFile.getCanonicalPath();
|
||||
|
||||
if (!canonicalFilePath.startsWith(canonicalDirPath + File.separator)) {
|
||||
throw new ApiException(Business.COMMON_FILE_NOT_ALLOWED_TO_DOWNLOAD, fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("验证文件路径失败: {}", filePath, e);
|
||||
throw new ApiException(Business.COMMON_FILE_NOT_ALLOWED_TO_DOWNLOAD, fileName);
|
||||
}
|
||||
|
||||
// 6. 检查文件是否存在
|
||||
if (!targetFile.exists() || !targetFile.isFile()) {
|
||||
throw new ApiException(Business.COMMON_OBJECT_NOT_FOUND, "日志文件", fileName);
|
||||
}
|
||||
|
||||
// 7. 读取文件内容
|
||||
try {
|
||||
return FileUtil.readBytes(targetFile);
|
||||
} catch (Exception e) {
|
||||
log.error("读取日志文件失败: {}", filePath, e);
|
||||
throw new ApiException(Business.UPLOAD_FILE_FAILED, "读取日志文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLogFileName(String logType) {
|
||||
String lowerType = logType.toLowerCase();
|
||||
if ("info".equals(lowerType)) {
|
||||
return LOG_FILE_PREFIX_INFO + ".log";
|
||||
} else if ("error".equals(lowerType)) {
|
||||
return LOG_FILE_PREFIX_ERROR + ".log";
|
||||
} else if ("debug".equals(lowerType)) {
|
||||
return LOG_FILE_PREFIX_DEBUG + ".log";
|
||||
} else {
|
||||
throw new ApiException(Business.COMMON_OBJECT_NOT_FOUND, "日志类型", logType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue