831
docs/12-错误处理与日志.md
Normal file
831
docs/12-错误处理与日志.md
Normal file
@@ -0,0 +1,831 @@
|
||||
# 模块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:部署与测试
|
||||
Reference in New Issue
Block a user