• JaVers 版本历史功能完整实现指南


    JaVers 版本历史功能完整实现指南

    本文档基于 FBI 系统的 JaVers 实现,提供从零开始在 Spring Boot + Vue 3 项目中集成实体版本历史功能的完整指导。涵盖依赖配置、事件驱动架构、Entity/DTO 注解、API 设计、前端 HistoryDialog 组件等全部环节。


    目录

    1. 技术选型与依赖
    2. JaVers 配置
    3. 核心基础设施(事件驱动架构)
    4. 实体与 DTO 的 JaVers 注解规范
    5. 业务层集成(如何发布事件触发快照)
    6. 后端 API 设计(查询、详情、对比)
    7. 已有数据初始化快照
    8. 前端 HistoryDialog 组件
    9. 前端 API 层
    10. 页面集成示例
    11. 完整实施步骤清单

    1. 技术选型与依赖

    后端(Java)

    JaVers 版本7.9.0

    Maven 依赖(在父 POM 的 中声明版本):

    <properties>
        <javers.version>7.9.0javers.version>
    properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.javersgroupId>
                <artifactId>javers-spring-boot-starter-sqlartifactId>
                <version>${javers.version}version>
            dependency>
        dependencies>
    dependencyManagement>
    

    在 common 模块中引入(所有业务模块依赖 common):

    <dependency>
        <groupId>org.javersgroupId>
        <artifactId>javers-spring-boot-starter-sqlartifactId>
    dependency>
    

    说明:使用 javers-spring-boot-starter-sql 而非 javers-spring-boot-starter-mongo。JaVers 的快照数据存储在应用同一 MySQL 数据库中,无需额外数据库。

    前端(Vue 3)

    无需额外依赖,使用 Element Plus 的 el-tableel-scrollbarel-checkboxel-empty 组件即可。


    2. JaVers 配置

    application.yml

    javers:
      spring-data:
        enabled: false    # 不使用 @JaversSpringDataAuditable 自动审计
      sql:
        enabled: true
        # 显式指定使用应用的 EntityManagerFactory,确保 JaVers 与应用共享事务
        entity-manager-bean-name: entityManagerFactory
    

    关键说明

    • spring-data.enabled: false:我们不使用 @JaversSpringDataAuditable 注解。这种自动审计方式会导致每次 Spring Data JPA 保存时都自动提交快照,粒度太粗且无法使用 DTO 包装。
    • sql.enabled: true:使用 SQL 存储(MySQL)。
    • entity-manager-bean-name:必须指定,确保 JaVers 的快照提交在应用的同一事务中。如果 JaVers 提交失败,整个业务操作回滚。

    JaVers 自动创建的表

    启动应用后,JaVers 会在数据库中自动创建以下表(通过 JPA ddl-auto=update 或 JaVers 内置的 Liquibase 脚本):

    表名 用途
    javers_commit 每次快照提交的元数据(作者、时间、提交ID)
    javers_snapshot 实体在每个版本号下的完整状态(JSON 序列化)
    javers_global_id 全局对象标识(类型名 + 实体ID 的映射)

    3. 核心基础设施(事件驱动架构)

    3.1 设计理念

    不使用 JaVers 自带的 @JaversSpringDataAuditable,而是通过自定义 Spring 事件机制手动触发快照提交。这样做的优势:

    1. 精确控制提交时机:只在业务真正完成时提交,不在中间状态提交
    2. 支持 DTO 包装:可以提交自定义的 SnapshotDTO(包含关联数据),而非原始 Entity
    3. 统一事件入口:所有实体的变更通过同一事件通道,方便后续扩展(如通知、日志等)
    4. 同步事务保证:事件监听器是同步的,快照提交与业务操作在同一事务中

    3.2 EntityChangeEvent —— 事件类

    package your.project.common.event;
    
    import lombok.Getter;
    import org.springframework.context.ApplicationEvent;
    
    @Getter
    public class EntityChangeEvent extends ApplicationEvent {
    
        public enum ChangeType {
            CREATED,    // 新增
            UPDATED,    // 更新
            DELETED     // 删除
        }
    
        private final ChangeType changeType;
        private final Object entity;        // 要提交给 JaVers 的对象(可以是 Entity 或 DTO)
        private final String author;        // 操作人标识
    
        public EntityChangeEvent(Object source, ChangeType changeType, Object entity, String author) {
            super(source);
            this.changeType = changeType;
            this.entity = entity;
            this.author = author;
        }
    
        /** 通过反射获取实体 ID */
        public Long getEntityId() {
            if (entity != null) {
                try {
                    var method = entity.getClass().getMethod("getId");
                    return (Long) method.invoke(entity);
                } catch (Exception e) {
                    return null;
                }
            }
            return null;
        }
    
        /** 获取实体类型名称 */
        public String getEntityType() {
            return entity != null ? entity.getClass().getSimpleName() : null;
        }
    }
    

    3.3 EntityChangeEventPublisher —— 事件发布器

    package your.project.common.event;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.stereotype.Component;
    
    @Component
    @RequiredArgsConstructor
    public class EntityChangeEventPublisher {
    
        private static final String DEFAULT_AUTHOR = "system";
        private final ApplicationEventPublisher publisher;
    
        /** 获取当前登录用户标识。
         *  你需要替换为你们项目中获取当前用户的方法 */
        private String getCurrentAuthor() {
            // 示例:从 SecurityContext 或自定义 ThreadLocal 获取
            // return SecurityContextHolder.getContext().getAuthentication().getName();
            return DEFAULT_AUTHOR;
        }
    
        /** 发布新增事件 */
        public  void publishCreated(T entity) {
            publishCreated(entity, getCurrentAuthor());
        }
    
        public  void publishCreated(T entity, String author) {
            publisher.publishEvent(new EntityChangeEvent(
                    this, EntityChangeEvent.ChangeType.CREATED, entity, author));
        }
    
        /** 发布更新事件 */
        public  void publishUpdated(T entity) {
            publishUpdated(entity, getCurrentAuthor());
        }
    
        public  void publishUpdated(T entity, String author) {
            publisher.publishEvent(new EntityChangeEvent(
                    this, EntityChangeEvent.ChangeType.UPDATED, entity, author));
        }
    
        /** 发布删除事件 */
        public  void publishDeleted(T entity) {
            publishDeleted(entity, getCurrentAuthor());
        }
    
        public  void publishDeleted(T entity, String author) {
            publisher.publishEvent(new EntityChangeEvent(
                    this, EntityChangeEvent.ChangeType.DELETED, entity, author));
        }
    }
    

    3.4 EntityChangeEventListener —— 事件监听器(核心)

    package your.project.common.event;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.javers.core.Javers;
    import org.springframework.context.event.EventListener;
    import org.springframework.stereotype.Component;
    
    @Component
    @RequiredArgsConstructor
    @Slf4j
    public class EntityChangeEventListener {
    
        private final Javers javers;
    
        /**
         * 同步监听实体变更事件,将实体状态提交到 JaVers。
         * 同步执行保证快照提交与业务操作在同一数据库事务中。
         */
        @EventListener
        public void handleEvent(EntityChangeEvent event) {
            try {
                switch (event.getChangeType()) {
                    case CREATED, UPDATED, DELETED:
                        // 核心调用:将对象提交给 JaVers
                        javers.commit(event.getAuthor(), event.getEntity());
                        break;
                }
            } catch (Exception e) {
                log.error("记录实体变更审计失败: {}", event, e);
                throw new RuntimeException("记录实体变更审计失败: " + e.getMessage());
            }
        }
    }
    

    重要说明

    • 监听器不加 @Async,是同步执行的。这意味着 JaVers 的 commit() 在同一个数据库事务中完成。
    • 如果 commit() 抛出异常,会向上传播导致业务操作回滚。
    • javers.commit(author, entity) 的两个参数:author 是操作人标识(可以是用户ID或用户名),entity 是待快照的对象(Entity 或 DTO)。

    4. 实体与 DTO 的 JaVers 注解规范

    4.1 基础实体类(TimeEntity —— 所有实体的父类)

    package your.project.common.entity;
    
    import jakarta.persistence.*;
    import lombok.Data;
    import org.javers.core.metamodel.annotation.DiffIgnore;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    import java.time.LocalDateTime;
    
    @Data
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class TimeEntity {
    
        @Column(updatable = false)
        @CreatedDate
        @DiffIgnore    // 创建时间不纳入版本对比
        private LocalDateTime createTime;
    
        @Column
        @LastModifiedDate
        @DiffIgnore    // 更新时间不纳入版本对比
        private LocalDateTime updateTime;
    }
    
    package your.project.common.entity;
    
    import jakarta.persistence.*;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    
    @EqualsAndHashCode(callSuper = true)
    @Data
    @MappedSuperclass
    public abstract class BaseEntity extends TimeEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.TABLE, generator = "id_generator")
        @TableGenerator(name = "id_generator")
        private Long id;
    }
    

    4.2 直接追踪 Entity(模式A)

    适用于简单实体,直接对 Entity 类加 JaVers 注解:

    package your.project.domain.entity;
    
    import jakarta.persistence.*;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    import org.javers.core.metamodel.annotation.PropertyName;
    import org.javers.core.metamodel.annotation.TypeName;
    import your.project.common.entity.BaseEntity;
    
    @Data
    @Entity
    @EqualsAndHashCode(callSuper = true)
    @Table(name = "your_entity")
    @TypeName("YourEntity")        // 给 JaVers 看的类型名,建议与类名一致
    public class YourEntity extends BaseEntity {
    
        @Column(length = 100)
        @PropertyName("名称")        // 版本对比时显示的中文名
        private String name;
    
        @Column(length = 50)
        @PropertyName("状态")
        private String status;
    
        @Column(length = 200)
        @PropertyName("描述")
        private String description;
    
        // 不想纳入版本记录的字段加 @DiffIgnore
        @DiffIgnore
        @Column(length = 50)
        private String internalCode;
    }
    

    4.3 使用 SnapshotDTO 追踪(模式B)

    适用于需要将关联数据也纳入快照的复杂聚合场景。核心思路:JaVers 提交的不是原始 Entity,而是手动构建的 DTO

    Step 1:定义 DTO(带 JaVers 注解)

    package your.project.domain.dto;
    
    import lombok.Data;
    import org.javers.core.metamodel.annotation.Id;
    import org.javers.core.metamodel.annotation.PropertyName;
    import org.javers.core.metamodel.annotation.TypeName;
    import org.javers.core.metamodel.annotation.DiffIgnore;
    import java.time.LocalDateTime;
    import java.util.List;
    
    @Data
    @TypeName("YourAggregateSnapshotDTO")
    public class YourAggregateSnapshotDTO {
    
        @Id                          // JaVers 的 @Id,不是 JPA 的
        @PropertyName("ID")
        private Long id;
    
        @PropertyName("名称")
        private String name;
    
        @PropertyName("状态")
        private String status;
    
        @PropertyName("子项列表")
        private List children;
    
        @DiffIgnore
        @PropertyName("创建时间")
        private LocalDateTime createTime;
    
        /**
         * 工厂方法:从 Entity 构建 DTO
         */
        public static YourAggregateSnapshotDTO from(YourEntity entity, List children) {
            YourAggregateSnapshotDTO dto = new YourAggregateSnapshotDTO();
            dto.setId(entity.getId());
            dto.setName(entity.getName());
            dto.setStatus(entity.getStatus());
            dto.setCreateTime(entity.getCreateTime());
            dto.setChildren(children.stream()
                    .map(ChildSnapshotDTO::from)
                    .toList());
            return dto;
        }
    }
    

    Step 2:子项用 @Value 标注(JaVers 的 ValueObject)

    package your.project.domain.dto;
    
    import lombok.Value;
    import org.javers.core.metamodel.annotation.PropertyName;
    
    @Value    // JaVers 的 @Value 注解,表示这是一个值对象,不独立追踪版本
    public class ChildSnapshotDTO {
    
        @PropertyName("子项ID")
        private Long childId;
    
        @PropertyName("子项名称")
        private String childName;
    
        public static ChildSnapshotDTO from(ChildEntity entity) {
            return new ChildSnapshotDTO(entity.getId(), entity.getName());
        }
    }
    

    4.4 JaVers 注解速查表

    注解 作用 使用位置
    @TypeName("名称") 给 JaVers 设置类型名称,影响查询和显示 Entity / DTO 类
    @Id 标记 JaVers 的全局唯一标识字段 DTO 的 ID 字段(Entity 用 JPA 的 @Id
    @PropertyName("中文名") 给字段设置显示名称,前端对比表显示 所有追踪的字段
    @DiffIgnore 忽略该字段,不纳入变更对比和快照 createTime、updateTime、内部编码等
    @Value 标记为 JaVers 值对象(无独立版本,跟随父对象) 列表中嵌套的子对象

    5. 业务层集成(如何发布事件触发快照)

    5.1 基本原则

    在业务操作完成后,Controller 返回之前,调用 eventPublisher.publishXxx()

    5.2 模式A:直接提交 Entity

    @Service
    @Transactional
    @RequiredArgsConstructor
    public class YourBiz {
    
        private final YourRepository repository;
        private final EntityChangeEventPublisher eventPublisher;
    
        // 新增
        public YourEntity create(YourCreateParam param) {
            YourEntity entity = new YourEntity();
            entity.setName(param.getName());
            entity.setStatus(param.getStatus());
            entity = repository.save(entity);
    
            // 发布创建事件 → 触发 JaVers 快照
            eventPublisher.publishCreated(entity);
    
            return entity;
        }
    
        // 更新
        public YourEntity update(Long id, YourUpdateParam param) {
            YourEntity entity = repository.findById(id)
                    .orElseThrow(() -> new RuntimeException("数据不存在"));
            entity.setName(param.getName());
            entity.setStatus(param.getStatus());
            entity = repository.save(entity);
    
            // 发布更新事件 → 触发 JaVers 快照
            eventPublisher.publishUpdated(entity);
    
            return entity;
        }
    
        // 删除
        public void delete(Long id) {
            YourEntity entity = repository.findById(id)
                    .orElseThrow(() -> new RuntimeException("数据不存在"));
    
            // 先发布删除事件(记录删除前的最后状态)
            eventPublisher.publishDeleted(entity);
    
            repository.delete(entity);
        }
    }
    

    5.3 模式B:提交 DTO(聚合场景)

    @Service
    @Transactional
    @RequiredArgsConstructor
    public class YourAggregateBiz {
    
        private final YourEntityRepository entityRepository;
        private final ChildEntityRepository childRepository;
        private final EntityChangeEventPublisher eventPublisher;
    
        public void update(Long id, YourUpdateParam param) {
            YourEntity entity = entityRepository.findById(id)
                    .orElseThrow(() -> new RuntimeException("数据不存在"));
            entity.setName(param.getName());
            entity.setStatus(param.getStatus());
            entityRepository.save(entity);
    
            // 加载关联数据
            List children = childRepository.findByParentId(id);
    
            // 构建 DTO 并发布事件
            YourAggregateSnapshotDTO dto = YourAggregateSnapshotDTO.from(entity, children);
            eventPublisher.publishUpdated(dto);
        }
    }
    

    5.4 定时任务/系统自动操作中的快照

    // 对于非用户触发的操作(如定时同步),使用指定作者
    eventPublisher.publishCreated(entity, "system_sync");
    
    // 或使用默认(会自动获取当前登录用户,非 Web 上下文中为 "system")
    eventPublisher.publishUpdated(entity);
    

    6. 后端 API 设计(查询、详情、对比)

    6.1 JaVersHistoryService —— 通用查询服务

    这是所有实体的版本历史查询的唯一实现,所有 Entity 共用。

    package your.project.domain.service;
    
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.javers.core.Javers;
    import org.javers.core.diff.Diff;
    import org.javers.core.metamodel.object.CdoSnapshot;
    import org.javers.repository.jql.QueryBuilder;
    import org.javers.shadow.Shadow;
    import org.springframework.stereotype.Service;
    
    import java.util.*;
    
    @Service
    @Slf4j
    public class JaVersHistoryService {
    
        private final Javers javers;
    
        public JaVersHistoryService(Javers javers) {
            this.javers = javers;
        }
    
        /**
         * 获取实体的历史快照列表(版本元数据,分页)
         *
         * @param entityId    实体ID(数据库主键)
         * @param entityClass 实体类(或 DTO 类)
         * @param limit       每页条数
         * @param skip        跳过条数
         * @return 版本列表(每项包含 version 和 commitMetadata)
         */
        public List> getEntitySnapshots(
                Long entityId, Class entityClass, int limit, int skip) {
    
            var query = QueryBuilder.byInstanceId(entityId, entityClass)
                    .skip(skip)
                    .limit(limit)
                    .build();
    
            List snapshots = javers.findSnapshots(query);
    
            return snapshots.stream()
                    .map(this::convertToMetadata)
                    .toList();
        }
    
        /**
         * 获取指定版本的完整快照数据
         *
         * @param entityId    实体ID
         * @param entityClass 实体类
         * @param version     版本号
         * @return 完整的属性 Map(属性名 → 属性值)
         */
        public Map getEntitySnapshotByVersion(
                Long entityId, int version, Class entityClass) {
    
            var query = QueryBuilder.byInstanceId(entityId, entityClass)
                    .withVersion(version)
                    .build();
    
            List snapshots = javers.findSnapshots(query);
    
            if (snapshots.isEmpty()) {
                throw new RuntimeException(String.format(
                        "版本号 %d 不存在,实体ID: %d", version, entityId));
            }
    
            Map stateMap = new HashMap<>();
            snapshots.get(0).getState().forEachProperty(stateMap::put);
            return stateMap;
        }
    
        /**
         * 比较两个版本的差异
         *
         * @param entityId    实体ID
         * @param version1    旧版本号
         * @param version2    新版本号
         * @param entityClass 实体类
         * @return JaVers Diff 对象(包含 changes 列表)
         */
        public Diff compareVersions(
                Long entityId, int version1, int version2, Class entityClass) {
    
            Object olderVersion = getShadow(entityId, version1, entityClass);
            Object newerVersion = getShadow(entityId, version2, entityClass);
            return javers.compare(olderVersion, newerVersion);
        }
    
        /** 获取指定版本的原始对象 */
        private Object getShadow(Long entityId, int version, Class entityClass) {
            var query = QueryBuilder.byInstanceId(entityId, entityClass)
                    .withVersion(version)
                    .build();
    
            List> shadows = javers.findShadows(query);
    
            if (shadows.isEmpty()) {
                throw new RuntimeException(String.format(
                        "版本号 %d 不存在,实体ID: %d", version, entityId));
            }
            return shadows.get(0).get();
        }
    
        /** 将 CdoSnapshot 转为精简的元数据 Map */
        private Map convertToMetadata(CdoSnapshot snapshot) {
            Map map = new HashMap<>();
            map.put("version", snapshot.getVersion());
    
            Map commitMetadata = new HashMap<>();
            String authorId = snapshot.getCommitMetadata().getAuthor();
            commitMetadata.put("author", resolveAuthorName(authorId));
            commitMetadata.put("authorId", authorId);
            commitMetadata.put("commitDate",
                    snapshot.getCommitMetadata().getCommitDate());
            map.put("commitMetadata", commitMetadata);
    
            return map;
        }
    
        /**
         * 根据作者ID解析显示名称。
         * 你需要替换为你们项目的用户查询方式。
         */
        private String resolveAuthorName(String authorId) {
            if (authorId == null || authorId.isEmpty()) {
                return "未知";
            }
            // 示例:从用户表查询
            // return userRepository.findById(Long.parseLong(authorId))
            //         .map(UserEntity::getDisplayName)
            //         .orElse(authorId);
            return authorId;
        }
    }
    

    6.2 Controller 示例

    每个需要版本历史的 Entity 在 Controller 中添加 3 个接口。以 YourEntity 为例:

    package your.project.interfaces.web;
    
    import org.javers.core.diff.Diff;
    import org.springframework.web.bind.annotation.*;
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/your-api-path/your-entity")
    public class YourEntityController {
    
        @Resource
        private JaVersHistoryService jaVersHistoryService;
    
        // ============ 版本历史接口 ============
    
        /** 获取历史快照列表(分页) */
        @GetMapping("snapshot-menu/{entityId}")
        public List> getSnapshots(
                @PathVariable Long entityId,
                @RequestParam(defaultValue = "20") int limit,
                @RequestParam(defaultValue = "0") int skip) {
            return jaVersHistoryService.getEntitySnapshots(
                    entityId, YourEntity.class, limit, skip);
        }
    
        /** 获取指定版本的完整快照 */
        @GetMapping("snapshot/{entityId}/{version}")
        public Map getSnapshotByVersion(
                @PathVariable Long entityId,
                @PathVariable int version) {
            return jaVersHistoryService.getEntitySnapshotByVersion(
                    entityId, version, YourEntity.class);
        }
    
        /** 比较两个版本 */
        @GetMapping("compare/{entityId}")
        public Diff compareVersions(
                @PathVariable Long entityId,
                @RequestParam int version1,
                @RequestParam int version2) {
            return jaVersHistoryService.compareVersions(
                    entityId, version1, version2, YourEntity.class);
        }
    }
    

    接口路径规范:建议路径格式为 /{业务前缀}/snapshot-menu/{entityId}/{业务前缀}/snapshot/{entityId}/{version}/{业务前缀}/compare/{entityId}

    6.3 关键 API 参数说明

    参数 类型 说明
    entityId Long 实体在数据库中的主键 ID(对应 JaVers 的 instanceId
    entityClass Class 传给 JaVers 的类型。直接追踪用 Entity.class,DTO 追踪用 DTO.class
    version int 版本号。JaVers 从 1 开始自增,每次 commit 产生一个新版本
    limit int 每页条数,默认 20
    skip int 跳过的条数,用于分页。前端第 N 页的 skip = N * limit

    6.4 Diff 返回结构

    compareVersions 返回的 Diff 对象 JSON 序列化后的结构:

    {
      "changes": [
        {
          "propertyName": "name",
          "left": "旧名称",
          "right": "新名称"
        },
        {
          "propertyName": "status",
          "left": "pending",
          "right": "completed"
        }
      ]
    }
    

    前端直接使用 changes 数组,每项有 propertyName(字段名)、left(旧值)、right(新值)。


    7. 已有数据初始化快照

    如果系统中已有历史数据(在集成 JaVers 之前就存在的记录),需要做一次性初始化。

    package your.project.application.biz;
    
    import jakarta.annotation.Resource;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.javers.core.Javers;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class JaVersInitBiz {
    
        private final Javers javers;
        private final YourEntityRepository yourEntityRepository;
    
        /** 为已有数据创建初始快照 */
        @Transactional
        public void initYourEntities() {
            List entities = yourEntityRepository.findAll();
            log.info("开始初始化 YourEntity 快照,共 {} 条", entities.size());
    
            for (YourEntity entity : entities) {
                try {
                    javers.commit("system_init", entity);
                } catch (Exception e) {
                    log.error("初始化快照失败: entityId={}", entity.getId(), e);
                }
            }
    
            log.info("初始化 YourEntity 快照完成");
        }
    }
    

    提供一个后台接口触发:

    @RestController
    @RequestMapping("/admin")
    public class AdminController {
    
        @Resource
        private JaVersInitBiz jaVersInitBiz;
    
        @PostMapping("/javers/init")
        public String initSnapshots(@RequestParam(defaultValue = "all") String type) {
            switch (type) {
                case "yourEntity":
                    jaVersInitBiz.initYourEntities();
                    break;
                case "all":
                    jaVersInitBiz.initYourEntities();
                    // 可以继续添加其他实体
                    break;
            }
            return "初始化完成";
        }
    }
    

    8. 前端 HistoryDialog 组件

    8.1 组件概述

    HistoryDialog.vue 是一个通用的版本历史弹窗组件,左右分栏布局:

    +----------------------------+--------------------------------------+
    |  版本列表 (200px)           |  详情/对比区                          |
    |                            |                                      |
    |  [对比] 按钮(需选2个版本)  |  对比模式:表格(属性 | 旧值 | 新值)  |
    |                            |                                      |
    |  [x] V3 张三               |  单版本模式:JSON 格式完整快照          |
    |  2024-01-03 10:00          |                                      |
    |                            |                                      |
    |  [x] V2 李四  <-- 高亮     |                                      |
    |  2024-01-02 10:00          |                                      |
    |                            |                                      |
    |  [ ] V1 王五               |                                      |
    |  2024-01-01 10:00          |                                      |
    |                            |                                      |
    |  ...(无限滚动加载更多)     |                                      |
    +----------------------------+--------------------------------------+
    

    8.2 完整组件代码

    
    
    
    
    
    

    8.3 组件 Props 说明(通过 open() 方法传入)

    参数 类型 说明
    entityId Number 实体在数据库中的主键 ID
    getSnapshots Function API函数:(entityId, { limit, skip }) => Promise
    getSnapshotByVersion Function API函数:(entityId, version) => Promise
    compareVersions Function API函数:(entityId, version1, version2) => Promise
    labelMapObj Object 字段名到中文标签的映射,如 { name: "名称" }
    titleStr String 弹窗标题
    valueFormatter Function 可选,自定义值格式化:(key, value) => formattedString

    9. 前端 API 层

    每个实体需要在 API 文件中添加 3 个函数:

    // yourApi.js
    import request from "@/utils/request"; // 你们的 HTTP 客户端
    
    const BASE = "/your-api-path/your-entity";
    
    // 获取历史快照列表
    export function getYourEntitySnapshots(entityId, params) {
      return request.get(`${BASE}/snapshot-menu/${entityId}`, { params });
    }
    
    // 获取指定版本完整快照
    export function getYourEntitySnapshot(entityId, version) {
      return request.get(`${BASE}/snapshot/${entityId}/${version}`);
    }
    
    // 比较两个版本
    export function compareYourEntityVersions(entityId, version1, version2) {
      return request.get(`${BASE}/compare/${entityId}`, {
        params: { version1, version2 },
      });
    }
    

    10. 页面集成示例

    在列表页添加"变更历史"按钮

    
    
    
    

    labelMap 映射规则

    • 如果 Entity 使用了 @PropertyName("中文名")propertyName 会是中文名,labelMap 可以传空对象 {}
    • 如果 Entity 没使用 @PropertyName,前端拿到的 propertyName 是 Java 字段名(英文),此时 labelMap 必须做映射

    11. 完整实施步骤清单

    按以下顺序实施,每步均可在局部验证:

    后端步骤

    前端步骤

    验证清单


    常见问题

    Q: 为什么不直接用 @JaversSpringDataAuditable

    A: 自动审计虽然简单,但有两个问题:(1) 每次 repository.save() 都触发,无法区分临时保存和最终提交;(2) 只能提交原始 Entity,无法提交聚合 DTO。事件驱动方式更灵活,业务代码明确控制何时提交。

    Q: DTO 模式和直接 Entity 模式怎么选?

    A: 如果实体只包含自身字段(无关联数据),直接用 Entity 模式。如果需要在快照中包含关联数据(如"项目-详情-迭代"的聚合视图),用 DTO 模式。Entity 模式更简单,DTO 模式更强大。

    Q: JaVers 快照会不会让数据库越来越大?

    A: 每次 commit 产生一条快照记录(JSON 存储)。对于普通业务实体(几百到几千条,修改频率不高),增长非常缓慢。JaVers 不提供自动清理功能,如需清理可自行按 commitDate 删除历史快照。

    Q: 如果 Entity 加了新字段,旧快照中该字段值是什么?

    A: 旧快照中没有该字段(因为提交时不包含),前端查询详情时会显示为 undefined。这通常是可接受的行为。如果需要在展示时处理,可以在 valueFormatter 中对缺失字段做默认值处理。

  • 相关阅读:
    容器的通俗讲解
    【Hack The Box】Linux练习-- Popcorn
    vsftpd配置
    【云计算 | OpenStack】从零开始使用libvirt创建一个kvm虚机
    包管理工具--》发布一个自己的npm包
    JavaScrip 学习笔记
    Linux——报错合集2
    【问题思考总结】解方程的时候什么时候可以消去方程?如何保证不丢解?
    【数据结构】线性表(七)堆栈:链式栈及其基本操作(初始化、判空、入栈、出栈、存取栈顶元素、清空栈);顺序栈与链式栈之比较
    将 Ordinals 与比特币智能合约集成:第 3 部分
  • 原文地址:https://www.cnblogs.com/xudong1990/p/20209231