操作记录表
sql
CREATE TABLE `operation_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`check_id` bigint(20) NOT NULL COMMENT '检测id',
`status` varchar(64) DEFAULT NULL COMMENT '检测old状态-检测new状态',
`operation` varchar(255) NOT NULL COMMENT '操作事件',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`created_by_id` bigint(20) DEFAULT NULL COMMENT '创建者id',
`created_by_name` varchar(255) DEFAULT NULL COMMENT '创建者名称',
`old_data` json DEFAULT NULL COMMENT '存储操作前的旧数据,JSON格式',
`remark` varchar(255) DEFAULT NULL COMMENT '备注信息,用于记录额外的操作说明',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='操作记录';
操作记录实体类
java
package com.applets.manager.core.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 操作记录实体类
* </p>
*
* @author MybatisGenerator
* @since 2024-05-15
*/
@Data
@Builder
@TableName("check_status_record")
public class CheckStatusRecord implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 自增id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 检测id(业务主键,可以自行修改)
*/
@TableField("check_id")
private Long checkId;
/**
* 检测状态
*/
@TableField("status")
private String status;
/**
* 操作事件
*/
@TableField("operation")
private String operation;
/**
* 是否删除
*/
@TableField("deleted")
private Boolean deleted;
/**
* 创建时间
*/
@TableField("created_time")
private LocalDateTime createdTime;
/**
* 创建者id
*/
@TableField("created_by_id")
private Long createdById;
/**
* 创建者名称
*/
@TableField("created_by_name")
private String createdByName;
/**
* 老数据
*/
@TableField("old_data")
private String oldData;
/**
* 备注
*/
@TableField("remark")
private String remark;
}
操作记录表数据存储实例
执行业务,存入操作记录并且存入快照数据
此处的我存在oldData的字段类型为map,这样oldData可以一次性存入多个修改记录,就比如说如下方法enterEvaluationPrice修改了ylcheck和saleConditions
- key为YlCheck.class.getName() + "-" + "ylCheckMapper"+"-update"即(存入的对象全类名-容器中操作对应类的mapper名称-数据库操作类型)
- value 为历史快照数据
- 主要使用方法就是在做数据库操作之前,把要操作的历史数据查询出来,json的方法序列化,存入操作记录表中的oldData的字段,因为我这里是操作了多个表,所有我用map的方式存储多个
java
@Override
@Transactional(rollbackFor = Exception.class)
public Result enterEvaluationPrice(EnterPriceReqVo req) {
Integer deliveryDays = req.getDeliveryDays();
Long id = req.getId();
BigDecimal acquisitionPrice = req.getAcquisitionPrice();
BigDecimal multiply = acquisitionPrice.multiply(new BigDecimal(10000));
try {
HashMap<String, Object> oldDataMap = new HashMap<>();
YlCheck ylCheck = ylCheckMapper.selectById(id);
if (ylCheck == null) {
return Result.failure(CommonResultStatus.VEHICLE_REPORT_NOT_FIND);
}
if (ObjectUtils.isNotEmpty(deliveryDays)) {
LambdaUpdateWrapper<SaleConditions> saleConditionsLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
LambdaUpdateWrapper<SaleConditions> qw = saleConditionsLambdaUpdateWrapper
.eq(SaleConditions::getCarId, ylCheck.getCheckCarId());
saleConditionsLambdaUpdateWrapper.set(SaleConditions::getDeliveryDays, deliveryDays);
List<SaleConditions> saleConditions = saleConditionsMapper.selectList(qw);
oldDataMap.put(SaleConditions.class.getName() + "-" + "saleConditionsMapper"+"-update", saleConditions); //存入历史数据
saleConditionsMapper.update(null, saleConditionsLambdaUpdateWrapper);
}
oldDataMap.put(YlCheck.class.getName() + "-" + "ylCheckMapper"+"-update", ylCheck);//存入历史数据
//其他业务代码
ylCheckMapper.update(null, new LambdaUpdateWrapper<YlCheck>()
.eq(YlCheck::getId, ylCheck.getId())
.set(YlCheck::getStatus, to.name())//变更后的状态
);
// 存入操作记录
CheckStatusRecord checkStatusRecord = CheckStatusRecord.builder()
.checkId(ylCheck.getId())
.status(from.name() + "-" + to.name())
.operation(event.name())
.createdByName(localUserInfo.getUserName())
.createdTime(LocalDateTime.now())
.deleted(false)
.oldData(JSON.toJSONString(oldDataMap))
.build();
checkStatusRecordMapper.insert(checkStatusRecord);
//其他业务代码
}
评估回滚请求对象
- 此处就传了一个checkId即我这里的业务主键,可以自定义修改
java
package com.applets.manager.core.model.vo.req;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
@ApiModel("评估回滚请求对象")
public class YlCheckRollbackReqVo extends BaseReqVo implements Serializable {
private static final long serialVersionUID = 718144265818263634L;
/**
* 检测id
*/
@ApiModelProperty("检测id")
private Long checkId;
}
根据操作记录回滚业务数据
rollback业务方法
- checkId 为某业务主键,可以自行改动
- 通过checkid查询出对应的操作记录(我这里是查询最新的,读者可以自定义查询条件)
- 查到操作记录后将historyRecord.getOldData() JSON 字符串反序列化为 Map
- 遍历 Map,反射获取类和 Mapper
- 通过反射获取类,提供给反序列化时使用
- 进行反序列化
- 检查数据是否是数组类型还是单个对象(两种反序列化的方式不同)
- 根据 type 判断操作类型并进行相应的回滚(insert,update,delete)
- 如果是逻辑删除的化就单独使用update回滚即可,把对应的删除标志修改一下即可
- 如果不是逻辑删除的化insert回滚就需要反向执行删除操作;delete回滚就需要反向执行插入操作.
- 内部也需要检查数据是否是数组类型还是单个对象(baseMapper处理数组类型和单个对象的方法不同)
java
@Override
@Transactional(rollbackFor = Exception.class)
public void rollback(YlCheckRollbackReqVo req) {
Long checkId = req.getCheckId();
if (ObjectUtils.isEmpty(checkId)){
throw new BusinessException("参数错误");
}
// 查询保存的历史数据最新的一条 (JSON 字符串)
LambdaQueryWrapper<CheckStatusRecord> qw = new LambdaQueryWrapper<>();
qw.eq(CheckStatusRecord::getCheckId, checkId)
.orderByDesc(CheckStatusRecord::getCreatedTime)
.last("LIMIT 1"); // 只查询一条
CheckStatusRecord historyRecord = checkStatusRecordMapper.selectOne(qw);
if (historyRecord == null || historyRecord.getOldData() == null) {
throw new BusinessException("没有找到可以回滚的历史数据");
}
// 1. 将 JSON 字符串反序列化为 Map
Map<String, Object> oldDataMap = JSON.parseObject(historyRecord.getOldData(), Map.class);
// 2. 遍历 Map,反射获取类和 Mapper
for (Map.Entry<String, Object> entry : oldDataMap.entrySet()) {
// 解析 key,获取类名和 Mapper 名,和操作类型
String[] keyParts = entry.getKey().split("-");
String className = keyParts[0];
String mapperName = keyParts[1];
String type = keyParts[2]; // 获取操作类型
try {
// 通过反射获取类
Class<?> clazz = Class.forName(className);
Object oldData;
// 3. 检查数据是否是数组类型
if (entry.getValue() instanceof JSONArray) {
// 如果是数组类型,使用 parseArray 进行反序列化
oldData = JSON.parseArray(JSON.toJSONString(entry.getValue()), clazz);
} else {
// 如果是单个对象,使用 parseObject 进行反序列化
oldData = JSON.parseObject(JSON.toJSONString(entry.getValue()), clazz);
}
// 通过 Spring 应用上下文获取 Mapper 实例
Object mapper = applicationContext.getBean(mapperName);
// 4. 根据 type 判断操作类型并进行相应的回滚
if ("insert".equals(type)) {
// 如果是新增操作,回滚时执行删除操作
if (oldData instanceof List) {
for (Object item : (List<?>) oldData) {
if (mapper instanceof BaseMapper) {
String jsonItem = JSON.toJSONString(item);
JSONObject jsonObjectItem = JSON.parseObject(jsonItem);
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.deleteById(jsonObjectItem.getLong("id")); // 删除每个元素
}
}
} else {
if (mapper instanceof BaseMapper) {
String jsonOldData = JSON.toJSONString(oldData);
JSONObject jsonObjectOldData = JSON.parseObject(jsonOldData);
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.deleteById(jsonObjectOldData.getLong("id")); // 删除单个对象
}
}
} else if ("update".equals(type)) {
// 如果是更新操作,回滚时恢复原始数据
if (oldData instanceof List) {
for (Object item : (List<?>) oldData) {
if (mapper instanceof BaseMapper) {
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.updateById(item); // 恢复每个元素
}
}
} else {
if (mapper instanceof BaseMapper) {
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.updateById(oldData); // 恢复单个对象
}
}
} else if ("delete".equals(type)) {
// 如果是删除操作,回滚时重新插入数据
if (oldData instanceof List) {
for (Object item : (List<?>) oldData) {
if (mapper instanceof BaseMapper) {
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.insert(item); // 插入每个元素
}
}
} else {
if (mapper instanceof BaseMapper) {
BaseMapper baseMapper = (BaseMapper) mapper;
baseMapper.insert(oldData); // 插入单个对象
}
}
}
} catch (ClassNotFoundException e) {
log.error("回滚操作失败: {}", e);
throw new BusinessException("回滚操作失败: " + className);
}
}
}