每一年都奔走在自己热爱里

没有人是一座孤岛,总有谁爱着你

springboot数据修改历史记录

在一些领域,记录数据的变更历史是非常重要的。比如工业采集系统…
需要记录指标的信息。再比如一些非常注重安全的系统,希望在必要时可以对所有的历史操作追根溯源,有据可查。

0.前言

比如,修改一个人的姓名从“张三”变为了“李四”,那么在进行记录的时候,记录的信息可能如下:

这样就很好的体现出了修改了哪个字段,修改前后的数据分别是什么。
关键的信息无论怎么修改都会有据可查,时间、人物、修改数据前后信息等。

设计思路:

1、获取到两个对象中属性列表,
2、遍历对比,
3、属性名相同属性值不同的把属性名及两个对象的属性值保存进Map<String,Object>里,
4、返回List<Map<String,Object>对象

1.新建FieldMeta 类:

底层就是利用的java反射机制,通过自定义注解实现。

新增自定义注解FieldMeta :

/**
 * @description:  底层就是利用的java反射机制,通过自定义注解实现
 * @author: 黑猫
 * @date: 2023/4/17 10:59
 * @version:1.0
 */
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
@Documented
public @interface FieldMeta {
    String name() default ""; 
    String description() default "";
}

在需要记录修改历史的字段上添加@FieldMeta注解,标识它,只要变化,就会记录它的信息。

2.新建CompareObjectUtils类:
用于 对比两个对象中同名属性的值是否相同。

源码:

package com.bonc.boot.module.web.util;

/**
 * @description:
 * @author: 黑猫
 * @date: 2023/4/17 10:59
 * @version:1.0
 */

import com.bonc.boot.module.web.annotation.FieldMeta;
import org.apache.http.client.utils.DateUtils;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.util.*;

@Component
public class CompareObjectUtils {
    private static CompareObjectUtils compareObjectUtils;

    @PostConstruct
    public void init() {
        compareObjectUtils = this;
    }
    /**
     * 获取两个对象同名属性内容不相同的列表
     * @param class1 对象1
     * @param class2 对象2
     * @return
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     */
    public static List<Map<String, Object>> compareTwoClass(Object class1, Object class2) throws ClassNotFoundException, IllegalAccessException {
        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
        //获取对象的class
        Class<?> clazz1 = class1.getClass();
        Class<?> clazz2 = class2.getClass();
        //获取对象的属性列表
        Field[] field1 = clazz1.getDeclaredFields();
        Field[] field2 = clazz2.getDeclaredFields();
        //遍历属性列表field1
        for (int i = 0; i < field1.length; i++) {
            if(field1[i].isAnnotationPresent(FieldMeta.class))
                //遍历属性列表field2
                for (int j = 0; j < field2.length; j++) {
                    //如果field1[i]属性名与field2[j]属性名内容相同
                    if (field1[i].getName().equals(field2[j].getName())) {
                        field1[i].setAccessible(true);
                        field2[j].setAccessible(true);
                        //如果field1[i]属性值与field2[j]属性值内容不相同
                        if (!compareTwo(field1[i].get(class1), field2[j].get(class2)) && field1[i].isAnnotationPresent(FieldMeta.class) && field2[j].isAnnotationPresent(FieldMeta.class)) {
                            FieldMeta metaAnnotation = field1[i].getAnnotation(FieldMeta.class);
                            Map<String, Object> map2 = new HashMap<String, Object>();
                            map2.put("name", metaAnnotation.name());
                            map2.put("old", field1[i].get(class1) == null ? "" : field1[i].get(class1) );
                            map2.put("new", field2[j].get(class2));
                            //解决时间格式化问题-bean上加了@DateTimeFormat(pattern="yyyy-MM-dd")
                            if(field1[i].isAnnotationPresent(DateTimeFormat.class) && field2[j].isAnnotationPresent(DateTimeFormat.class) ){
                                String old = DateUtils.formatDate((Date) field1[i].get(class1),field1[i].getAnnotation(DateTimeFormat.class).pattern());
                                map2.put("old",old == null ? "": old);
                                map2.put("new", DateUtils.formatDate((Date) field2[j].get(class2),field2[j].getAnnotation(DateTimeFormat.class).pattern()));
                            }
                            //解决数据字典text/value转换问题-bean上加了@Dict(dicCode = "groupField",isCommon = false)  忽略以下代码因为本人不用
                            list.add(map2);
                        }
                        break;
                    }
                }
        }
        return list;
    }

    //对比两个数据是否内容相同
    public static boolean compareTwo(Object object1, Object object2) {

        if (object1 == null && object2 == null) {
            return true;
        }
        if (object1 == null && object2 != null) {
            return false;
        }
        if (object1.equals(object2)) {
            return true;
        }
        return false;
    }
}


3.调用方式:
在web项目的impl中的方法,controller调用即可。

/**
 * @description: 数据填报控制层
 * @author: 黑猫
 * @date: 2023/4/15 22:22
 * @version:1.0
 */
@Tag(name = "管理后台 - 数据填报")
@RestController
@RequestMapping("/dataFill")
@Validated
@CrossOrigin
public class DataFillController extends BaseController {
   @PostMapping("/excavateInfo/save")
    @Operation(summary = "保存采掘信息")
    @PreAuthorize("@ss.hasPermission('indexmgr:index:update')")
    public CommonResult<Integer> saveExcavateInfo(@Validated @RequestBody ExcavateInfoEntity excavateInfoEntity) {
         //判断id是否为空,如果不为空进行更新操作,否则进行新增操作
        if(excavateInfoEntity.getId()!=0){
            dataFillingService.updateExcavateInfo(excavateInfoEntity);
        }else{
            dataFillingService.saveExcavateInfo(excavateInfoEntity);
        } 
        return CommonResult.success(excavateInfoEntity.getId());
    }
}

比如:新建一个DataFillServiceImpl .java 调用


/**
 * @description: 数据填报Service实现类
 * @author: 黑猫
 * @date: 2023/4/15 22:44
 * @version:1.0
 */
@Service
@Validated
@Slf4j
public class DataFillServiceImpl implements DataFillService {
    @Resource
    private DataFillMapper dataFillMapper;

    @Override
    public int saveExcavateInfo(ExcavateInfoEntity excavateInfoEntity) {
        excavateInfoEntity.setCreator(SecurityFrameworkUtils.getLoginUserId().toString());
        return dataFillMapper.saveExcavateInfo(excavateInfoEntity);
    }

    @Override
    public void updateExcavateInfo(ExcavateInfoEntity model) {
        try {
            //第一步:查旧数据
            ExcavateInfoEntity oldModel = dataFillMapper.selectExcavateInfoById(model.getId());
            List<Map<String, Object>> list = new ArrayList<>();
            //第二步:旧新数据对比
            list = CompareObjectUtils.compareTwoClass(oldModel, model);
            String content = "";  // 定义变更字符串
            for (Map<String, Object> map : list) {
                if (map.get("old") == null) {
                    map.put("old", "无");
                }
                content += map.get("name") + ":" + map.get("old") + " 变更为 " + map.get("new") + "。";
            }
            //第三步:记录表新增历史
            if (content.length() > 0) {
                KpiOperateLogEntity item = new KpiOperateLogEntity();   // 数据变更实体对象
                item.setCollectDay(model.getCollectDay());
                item.setChangeContent(content);  //变更内容
                item.setCreator(SecurityFrameworkUtils.getLoginUserId().toString());
                dataFillMapper.insertOperateLog(item); //记录表新增历史
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //第四步:更新操作
        model.setUpdater(SecurityFrameworkUtils.getLoginUserId().toString());
        dataFillMapper.updateExcavateInfo(model);
    }
}

mybatis 查旧数据

   <select id="selectExcavateInfoById" resultType="com.bonc.boot.module.web.entity.ExcavateInfoEntity">
        select * from nmzj_excavate_info where id = #{id}
    </select>

插入操作日志sql

  <insert id="insertOperateLog" parameterType="com.bonc.boot.module.web.entity.KpiOperateLogEntity">
        insert into nmzj_kpi_operate_log(collect_day,change_content,creator) values(#{collectDay},#{changeContent},#{creator})
    </insert>

业务实体类

package com.bonc.boot.module.web.entity;

import com.bonc.boot.module.web.annotation.FieldMeta;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @description: 采掘信息实体类 注意:对比的字段需要填写@FieldMeta
 * @author: 黑猫
 * @date: 2023/4/15 22:27
 * @version:1.0
 */
@Data
public class ExcavateInfoEntity implements Serializable {
    //序列
    private int id;
    //工作面名称
    @FieldMeta(name = "工作面名称")
    private String workSurfaceName;
    //资源储量-总储量
    @FieldMeta(name = "资源储量-总储量")
    private String resReservesTotal;
    //资源储量-已采
    @FieldMeta(name = "资源储量-已采")
    private String resReservesCollected;
    //资源储量-剩余
    @FieldMeta(name = "资源储量-剩余")
    private String resReservesSurplus;
    //掘进长度-目标
    @FieldMeta(name = "掘进长度-目标")
    private String drivingLengthTarget;
    //掘进长度-已掘
    @FieldMeta(name = "掘进长度-已掘")
    private String drivingLengthExcavated;
    //掘进长度-剩余
    @FieldMeta(name = "掘进长度-剩余")
    private String drivingLengthSurplus;
    //采面征迁
    @FieldMeta(name = "采面征迁")
    private String drivingLengthCollectSurfaceMigration;
    //创建时间
    private Date createTime;
    //更新时间
    private Date updateTime;
    //逻辑删除
    private int deleted;
    //创建者
    private String creator;
    //更新者
    private String updater;
    //状态
    private String state;
    //指标日期
    private String collectDay;
}

同一个实体中,可以添加多个该注解给不同的字段,达到比较多个字段的效果。

操作日志记录表实体类:

package com.bonc.boot.module.web.entity;

import lombok.Data;

/**
 * @description:
 * @author: 黑猫
 * @date: 2023/4/17 11:29
 * @version:1.0
 */

@Data
public class KpiOperateLogEntity {
    private int id; //主键ID
    private String collectDay; //指标日期
    private String changeContent;  //变更内容
    private String creator;  //更新人
    private String createTime;  //修改时间
}

创建指标操作记录表sql

CREATE TABLE `your_dbname`.`kpi_operate_log`  (
  `id` bigint(255) NOT NULL AUTO_INCREMENT COMMENT '主键序列',
  `collect_day` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '指标日期',
  `change_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '变更内容',
  `creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

完成这些之后,可以启动项目,查看了。

效果如图:

本文到此结束了,如果有什么建议请提出来。

posted @ 2023-04-19 11:12  星星之草%  阅读(1461)  评论(0编辑  收藏  举报