通用模块数据操作记录表设计
下面我设计一个通用的操作记录系统,可以记录各个模块的数据变更历史,包括新增、修改和删除操作的前后值。
数据库表设计
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("操作日志归档完成");
}
}
系统架构图
设计优势
-
通用性:适用于任何业务模块的操作记录
-
灵活性:支持JSON格式存储前后数据,适应不同数据结构
-
高性能:通过异步处理确保不影响主业务流程
-
可扩展性:支持多租户、敏感数据脱敏等高级特性
-
审计友好:完整记录操作历史,满足合规要求
-
查询优化:通过索引支持高效的历史记录查询
这个设计可以满足企业级应用中对于数据操作记录的通用需求,提供了完整的操作历史追踪能力,同时保持了系统的性能和可维护性。