SpringBoot 通用模块 数据操作记录功能

通用模块数据操作记录表设计

        下面我设计一个通用的操作记录系统,可以记录各个模块的数据变更历史,包括新增、修改和删除操作的前后值。

数据库表设计

CREATE TABLE operation_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    module VARCHAR(50) NOT NULL COMMENT '业务模块',
    operation_type ENUM('CREATE', 'UPDATE', 'DELETE') NOT NULL COMMENT '操作类型',
    target_id VARCHAR(64) NOT NULL COMMENT '操作目标ID',
    target_type VARCHAR(100) NOT NULL COMMENT '操作目标类型',
    before_data JSON COMMENT '操作前数据(JSON格式)',
    after_data JSON COMMENT '操作后数据(JSON格式)',
    changed_fields VARCHAR(500) COMMENT '变更字段列表(逗号分隔)',
    operator_id VARCHAR(50) NOT NULL COMMENT '操作人ID',
    operator_name VARCHAR(100) NOT NULL COMMENT '操作人姓名',
    operation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
    ip_address VARCHAR(50) COMMENT '操作IP地址',
    tenant_id VARCHAR(50) COMMENT '租户ID(多租户系统)',
    additional_info JSON COMMENT '附加信息(JSON格式)'
) COMMENT '通用操作记录表';

CREATE INDEX idx_module ON operation_log(module);
CREATE INDEX idx_target ON operation_log(target_type, target_id);
CREATE INDEX idx_operation_time ON operation_log(operation_time);
CREATE INDEX idx_operator ON operation_log(operator_id);

核心实体类

@Entity
@Table(name = "operation_log")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperationLog {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String module;
    
    @Column(name = "operation_type", nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private OperationType operationType;
    
    @Column(name = "target_id", nullable = false, length = 64)
    private String targetId;
    
    @Column(name = "target_type", nullable = false, length = 100)
    private String targetType;
    
    @Column(name = "before_data", columnDefinition = "JSON")
    @Convert(converter = JsonConverter.class)
    private Object beforeData;
    
    @Column(name = "after_data", columnDefinition = "JSON")
    @Convert(converter = JsonConverter.class)
    private Object afterData;
    
    @Column(name = "changed_fields", length = 500)
    private String changedFields;
    
    @Column(name = "operator_id", nullable = false, length = 50)
    private String operatorId;
    
    @Column(name = "operator_name", nullable = false, length = 100)
    private String operatorName;
    
    @Column(name = "operation_time", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date operationTime;
    
    @Column(name = "ip_address", length = 50)
    private String ipAddress;
    
    @Column(name = "tenant_id", length = 50)
    private String tenantId;
    
    @Column(name = "additional_info", columnDefinition = "JSON")
    @Convert(converter = JsonConverter.class)
    private Map<String, Object> additionalInfo;
    
    public enum OperationType {
        CREATE, UPDATE, DELETE
    }
}

// JSON转换器
@Converter
public class JsonConverter implements AttributeConverter<Object, String> {
    
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public String convertToDatabaseColumn(Object attribute) {
        try {
            return objectMapper.writeValueAsString(attribute);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("JSON转换失败", e);
        }
    }
    
    @Override
    public Object convertToEntityAttribute(String dbData) {
        try {
            if (dbData == null || dbData.isEmpty()) return null;
            return objectMapper.readValue(dbData, Object.class);
        } catch (IOException e) {
            throw new IllegalArgumentException("JSON解析失败", e);
        }
    }
}

通用操作记录服务

public interface OperationLogService {
    
    /**
     * 记录操作日志
     * 
     * @param module 业务模块
     * @param operationType 操作类型
     * @param targetId 目标ID
     * @param targetType 目标类型
     * @param beforeData 操作前数据
     * @param afterData 操作后数据
     * @param operatorId 操作人ID
     * @param operatorName 操作人姓名
     */
    void logOperation(String module, OperationType operationType, 
                     String targetId, String targetType,
                     Object beforeData, Object afterData,
                     String operatorId, String operatorName);
    
    /**
     * 获取操作历史
     * 
     * @param targetType 目标类型
     * @param targetId 目标ID
     * @return 操作历史列表
     */
    List<OperationLog> getOperationHistory(String targetType, String targetId);
    
    /**
     * 获取操作历史(分页)
     * 
     * @param query 查询条件
     * @param pageable 分页信息
     * @return 分页操作历史
     */
    Page<OperationLog> searchOperationHistory(OperationLogQuery query, Pageable pageable);
}

@Service
@Slf4j
public class OperationLogServiceImpl implements OperationLogService {
    
    private final OperationLogRepository operationLogRepository;
    private final ObjectMapper objectMapper;
    
    @Autowired
    public OperationLogServiceImpl(OperationLogRepository operationLogRepository, 
                                  ObjectMapper objectMapper) {
        this.operationLogRepository = operationLogRepository;
        this.objectMapper = objectMapper;
    }
    
    @Async("operationLogExecutor")
    @Override
    public void logOperation(String module, OperationType operationType, 
                           String targetId, String targetType,
                           Object beforeData, Object afterData,
                           String operatorId, String operatorName) {
        
        try {
            // 1. 序列化前后数据
            Object serializedBefore = serializeData(beforeData);
            Object serializedAfter = serializeData(afterData);
            
            // 2. 识别变更字段
            Set<String> changedFields = detectChangedFields(serializedBefore, serializedAfter);
            
            // 3. 构建操作日志
            OperationLog log = new OperationLog();
            log.setModule(module);
            log.setOperationType(operationType);
            log.setTargetId(targetId);
            log.setTargetType(targetType);
            log.setBeforeData(serializedBefore);
            log.setAfterData(serializedAfter);
            log.setChangedFields(String.join(",", changedFields));
            log.setOperatorId(operatorId);
            log.setOperatorName(operatorName);
            log.setOperationTime(new Date());
            log.setIpAddress(RequestContextUtils.getClientIp());
            
            // 4. 保存日志
            operationLogRepository.save(log);
            
        } catch (Exception e) {
            log.error("记录操作日志失败: {}-{}", module, targetId, e);
            // 这里可以添加降级处理逻辑
        }
    }
    
    private Object serializeData(Object data) {
        if (data == null) return null;
        
        try {
            // 如果是基本类型直接返回
            if (data instanceof String || 
                data instanceof Number || 
                data instanceof Boolean) {
                return data;
            }
            
            // 转换为Map格式
            return objectMapper.convertValue(data, new TypeReference<Map<String, Object>>() {});
        } catch (IllegalArgumentException e) {
            log.warn("序列化操作数据失败,使用toString表示", e);
            return data.toString();
        }
    }
    
    private Set<String> detectChangedFields(Object before, Object after) {
        if (before == null || after == null) {
            return Collections.emptySet();
        }
        
        if (!(before instanceof Map) || !(after instanceof Map)) {
            return Collections.emptySet();
        }
        
        Map<String, Object> beforeMap = (Map<String, Object>) before;
        Map<String, Object> afterMap = (Map<String, Object>) after;
        
        Set<String> changedFields = new HashSet<>();
        
        // 检查所有字段的变化
        for (String key : afterMap.keySet()) {
            Object beforeValue = beforeMap.get(key);
            Object afterValue = afterMap.get(key);
            
            if (!Objects.equals(beforeValue, afterValue)) {
                changedFields.add(key);
            }
        }
        
        return changedFields;
    }
    
    @Override
    public List<OperationLog> getOperationHistory(String targetType, String targetId) {
        return operationLogRepository.findByTargetTypeAndTargetIdOrderByOperationTimeDesc(targetType, targetId);
    }
    
    @Override
    public Page<OperationLog> searchOperationHistory(OperationLogQuery query, Pageable pageable) {
        return operationLogRepository.findAll(
            OperationLogSpecifications.withQuery(query), 
            pageable
        );
    }
}

// 查询条件封装
@Data
public class OperationLogQuery {
    private String module;
    private OperationType operationType;
    private String targetType;
    private String targetId;
    private String operatorId;
    private String operatorName;
    private Date startTime;
    private Date endTime;
}

// 查询规格
public class OperationLogSpecifications {
    
    public static Specification<OperationLog> withQuery(OperationLogQuery query) {
        return (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            if (StringUtils.hasText(query.getModule())) {
                predicates.add(criteriaBuilder.equal(root.get("module"), query.getModule()));
            }
            
            if (query.getOperationType() != null) {
                predicates.add(criteriaBuilder.equal(root.get("operationType"), query.getOperationType()));
            }
            
            if (StringUtils.hasText(query.getTargetType())) {
                predicates.add(criteriaBuilder.equal(root.get("targetType"), query.getTargetType()));
            }
            
            if (StringUtils.hasText(query.getTargetId())) {
                predicates.add(criteriaBuilder.equal(root.get("targetId"), query.getTargetId()));
            }
            
            if (StringUtils.hasText(query.getOperatorId())) {
                predicates.add(criteriaBuilder.equal(root.get("operatorId"), query.getOperatorId()));
            }
            
            if (StringUtils.hasText(query.getOperatorName())) {
                predicates.add(criteriaBuilder.like(root.get("operatorName"), "%" + query.getOperatorName() + "%"));
            }
            
            if (query.getStartTime() != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("operationTime"), query.getStartTime()));
            }
            
            if (query.getEndTime() != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("operationTime"), query.getEndTime()));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

在业务模块中使用

用户服务示例

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    private final OperationLogService operationLogService;
    
    @Transactional
    public User createUser(UserDTO userDTO, String operatorId, String operatorName) {
        User user = new User();
        BeanUtils.copyProperties(userDTO, user);
        
        // 保存前无数据
        User savedUser = userRepository.save(user);
        
        // 记录操作日志 - 新增操作
        operationLogService.logOperation(
            "UserManagement", 
            OperationType.CREATE, 
            savedUser.getId().toString(),
            "User",
            null, // beforeData
            savedUser, // afterData
            operatorId,
            operatorName
        );
        
        return savedUser;
    }
    
    @Transactional
    public User updateUser(Long userId, UserDTO userDTO, String operatorId, String operatorName) {
        User existingUser = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
        
        // 保存修改前的数据
        User beforeUpdate = new User();
        BeanUtils.copyProperties(existingUser, beforeUpdate);
        
        // 更新用户信息
        BeanUtils.copyProperties(userDTO, existingUser, "id");
        User updatedUser = userRepository.save(existingUser);
        
        // 记录操作日志 - 更新操作
        operationLogService.logOperation(
            "UserManagement", 
            OperationType.UPDATE, 
            userId.toString(),
            "User",
            beforeUpdate, // beforeData
            updatedUser, // afterData
            operatorId,
            operatorName
        );
        
        return updatedUser;
    }
    
    @Transactional
    public void deleteUser(Long userId, String operatorId, String operatorName) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
        
        // 记录操作日志 - 删除操作
        operationLogService.logOperation(
            "UserManagement", 
            OperationType.DELETE, 
            userId.toString(),
            "User",
            user, // beforeData
            null, // afterData
            operatorId,
            operatorName
        );
        
        userRepository.delete(user);
    }
}

订单服务示例

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final OperationLogService operationLogService;
    
    @Transactional
    public Order updateOrderStatus(Long orderId, OrderStatus newStatus, 
                                  String operatorId, String operatorName) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new EntityNotFoundException("订单不存在"));
        
        // 保存修改前的数据
        Order beforeUpdate = new Order();
        BeanUtils.copyProperties(order, beforeUpdate);
        
        // 更新订单状态
        order.setStatus(newStatus);
        Order updatedOrder = orderRepository.save(order);
        
        // 记录操作日志
        operationLogService.logOperation(
            "OrderManagement", 
            OperationType.UPDATE, 
            orderId.toString(),
            "Order",
            beforeUpdate, // beforeData
            updatedOrder, // afterData
            operatorId,
            operatorName
        );
        
        return updatedOrder;
    }
}

操作日志查询接口

@RestController
@RequestMapping("/api/operation-logs")
public class OperationLogController {
    
    private final OperationLogService operationLogService;
    
    @GetMapping("/by-target")
    public ResponseEntity<List<OperationLog>> getLogsByTarget(
            @RequestParam String targetType,
            @RequestParam String targetId) {
        List<OperationLog> logs = operationLogService.getOperationHistory(targetType, targetId);
        return ResponseEntity.ok(logs);
    }
    
    @GetMapping("/search")
    public ResponseEntity<Page<OperationLog>> searchLogs(
            @ModelAttribute OperationLogQuery query,
            Pageable pageable) {
        Page<OperationLog> result = operationLogService.searchOperationHistory(query, pageable);
        return ResponseEntity.ok(result);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<OperationLog> getLogDetail(@PathVariable Long id) {
        return operationLogService.findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
    }
}

高级特性增强

1. 敏感数据脱敏

public class DataMaskingUtils {
    
    private static final Set<String> SENSITIVE_FIELDS = Set.of(
        "password", "idCard", "phone", "email", "bankCard"
    );
    
    public static Map<String, Object> maskSensitiveData(Map<String, Object> data) {
        if (data == null) return null;
        
        Map<String, Object> maskedData = new HashMap<>(data);
        
        SENSITIVE_FIELDS.forEach(field -> {
            if (maskedData.containsKey(field)) {
                maskedData.put(field, "******");
            }
        });
        
        return maskedData;
    }
}

// 在OperationLogService中应用
private Object serializeData(Object data) {
    // ... 其他代码
    
    if (data instanceof Map) {
        return DataMaskingUtils.maskSensitiveData((Map<String, Object>) data);
    }
    
    // ... 其他代码
}

2. 操作日志审计事件

// 定义操作日志事件
public class OperationLogEvent extends ApplicationEvent {
    
    private final OperationLog operationLog;
    
    public OperationLogEvent(Object source, OperationLog operationLog) {
        super(source);
        this.operationLog = operationLog;
    }
    
    public OperationLog getOperationLog() {
        return operationLog;
    }
}

// 在OperationLogService中发布事件
@Async("operationLogExecutor")
@Override
public void logOperation(...) {
    // ... 记录日志
    
    // 发布事件
    applicationEventPublisher.publishEvent(new OperationLogEvent(this, log));
}

// 审计服务监听事件
@Component
@Slf4j
public class AuditEventListener {
    
    @EventListener
    public void handleOperationLogEvent(OperationLogEvent event) {
        OperationLog log = event.getOperationLog();
        
        // 关键操作实时通知
        if (isCriticalOperation(log)) {
            notificationService.sendCriticalOperationAlert(log);
        }
        
        // 审计日志处理
        auditService.recordOperation(log);
    }
    
    private boolean isCriticalOperation(OperationLog log) {
        return "DELETE".equals(log.getOperationType()) || 
               "UserManagement".equals(log.getModule()) && 
               "UPDATE".equals(log.getOperationType());
    }
}

3. 操作日志归档策略

@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
@Transactional
public void archiveOldLogs() {
    LocalDateTime archiveTime = LocalDateTime.now().minusMonths(3);
    Date archiveDate = Date.from(archiveTime.atZone(ZoneId.systemDefault()).toInstant());
    
    List<OperationLog> oldLogs = operationLogRepository
        .findByOperationTimeBefore(archiveDate);
    
    if (!oldLogs.isEmpty()) {
        log.info("开始归档 {} 条操作日志", oldLogs.size());
        
        // 归档到历史表
        operationLogArchiveRepository.saveAll(oldLogs);
        
        // 删除主表数据
        operationLogRepository.deleteAll(oldLogs);
        
        log.info("操作日志归档完成");
    }
}

系统架构图

设计优势

  1. 通用性:适用于任何业务模块的操作记录

  2. 灵活性:支持JSON格式存储前后数据,适应不同数据结构

  3. 高性能:通过异步处理确保不影响主业务流程

  4. 可扩展性:支持多租户、敏感数据脱敏等高级特性

  5. 审计友好:完整记录操作历史,满足合规要求

  6. 查询优化:通过索引支持高效的历史记录查询

        这个设计可以满足企业级应用中对于数据操作记录的通用需求,提供了完整的操作历史追踪能力,同时保持了系统的性能和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值