832 lines
27 KiB
Markdown
832 lines
27 KiB
Markdown
# 模块12:错误处理与日志
|
||
|
||
---
|
||
|
||
## 🎨 UI设计系统概览
|
||
|
||
> **完整设计系统文档请参考:** `UI设计系统.md`
|
||
|
||
### 核心设计原则
|
||
- **现代简约**:界面清晰,层次分明
|
||
- **专业高效**:减少操作步骤,提升工作效率
|
||
- **一致性**:统一的视觉语言和交互模式
|
||
- **可访问性**:符合WCAG 2.1 AA标准
|
||
|
||
### 关键设计令牌
|
||
|
||
**颜色系统:**
|
||
- 主色:`#0d6efd`(操作按钮、选中状态)
|
||
- 成功:`#198754`(连接成功状态)
|
||
- 危险:`#dc3545`(删除操作、错误提示)
|
||
- 深灰:`#212529`(导航栏背景)
|
||
- 浅灰:`#e9ecef`(工具栏背景)
|
||
|
||
**字体系统:**
|
||
- 字体族:系统字体栈(-apple-system, Segoe UI, Roboto等)
|
||
- 正文:14px,行高1.5
|
||
- 标题:20-32px,行高1.2-1.4
|
||
- 小号文字:12px(文件大小、日期等)
|
||
|
||
**间距系统:**
|
||
- 基础单位:8px
|
||
- 标准间距:16px(1rem)
|
||
- 组件内边距:8px-16px
|
||
|
||
**组件规范:**
|
||
- 导航栏:高度48px,深色背景
|
||
- 工具栏:浅灰背景,按钮间距8px
|
||
- 文件项:最小高度44px,悬停效果150ms
|
||
- 按钮:圆角4px,过渡150ms
|
||
|
||
**交互规范:**
|
||
- 悬停效果:150ms过渡
|
||
- 触摸目标:最小44x44px
|
||
- 键盘导航:Tab、Enter、Delete、F2、F5、Esc
|
||
- 焦点状态:2px蓝色轮廓
|
||
|
||
**响应式断点:**
|
||
- 移动端:< 768px(双面板垂直排列)
|
||
- 平板:768px - 1024px
|
||
- 桌面:> 1024px(标准布局)
|
||
|
||
---
|
||
|
||
## 12.1 全局异常处理
|
||
|
||
### 12.1.1 自定义异常类
|
||
|
||
```java
|
||
package com.sftp.manager.exception;
|
||
|
||
public class SftpException extends RuntimeException {
|
||
private String errorCode;
|
||
|
||
public SftpException(String message) {
|
||
super(message);
|
||
}
|
||
|
||
public SftpException(String errorCode, String message) {
|
||
super(message);
|
||
this.errorCode = errorCode;
|
||
}
|
||
|
||
public String getErrorCode() {
|
||
return errorCode;
|
||
}
|
||
}
|
||
```
|
||
|
||
```java
|
||
package com.sftp.manager.exception;
|
||
|
||
public class FileOperationException extends RuntimeException {
|
||
private String filePath;
|
||
|
||
public FileOperationException(String message, String filePath) {
|
||
super(message);
|
||
this.filePath = filePath;
|
||
}
|
||
|
||
public String getFilePath() {
|
||
return filePath;
|
||
}
|
||
}
|
||
```
|
||
|
||
```java
|
||
package com.sftp.manager.exception;
|
||
|
||
public class ConnectionException extends RuntimeException {
|
||
private String connectionId;
|
||
|
||
public ConnectionException(String message, String connectionId) {
|
||
super(message);
|
||
this.connectionId = connectionId;
|
||
}
|
||
|
||
public ConnectionException(String message, Throwable cause, String connectionId) {
|
||
super(message, cause);
|
||
this.connectionId = connectionId;
|
||
}
|
||
|
||
public String getConnectionId() {
|
||
return connectionId;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.1.2 全局异常处理器
|
||
|
||
```java
|
||
package com.sftp.manager.exception;
|
||
|
||
import com.sftp.manager.dto.ApiResponse;
|
||
import org.springframework.http.HttpStatus;
|
||
import org.springframework.http.ResponseEntity;
|
||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||
|
||
import java.util.HashMap;
|
||
import java.util.Map;
|
||
|
||
@RestControllerAdvice
|
||
public class GlobalExceptionHandler {
|
||
|
||
@ExceptionHandler(SftpException.class)
|
||
public ResponseEntity<ApiResponse> handleSftpException(SftpException e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("SFTP操作失败");
|
||
response.setError(e.getMessage());
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "SFTP_ERROR");
|
||
errorDetail.put("message", e.getMessage());
|
||
if (e.getErrorCode() != null) {
|
||
errorDetail.put("errorCode", e.getErrorCode());
|
||
}
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||
}
|
||
|
||
@ExceptionHandler(FileOperationException.class)
|
||
public ResponseEntity<ApiResponse> handleFileOperationException(FileOperationException e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("文件操作失败");
|
||
response.setError(e.getMessage());
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "FILE_OPERATION_ERROR");
|
||
errorDetail.put("message", e.getMessage());
|
||
errorDetail.put("filePath", e.getFilePath());
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||
}
|
||
|
||
@ExceptionHandler(ConnectionException.class)
|
||
public ResponseEntity<ApiResponse> handleConnectionException(ConnectionException e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("连接失败");
|
||
response.setError(e.getMessage());
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "CONNECTION_ERROR");
|
||
errorDetail.put("message", e.getMessage());
|
||
if (e.getConnectionId() != null) {
|
||
errorDetail.put("connectionId", e.getConnectionId());
|
||
}
|
||
if (e.getCause() != null) {
|
||
errorDetail.put("cause", e.getCause().getMessage());
|
||
}
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||
}
|
||
|
||
@ExceptionHandler(IllegalArgumentException.class)
|
||
public ResponseEntity<ApiResponse> handleIllegalArgumentException(IllegalArgumentException e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("参数错误");
|
||
response.setError(e.getMessage());
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "INVALID_PARAMETER");
|
||
errorDetail.put("message", e.getMessage());
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
|
||
}
|
||
|
||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||
public ResponseEntity<ApiResponse> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("文件大小超过限制");
|
||
response.setError("上传的文件大小超过最大限制");
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "FILE_TOO_LARGE");
|
||
errorDetail.put("message", "上传的文件大小超过最大限制");
|
||
errorDetail.put("maxSize", e.getMaxUploadSize());
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
|
||
}
|
||
|
||
@ExceptionHandler(Exception.class)
|
||
public ResponseEntity<ApiResponse> handleException(Exception e) {
|
||
ApiResponse<Object> response = new ApiResponse<>();
|
||
response.setSuccess(false);
|
||
response.setMessage("系统错误");
|
||
response.setError("服务器内部错误");
|
||
|
||
Map<String, Object> errorDetail = new HashMap<>();
|
||
errorDetail.put("code", "INTERNAL_SERVER_ERROR");
|
||
errorDetail.put("message", e.getMessage());
|
||
errorDetail.put("type", e.getClass().getSimpleName());
|
||
response.setData(errorDetail);
|
||
|
||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.1.3 错误信息工具类
|
||
|
||
```java
|
||
package com.sftp.manager.util;
|
||
|
||
public class ErrorMessageUtil {
|
||
|
||
public static String getConnectionErrorMessage(Exception e) {
|
||
String message = e.getMessage();
|
||
if (message == null) {
|
||
return "连接失败";
|
||
}
|
||
|
||
if (message.contains("Auth fail")) {
|
||
return "认证失败:用户名或密码错误";
|
||
}
|
||
if (message.contains("timeout")) {
|
||
return "连接超时:服务器响应超时";
|
||
}
|
||
if (message.contains("Connection refused")) {
|
||
return "连接被拒绝:服务器拒绝连接";
|
||
}
|
||
if (message.contains("UnknownHost")) {
|
||
return "主机名无法解析:请检查服务器地址";
|
||
}
|
||
|
||
return "连接失败:" + message;
|
||
}
|
||
|
||
public static String getFileOperationErrorMessage(Exception e) {
|
||
String message = e.getMessage();
|
||
if (message == null) {
|
||
return "文件操作失败";
|
||
}
|
||
|
||
if (message.contains("No such file")) {
|
||
return "文件或目录不存在";
|
||
}
|
||
if (message.contains("Permission denied")) {
|
||
return "权限不足";
|
||
}
|
||
if (message.contains("No space left")) {
|
||
return "磁盘空间不足";
|
||
}
|
||
if (message.contains("Directory not empty")) {
|
||
return "目录不为空";
|
||
}
|
||
|
||
return "文件操作失败:" + message;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 12.2 日志配置
|
||
|
||
### 12.2.1 Logback配置(logback-spring.xml)
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<configuration>
|
||
<!-- 日志文件路径 -->
|
||
<property name="LOG_PATH" value="logs"/>
|
||
<property name="LOG_FILE" value="sftp-manager"/>
|
||
|
||
<!-- 控制台输出 -->
|
||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||
<encoder>
|
||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||
<charset>UTF-8</charset>
|
||
</encoder>
|
||
</appender>
|
||
|
||
<!-- 文件输出 -->
|
||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||
<file>${LOG_PATH}/${LOG_FILE}.log</file>
|
||
<encoder>
|
||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||
<charset>UTF-8</charset>
|
||
</encoder>
|
||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||
<fileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||
<maxHistory>30</maxHistory>
|
||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||
<maxFileSize>100MB</maxFileSize>
|
||
</timeBasedFileNamingAndTriggeringPolicy>
|
||
</rollingPolicy>
|
||
</appender>
|
||
|
||
<!-- 错误日志文件 -->
|
||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
|
||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||
<level>ERROR</level>
|
||
<onMatch>ACCEPT</onMatch>
|
||
<onMismatch>DENY</onMismatch>
|
||
</filter>
|
||
<encoder>
|
||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||
<charset>UTF-8</charset>
|
||
</encoder>
|
||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||
<maxHistory>30</maxHistory>
|
||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||
<maxFileSize>100MB</maxFileSize>
|
||
</timeBasedFileNamingAndTriggeringPolicy>
|
||
</rollingPolicy>
|
||
</appender>
|
||
|
||
<!-- SQL日志 -->
|
||
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
|
||
|
||
<!-- 应用日志级别 -->
|
||
<logger name="com.sftp.manager" level="DEBUG"/>
|
||
|
||
<root level="INFO">
|
||
<appender-ref ref="CONSOLE"/>
|
||
<appender-ref ref="FILE"/>
|
||
<appender-ref ref="ERROR_FILE"/>
|
||
</root>
|
||
</configuration>
|
||
```
|
||
|
||
### 12.2.2 日志工具类
|
||
|
||
```java
|
||
package com.sftp.manager.util;
|
||
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
|
||
public class LogUtil {
|
||
|
||
public static void logOperation(Logger logger, String operation,
|
||
String sessionId, String sourcePath,
|
||
String targetPath, boolean success,
|
||
String error) {
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.append("操作: ").append(operation);
|
||
sb.append(", 会话: ").append(sessionId);
|
||
sb.append(", 源路径: ").append(sourcePath);
|
||
if (targetPath != null) {
|
||
sb.append(", 目标路径: ").append(targetPath);
|
||
}
|
||
sb.append(", 结果: ").append(success ? "成功" : "失败");
|
||
if (error != null) {
|
||
sb.append(", 错误: ").append(error);
|
||
}
|
||
|
||
if (success) {
|
||
logger.info(sb.toString());
|
||
} else {
|
||
logger.error(sb.toString());
|
||
}
|
||
}
|
||
|
||
public static void logConnection(Logger logger, String operation,
|
||
String connectionId, boolean success,
|
||
String error) {
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.append("连接操作: ").append(operation);
|
||
sb.append(", 连接ID: ").append(connectionId);
|
||
sb.append(", 结果: ").append(success ? "成功" : "失败");
|
||
if (error != null) {
|
||
sb.append(", 错误: ").append(error);
|
||
}
|
||
|
||
if (success) {
|
||
logger.info(sb.toString());
|
||
} else {
|
||
logger.error(sb.toString());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 12.3 操作日志记录
|
||
|
||
### 12.3.1 操作日志实体
|
||
|
||
```java
|
||
package com.sftp.manager.model;
|
||
|
||
import lombok.Data;
|
||
import javax.persistence.*;
|
||
import java.time.LocalDateTime;
|
||
|
||
@Data
|
||
@Entity
|
||
@Table(name = "operation_logs")
|
||
public class OperationLog {
|
||
@Id
|
||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||
private Long id;
|
||
|
||
@Column(name = "operation_type", nullable = false)
|
||
private String operationType; // 操作类型:upload, download, delete等
|
||
|
||
@Column(name = "session_id")
|
||
private String sessionId; // 会话ID
|
||
|
||
@Column(name = "source_path", length = 1000)
|
||
private String sourcePath; // 源路径
|
||
|
||
@Column(name = "target_path", length = 1000)
|
||
private String targetPath; // 目标路径
|
||
|
||
@Column(nullable = false)
|
||
private boolean success; // 是否成功
|
||
|
||
@Column(columnDefinition = "TEXT")
|
||
private String errorMessage; // 错误信息
|
||
|
||
@Column(name = "operation_time", nullable = false)
|
||
private LocalDateTime operationTime; // 操作时间
|
||
|
||
@Column(name = "user_ip", length = 50)
|
||
private String userIp; // 用户IP
|
||
|
||
@PrePersist
|
||
protected void onCreate() {
|
||
operationTime = LocalDateTime.now();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.3.2 操作日志Repository
|
||
|
||
```java
|
||
package com.sftp.manager.repository;
|
||
|
||
import com.sftp.manager.model.OperationLog;
|
||
import org.springframework.data.jpa.repository.JpaRepository;
|
||
import org.springframework.data.jpa.repository.Query;
|
||
import org.springframework.data.repository.query.Param;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.List;
|
||
|
||
public interface OperationLogRepository extends JpaRepository<OperationLog, Long> {
|
||
|
||
List<OperationLog> findBySessionIdOrderByOperationTimeDesc(String sessionId);
|
||
|
||
List<OperationLog> findByOperationTypeOrderByOperationTimeDesc(String operationType);
|
||
|
||
@Query("SELECT o FROM OperationLog o WHERE o.operationTime BETWEEN :startTime AND :endTime ORDER BY o.operationTime DESC")
|
||
List<OperationLog> findByOperationTimeBetween(@Param("startTime") LocalDateTime startTime,
|
||
@Param("endTime") LocalDateTime endTime);
|
||
|
||
@Query("SELECT o FROM OperationLog o WHERE o.success = false ORDER BY o.operationTime DESC")
|
||
List<OperationLog> findFailedOperations();
|
||
}
|
||
```
|
||
|
||
### 12.3.3 操作日志服务
|
||
|
||
```java
|
||
package com.sftp.manager.service;
|
||
|
||
import com.sftp.manager.model.OperationLog;
|
||
import com.sftp.manager.repository.OperationLogRepository;
|
||
import com.sftp.manager.util.LogUtil;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.web.context.request.RequestContextHolder;
|
||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||
|
||
import javax.servlet.http.HttpServletRequest;
|
||
import java.time.LocalDateTime;
|
||
import java.util.List;
|
||
|
||
@Service
|
||
public class OperationLogService {
|
||
|
||
private static final Logger logger = LoggerFactory.getLogger(OperationLogService.class);
|
||
|
||
@Autowired
|
||
private OperationLogRepository logRepository;
|
||
|
||
public void logOperation(String operationType, String sessionId,
|
||
String sourcePath, String targetPath,
|
||
boolean success, String errorMessage) {
|
||
OperationLog log = new OperationLog();
|
||
log.setOperationType(operationType);
|
||
log.setSessionId(sessionId);
|
||
log.setSourcePath(sourcePath);
|
||
log.setTargetPath(targetPath);
|
||
log.setSuccess(success);
|
||
log.setErrorMessage(errorMessage);
|
||
|
||
// 获取用户IP
|
||
HttpServletRequest request = getCurrentRequest();
|
||
if (request != null) {
|
||
String ip = getClientIpAddress(request);
|
||
log.setUserIp(ip);
|
||
}
|
||
|
||
try {
|
||
logRepository.save(log);
|
||
|
||
// 记录到日志文件
|
||
LogUtil.logOperation(logger, operationType, sessionId,
|
||
sourcePath, targetPath, success, errorMessage);
|
||
} catch (Exception e) {
|
||
logger.error("保存操作日志失败", e);
|
||
}
|
||
}
|
||
|
||
public List<OperationLog> getOperationLogs(String sessionId) {
|
||
return logRepository.findBySessionIdOrderByOperationTimeDesc(sessionId);
|
||
}
|
||
|
||
public List<OperationLog> getFailedOperations() {
|
||
return logRepository.findFailedOperations();
|
||
}
|
||
|
||
public List<OperationLog> getLogsByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||
return logRepository.findByOperationTimeBetween(startTime, endTime);
|
||
}
|
||
|
||
private HttpServletRequest getCurrentRequest() {
|
||
ServletRequestAttributes attributes =
|
||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||
return attributes != null ? attributes.getRequest() : null;
|
||
}
|
||
|
||
private String getClientIpAddress(HttpServletRequest request) {
|
||
String ip = request.getHeader("X-Forwarded-For");
|
||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||
ip = request.getHeader("Proxy-Client-IP");
|
||
}
|
||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||
}
|
||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||
}
|
||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||
}
|
||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||
ip = request.getRemoteAddr();
|
||
}
|
||
return ip;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.3.4 使用操作日志
|
||
|
||
```java
|
||
@Service
|
||
public class FileService {
|
||
|
||
@Autowired
|
||
private OperationLogService logService;
|
||
|
||
public void deleteFile(String sessionId, String path) {
|
||
try {
|
||
// 执行删除操作
|
||
localFileService.deleteFile(path);
|
||
|
||
// 记录成功日志
|
||
logService.logOperation("delete", sessionId, path, null, true, null);
|
||
} catch (Exception e) {
|
||
// 记录失败日志
|
||
logService.logOperation("delete", sessionId, path, null, false, e.getMessage());
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 12.4 日志查询接口
|
||
|
||
### 12.4.1 日志查询Controller
|
||
|
||
```java
|
||
package com.sftp.manager.controller;
|
||
|
||
import com.sftp.manager.dto.ApiResponse;
|
||
import com.sftp.manager.model.OperationLog;
|
||
import com.sftp.manager.service.OperationLogService;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.format.annotation.DateTimeFormat;
|
||
import org.springframework.web.bind.annotation.*;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.List;
|
||
|
||
@RestController
|
||
@RequestMapping("/api/logs")
|
||
@CrossOrigin(origins = "*")
|
||
public class LogController {
|
||
|
||
@Autowired
|
||
private OperationLogService logService;
|
||
|
||
@GetMapping("/session/{sessionId}")
|
||
public ApiResponse<List<OperationLog>> getLogsBySession(@PathVariable String sessionId) {
|
||
try {
|
||
List<OperationLog> logs = logService.getOperationLogs(sessionId);
|
||
return ApiResponse.success("查询成功", logs);
|
||
} catch (Exception e) {
|
||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@GetMapping("/failed")
|
||
public ApiResponse<List<OperationLog>> getFailedLogs() {
|
||
try {
|
||
List<OperationLog> logs = logService.getFailedOperations();
|
||
return ApiResponse.success("查询成功", logs);
|
||
} catch (Exception e) {
|
||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@GetMapping("/range")
|
||
public ApiResponse<List<OperationLog>> getLogsByTimeRange(
|
||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
|
||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
|
||
try {
|
||
List<OperationLog> logs = logService.getLogsByTimeRange(startTime, endTime);
|
||
return ApiResponse.success("查询成功", logs);
|
||
} catch (Exception e) {
|
||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 12.5 前端错误处理与展示
|
||
|
||
### 12.5.1 统一 API 错误处理(Axios 拦截器)
|
||
|
||
```javascript
|
||
// src/api/request.js 或 src/utils/request.js
|
||
import axios from 'axios';
|
||
|
||
const request = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 30000
|
||
});
|
||
|
||
// 响应拦截器:统一解析后端 ApiResponse 与错误
|
||
request.interceptors.response.use(
|
||
(response) => {
|
||
const res = response.data;
|
||
if (res.success === false) {
|
||
const message = res.message || res.error || '请求失败';
|
||
return Promise.reject(new Error(message));
|
||
}
|
||
return res;
|
||
},
|
||
(error) => {
|
||
const res = error.response?.data;
|
||
let message = '网络异常,请稍后重试';
|
||
if (res?.error) {
|
||
message = res.error;
|
||
} else if (res?.message) {
|
||
message = res.message;
|
||
} else if (error.message) {
|
||
message = error.message;
|
||
}
|
||
return Promise.reject(new Error(message));
|
||
}
|
||
);
|
||
|
||
export default request;
|
||
```
|
||
|
||
### 12.5.2 错误码与用户提示映射
|
||
|
||
```javascript
|
||
// src/utils/errorMessages.js
|
||
const ERROR_MESSAGES = {
|
||
SFTP_ERROR: 'SFTP 操作失败,请检查连接或路径',
|
||
FILE_OPERATION_ERROR: '文件操作失败',
|
||
CONNECTION_ERROR: '连接失败,请检查主机、端口和认证信息',
|
||
INVALID_PARAMETER: '参数错误,请检查输入',
|
||
FILE_TOO_LARGE: '文件大小超过限制',
|
||
INTERNAL_SERVER_ERROR: '服务器内部错误,请稍后重试'
|
||
};
|
||
|
||
export function getErrorMessage(code, fallback) {
|
||
return ERROR_MESSAGES[code] || fallback || '操作失败';
|
||
}
|
||
```
|
||
|
||
### 12.5.3 全局错误提示组件(Toast/Message)
|
||
|
||
- 所有接口调用在 `catch` 中统一使用项目已有的 Toast/Message 展示 `error.message`。
|
||
- 可根据 `response.data?.data?.code` 调用 `getErrorMessage(code, error.message)` 得到更友好的文案后再展示。
|
||
|
||
### 12.5.4 操作日志列表页(可选)
|
||
|
||
- 调用 `/api/logs/session/{sessionId}`、`/api/logs/failed`、`/api/logs/range` 展示操作日志。
|
||
- 表格列:操作类型、会话、源路径、目标路径、结果、错误信息、操作时间、IP。
|
||
- 支持按时间范围、会话、是否失败筛选。
|
||
|
||
## 12.6 异步操作日志(可选)
|
||
|
||
为减少对主流程性能影响,可将操作日志改为异步写入。
|
||
|
||
### 12.6.1 使用 @Async 异步记录
|
||
|
||
```java
|
||
// OperationLogService.java 中
|
||
import org.springframework.scheduling.annotation.Async;
|
||
|
||
@Async
|
||
public void logOperationAsync(String operationType, String sessionId,
|
||
String sourcePath, String targetPath,
|
||
boolean success, String errorMessage) {
|
||
logOperation(operationType, sessionId, sourcePath, targetPath, success, errorMessage);
|
||
}
|
||
```
|
||
|
||
启用异步需在启动类或配置类上添加:
|
||
|
||
```java
|
||
@SpringBootApplication
|
||
@EnableAsync
|
||
public class SftpManagerApplication {
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(SftpManagerApplication.class, args);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.6.2 异步配置(线程池)
|
||
|
||
```java
|
||
package com.sftp.manager.config;
|
||
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Configuration;
|
||
import org.springframework.scheduling.annotation.EnableAsync;
|
||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||
|
||
import java.util.concurrent.Executor;
|
||
|
||
@Configuration
|
||
@EnableAsync
|
||
public class AsyncConfig {
|
||
|
||
@Bean(name = "logTaskExecutor")
|
||
public Executor logTaskExecutor() {
|
||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||
executor.setCorePoolSize(2);
|
||
executor.setMaxPoolSize(5);
|
||
executor.setQueueCapacity(500);
|
||
executor.setThreadNamePrefix("log-async-");
|
||
executor.initialize();
|
||
return executor;
|
||
}
|
||
}
|
||
```
|
||
|
||
```java
|
||
@Async("logTaskExecutor")
|
||
public void logOperationAsync(...) { ... }
|
||
```
|
||
|
||
## 实施步骤
|
||
|
||
1. **创建异常类**:SftpException、FileOperationException、ConnectionException
|
||
2. **创建全局异常处理器**:GlobalExceptionHandler,并修正 `handleException` 中 `setError` 的调用
|
||
3. **创建工具类**:ErrorMessageUtil、LogUtil
|
||
4. **创建日志配置**:logback-spring.xml(控制台、文件、错误文件、轮转策略)
|
||
5. **创建操作日志**:OperationLog 实体、OperationLogRepository、OperationLogService(含 getLogsByTimeRange)
|
||
6. **在业务服务中集成**:在 FileService 等中调用 logService.logOperation
|
||
7. **创建日志查询接口**:LogController(/session/{sessionId}、/failed、/range)
|
||
8. **(可选)前端**:Axios 拦截器、错误码映射、Toast 展示、操作日志列表页
|
||
9. **(可选)异步日志**:@EnableAsync、logTaskExecutor、logOperationAsync
|
||
|
||
## 注意事项
|
||
|
||
1. **日志级别**:生产环境建议 root 为 INFO,`com.sftp.manager` 可为 DEBUG
|
||
2. **敏感信息**:避免在日志中记录密码、密钥、完整 token
|
||
3. **性能影响**:高并发场景使用异步记录操作日志(12.6)
|
||
4. **日志轮转**:已配置按天+大小轮转,maxHistory 可按需调整
|
||
5. **错误追踪**:保留 exception 的 message 与 cause,便于排查
|
||
6. **前端体验**:统一用 ApiResponse 结构,前端根据 code 做友好提示
|
||
|
||
## 下一步
|
||
|
||
完成模块12后,继续模块13:部署与测试
|