SpringBoot + 规则版本对比 + 差异高亮:新旧规则效果一目了然,降低上线风险!

问题背景

在业务系统中,规则引擎是核心组件之一,用于实现业务逻辑的灵活配置和快速调整。然而,规则的修改和上线往往伴随着风险:

  1. 规则复杂度高:业务规则通常包含多个条件和动作,逻辑复杂,难以直观理解
  2. 修改影响范围大:规则修改可能影响大量业务场景,难以全面评估影响
  3. 测试覆盖不足:规则测试往往依赖人工验证,容易遗漏边界情况
  4. 上线风险高:规则上线后发现问题,回滚成本高,影响业务连续性
  5. 版本管理混乱:缺乏有效的规则版本管理,难以追溯历史变更

这些问题在规则频繁更新的场景下尤为突出,比如电商平台的促销规则、风控系统的风控规则、推荐系统的推荐规则等。如何降低规则上线的风险,提高规则管理的效率,是业务系统面临的重要挑战。

核心概念

1. 规则版本管理

定义:对业务规则进行版本化管理,记录每次规则变更的历史信息,包括规则内容、修改时间、修改人、变更原因等。

优势

  • 可追溯性:能够追溯规则的历史变更,了解规则的演进过程
  • 可回滚:当新规则出现问题时,可以快速回滚到历史版本
  • 可对比:能够对比不同版本的规则差异,便于审核和验证

2. 规则对比

定义:对比新旧规则版本的内容差异,识别规则变更的具体内容,包括新增、修改和删除的条件和动作。

对比维度

  • 条件差异:对比规则条件的变化,如新增条件、修改条件、删除条件
  • 动作差异:对比规则动作的变化,如新增动作、修改动作、删除动作
  • 参数差异:对比规则参数的变化,如参数值的变化、参数类型的变化

3. 差异高亮

定义:通过视觉化的方式,高亮显示规则变更的具体内容,让规则差异一目了然。

高亮方式

  • 颜色区分:使用不同颜色表示不同类型的变更,如红色表示删除,绿色表示新增,黄色表示修改
  • 标记符号:使用符号标记变更内容,如"+"表示新增,"-"表示删除,"~"表示修改
  • 位置标注:在规则内容中标注变更的位置,便于快速定位

4. 规则测试

定义:在规则上线前,对新规则进行充分测试,验证规则的正确性和效果。

测试类型

  • 单元测试:测试规则的各个条件和动作是否按预期工作
  • 集成测试:测试规则与其他系统的集成是否正常
  • 回归测试:测试规则修改后,原有功能是否受到影响
  • 性能测试:测试规则的执行性能是否满足要求

实现方案

1. 技术栈选择

  • Spring Boot 2.7.5:提供基础框架支持
  • Spring Web:提供REST API
  • Spring Data JPA:操作数据库
  • MySQL 8.0:存储规则版本和对比结果
  • Lombok:简化代码
  • DiffUtils:规则内容对比
  • Bootstrap:Web界面样式

2. 数据库设计

规则版本表(rule_version)

字段名数据类型描述
idbigint(20)主键ID
rule_idvarchar(100)规则ID
rule_namevarchar(255)规则名称
rule_contenttext规则内容(JSON格式)
versionint(11)版本号
statustinyint(4)状态:0-草稿,1-已发布,2-已废弃
create_timedatetime创建时间
update_timedatetime更新时间
creatorvarchar(100)创建人
updatervarchar(100)更新人
change_reasonvarchar(500)变更原因

规则对比结果表(rule_diff)

字段名数据类型描述
idbigint(20)主键ID
rule_idvarchar(100)规则ID
old_versionint(11)旧版本号
new_versionint(11)新版本号
diff_contenttext差异内容(JSON格式)
create_timedatetime创建时间
creatorvarchar(100)创建人

3. 核心代码实现

3.1 规则版本实体类

@Entity
@Table(name = "rule_version")
public class RuleVersion {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "rule_id", nullable = false, length = 100)
    private String ruleId;
    
    @Column(name = "rule_name", nullable = false, length = 255)
    private String ruleName;
    
    @Column(name = "rule_content", nullable = false, columnDefinition = "text")
    private String ruleContent;
    
    @Column(name = "version", nullable = false)
    private Integer version;
    
    @Column(name = "status", nullable = false)
    private Integer status;
    
    @Column(name = "create_time", nullable = false, updatable = false)
    private LocalDateTime createTime;
    
    @Column(name = "update_time", nullable = false)
    private LocalDateTime updateTime;
    
    @Column(name = "creator", length = 100)
    private String creator;
    
    @Column(name = "updater", length = 100)
    private String updater;
    
    @Column(name = "change_reason", length = 500)
    private String changeReason;
    
    // getter/setter 方法
}

3.2 规则对比服务类

@Service
public class RuleDiffService {
    
    @Autowired
    private RuleVersionRepository ruleVersionRepository;
    
    @Autowired
    private RuleDiffRepository ruleDiffRepository;
    
    /**
     * 对比两个版本的规则
     */
    public RuleDiff compareVersions(String ruleId, Integer oldVersion, Integer newVersion) {
        // 查询两个版本的规则
        RuleVersion oldRule = ruleVersionRepository.findByRuleIdAndVersion(ruleId, oldVersion);
        RuleVersion newRule = ruleVersionRepository.findByRuleIdAndVersion(ruleId, newVersion);
        
        if (oldRule == null || newRule == null) {
            throw new RuntimeException("规则版本不存在");
        }
        
        // 解析规则内容
        Map<String, Object> oldContent = parseRuleContent(oldRule.getRuleContent());
        Map<String, Object> newContent = parseRuleContent(newRule.getRuleContent());
        
        // 对比规则内容
        Map<String, Object> diff = compareRuleContent(oldContent, newContent);
        
        // 保存对比结果
        RuleDiff ruleDiff = new RuleDiff();
        ruleDiff.setRuleId(ruleId);
        ruleDiff.setOldVersion(oldVersion);
        ruleDiff.setNewVersion(newVersion);
        ruleDiff.setDiffContent(serializeDiff(diff));
        ruleDiff.setCreateTime(LocalDateTime.now());
        ruleDiff.setCreator(getCurrentUser());
        
        return ruleDiffRepository.save(ruleDiff);
    }
    
    /**
     * 对比规则内容
     */
    private Map<String, Object> compareRuleContent(Map<String, Object> oldContent, Map<String, Object> newContent) {
        Map<String, Object> diff = new HashMap<>();
        
        // 对比条件
        List<Map<String, Object>> oldConditions = (List<Map<String, Object>>) oldContent.get("conditions");
        List<Map<String, Object>> newConditions = (List<Map<String, Object>>) newContent.get("conditions");
        diff.put("conditions", compareConditions(oldConditions, newConditions));
        
        // 对比动作
        List<Map<String, Object>> oldActions = (List<Map<String, Object>>) oldContent.get("actions");
        List<Map<String, Object>> newActions = (List<Map<String, Object>>) newContent.get("actions");
        diff.put("actions", compareActions(oldActions, newActions));
        
        return diff;
    }
    
    /**
     * 对比条件
     */
    private List<Map<String, Object>> compareConditions(List<Map<String, Object>> oldConditions, List<Map<String, Object>> newConditions) {
        List<Map<String, Object>> diff = new ArrayList<>();
        
        // 找出新增的条件
        for (Map<String, Object> newCondition : newConditions) {
            if (!oldConditions.contains(newCondition)) {
                Map<String, Object> item = new HashMap<>();
                item.put("type", "add");
                item.put("content", newCondition);
                diff.add(item);
            }
        }
        
        // 找出删除的条件
        for (Map<String, Object> oldCondition : oldConditions) {
            if (!newConditions.contains(oldCondition)) {
                Map<String, Object> item = new HashMap<>();
                item.put("type", "delete");
                item.put("content", oldCondition);
                diff.add(item);
            }
        }
        
        // 找出修改的条件
        for (Map<String, Object> oldCondition : oldConditions) {
            for (Map<String, Object> newCondition : newConditions) {
                if (oldCondition.get("id").equals(newCondition.get("id")) && !oldCondition.equals(newCondition)) {
                    Map<String, Object> item = new HashMap<>();
                    item.put("type", "modify");
                    item.put("oldContent", oldCondition);
                    item.put("newContent", newCondition);
                    diff.add(item);
                }
            }
        }
        
        return diff;
    }
    
    // 其他方法:compareActions、parseRuleContent、serializeDiff等
}

3.3 规则管理控制器

@RestController
@RequestMapping("/rules")
public class RuleController {
    
    @Autowired
    private RuleVersionRepository ruleVersionRepository;
    
    @Autowired
    private RuleDiffService ruleDiffService;
    
    /**
     * 创建新规则版本
     */
    @PostMapping
    public ResponseEntity<Result> createRule(@RequestBody RuleCreateRequest request) {
        try {
            // 获取当前最大版本号
            Integer maxVersion = ruleVersionRepository.findMaxVersionByRuleId(request.getRuleId());
            Integer newVersion = maxVersion == null ? 1 : maxVersion + 1;
            
            // 创建新规则版本
            RuleVersion ruleVersion = new RuleVersion();
            ruleVersion.setRuleId(request.getRuleId());
            ruleVersion.setRuleName(request.getRuleName());
            ruleVersion.setRuleContent(request.getRuleContent());
            ruleVersion.setVersion(newVersion);
            ruleVersion.setStatus(0); // 草稿状态
            ruleVersion.setCreator(getCurrentUser());
            ruleVersion.setChangeReason(request.getChangeReason());
            
            ruleVersionRepository.save(ruleVersion);
            
            return ResponseEntity.ok(Result.success(ruleVersion));
        } catch (Exception e) {
            log.error("创建规则失败", e);
            return ResponseEntity.ok(Result.error("创建规则失败:" + e.getMessage()));
        }
    }
    
    /**
     * 对比规则版本
     */
    @GetMapping("/{ruleId}/diff")
    public ResponseEntity<Result> compareVersions(
            @PathVariable String ruleId,
            @RequestParam Integer oldVersion,
            @RequestParam Integer newVersion) {
        try {
            RuleDiff ruleDiff = ruleDiffService.compareVersions(ruleId, oldVersion, newVersion);
            return ResponseEntity.ok(Result.success(ruleDiff));
        } catch (Exception e) {
            log.error("对比规则版本失败", e);
            return ResponseEntity.ok(Result.error("对比规则版本失败:" + e.getMessage()));
        }
    }
    
    /**
     * 发布规则版本
     */
    @PutMapping("/{ruleId}/publish")
    public ResponseEntity<Result> publishRule(
            @PathVariable String ruleId,
            @RequestParam Integer version) {
        try {
            // 废弃旧版本
            ruleVersionRepository.updateStatusByRuleId(ruleId, 2);
            
            // 发布新版本
            RuleVersion ruleVersion = ruleVersionRepository.findByRuleIdAndVersion(ruleId, version);
            ruleVersion.setStatus(1);
            ruleVersion.setUpdater(getCurrentUser());
            ruleVersionRepository.save(ruleVersion);
            
            return ResponseEntity.ok(Result.success("发布规则成功"));
        } catch (Exception e) {
            log.error("发布规则失败", e);
            return ResponseEntity.ok(Result.error("发布规则失败:" + e.getMessage()));
        }
    }
    
    // 其他方法:getRuleVersions、getRuleVersion等
}

3.4 Web界面

Web界面使用HTML、CSS和JavaScript实现,包括规则列表展示、规则版本对比、差异高亮显示等功能。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>规则版本对比</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <style>
        .diff-add {
            background-color: #d4edda;
            color: #155724;
            padding: 10px;
            margin: 5px 0;
            border-left: 4px solid #28a745;
        }
        .diff-delete {
            background-color: #f8d7da;
            color: #721c24;
            padding: 10px;
            margin: 5px 0;
            border-left: 4px solid #dc3545;
        }
        .diff-modify {
            background-color: #fff3cd;
            color: #856404;
            padding: 10px;
            margin: 5px 0;
            border-left: 4px solid #ffc107;
        }
        .diff-content {
            font-family: monospace;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <div class="container mt-5">
        <h1 class="mb-4">规则版本对比</h1>
        
        <!-- 规则选择 -->
        <div class="card mb-4">
            <div class="card-body">
                <div class="row">
                    <div class="col-md-4">
                        <label for="ruleSelect" class="form-label">选择规则</label>
                        <select class="form-select" id="ruleSelect">
                            <option value="">请选择规则</option>
                        </select>
                    </div>
                    <div class="col-md-3">
                        <label for="oldVersion" class="form-label">旧版本</label>
                        <select class="form-select" id="oldVersion">
                            <option value="">请选择版本</option>
                        </select>
                    </div>
                    <div class="col-md-3">
                        <label for="newVersion" class="form-label">新版本</label>
                        <select class="form-select" id="newVersion">
                            <option value="">请选择版本</option>
                        </select>
                    </div>
                    <div class="col-md-2 d-flex align-items-end">
                        <button class="btn btn-primary w-100" id="compareBtn">对比</button>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- 对比结果 -->
        <div class="card" id="diffResult" style="display: none;">
            <div class="card-header">
                <h5 class="mb-0">对比结果</h5>
            </div>
            <div class="card-body">
                <div id="diffContent">
                    <!-- 对比内容将通过JavaScript动态生成 -->
                </div>
            </div>
        </div>
    </div>
    
    <script>
        // 页面加载完成后加载规则列表
        $(document).ready(function() {
            loadRules();
        });
        
        // 加载规则列表
        function loadRules() {
            $.ajax({
                url: '/api/rules',
                type: 'GET',
                success: function(response) {
                    if (response.code === 200) {
                        var rules = response.data;
                        var select = $('#ruleSelect');
                        select.empty();
                        select.append('<option value="">请选择规则</option>');
                        
                        rules.forEach(function(rule) {
                            select.append('<option value="' + rule.ruleId + '">' + rule.ruleName + '</option>');
                        });
                    } else {
                        alert('加载规则列表失败:' + response.message);
                    }
                },
                error: function() {
                    alert('加载规则列表失败');
                }
            });
        }
        
        // 规则选择变化时加载版本列表
        $('#ruleSelect').change(function() {
            var ruleId = $(this).val();
            loadVersions(ruleId);
        });
        
        // 加载版本列表
        function loadVersions(ruleId) {
            if (!ruleId) {
                $('#oldVersion').empty().append('<option value="">请选择版本</option>');
                $('#newVersion').empty().append('<option value="">请选择版本</option>');
                return;
            }
            
            $.ajax({
                url: '/api/rules/' + ruleId + '/versions',
                type: 'GET',
                success: function(response) {
                    if (response.code === 200) {
                        var versions = response.data;
                        var oldSelect = $('#oldVersion');
                        var newSelect = $('#newVersion');
                        
                        oldSelect.empty().append('<option value="">请选择版本</option>');
                        newSelect.empty().append('<option value="">请选择版本</option>');
                        
                        versions.forEach(function(version) {
                            var option = '<option value="' + version.version + '">v' + version.version + ' - ' + version.status + ' - ' + version.createTime + '</option>';
                            oldSelect.append(option);
                            newSelect.append(option);
                        });
                    } else {
                        alert('加载版本列表失败:' + response.message);
                    }
                },
                error: function() {
                    alert('加载版本列表失败');
                }
            });
        }
        
        // 对比按钮点击事件
        $('#compareBtn').click(function() {
            var ruleId = $('#ruleSelect').val();
            var oldVersion = $('#oldVersion').val();
            var newVersion = $('#newVersion').val();
            
            if (!ruleId || !oldVersion || !newVersion) {
                alert('请选择规则和版本');
                return;
            }
            
            compareVersions(ruleId, oldVersion, newVersion);
        });
        
        // 对比版本
        function compareVersions(ruleId, oldVersion, newVersion) {
            $.ajax({
                url: '/api/rules/' + ruleId + '/diff',
                type: 'GET',
                data: {
                    oldVersion: oldVersion,
                    newVersion: newVersion
                },
                success: function(response) {
                    if (response.code === 200) {
                        var diff = response.data;
                        displayDiff(diff);
                    } else {
                        alert('对比版本失败:' + response.message);
                    }
                },
                error: function() {
                    alert('对比版本失败');
                }
            });
        }
        
        // 显示对比结果
        function displayDiff(diff) {
            var diffContent = $('#diffContent');
            diffContent.empty();
            
            // 显示条件差异
            var conditionsDiff = diff.conditions;
            if (conditionsDiff && conditionsDiff.length > 0) {
                var conditionsHtml = '<h6>条件差异</h6>';
                conditionsDiff.forEach(function(item) {
                    if (item.type === 'add') {
                        conditionsHtml += '<div class="diff-add"><strong>新增条件:</strong><div class="diff-content">' + JSON.stringify(item.content, null, 2) + '</div></div>';
                    } else if (item.type === 'delete') {
                        conditionsHtml += '<div class="diff-delete"><strong>删除条件:</strong><div class="diff-content">' + JSON.stringify(item.content, null, 2) + '</div></div>';
                    } else if (item.type === 'modify') {
                        conditionsHtml += '<div class="diff-modify"><strong>修改条件:</strong><div class="diff-content">旧值:' + JSON.stringify(item.oldContent, null, 2) + '</div><div class="diff-content">新值:' + JSON.stringify(item.newContent, null, 2) + '</div></div>';
                    }
                });
                diffContent.append(conditionsHtml);
            }
            
            // 显示动作差异
            var actionsDiff = diff.actions;
            if (actionsDiff && actionsDiff.length > 0) {
                var actionsHtml = '<h6>动作差异</h6>';
                actionsDiff.forEach(function(item) {
                    if (item.type === 'add') {
                        actionsHtml += '<div class="diff-add"><strong>新增动作:</strong><div class="diff-content">' + JSON.stringify(item.content, null, 2) + '</div></div>';
                    } else if (item.type === 'delete') {
                        actionsHtml += '<div class="diff-delete"><strong>删除动作:</strong><div class="diff-content">' + JSON.stringify(item.content, null, 2) + '</div></div>';
                    } else if (item.type === 'modify') {
                        actionsHtml += '<div class="diff-modify"><strong>修改动作:</strong><div class="diff-content">旧值:' + JSON.stringify(item.oldContent, null, 2) + '</div><div class="diff-content">新值:' + JSON.stringify(item.newContent, null, 2) + '</div></div>';
                    }
                });
                diffContent.append(actionsHtml);
            }
            
            $('#diffResult').show();
        }
    </script>
</body>
</html>

最佳实践

1. 规则版本管理

  • 版本号管理:使用递增的版本号,确保版本的唯一性和可追溯性
  • 状态管理:明确规则的状态(草稿、已发布、已废弃),避免误操作
  • 变更记录:记录规则变更的原因和修改人,便于追溯和审计
  • 权限控制:根据角色分配规则管理权限,确保规则的安全性

2. 规则对比

  • 全面对比:对比规则的所有内容,包括条件、动作、参数等
  • 精确识别:准确识别规则变更的类型(新增、修改、删除)
  • 差异标注:清晰标注差异的位置和内容,便于理解和审核
  • 对比结果保存:保存对比结果,便于后续查看和审计

3. 差异高亮

  • 颜色区分:使用不同颜色表示不同类型的变更,提高可读性
  • 符号标记:使用符号标记变更内容,便于快速识别
  • 位置标注:在规则内容中标注变更的位置,便于定位
  • 格式化显示:格式化显示规则内容,提高可读性

4. 规则测试

  • 充分测试:在规则上线前进行充分的测试,验证规则的正确性
  • 回归测试:测试规则修改后,原有功能是否受到影响
  • 性能测试:测试规则的执行性能是否满足要求
  • 灰度发布:采用灰度发布策略,逐步放量,降低上线风险

5. 上线流程

  • 审核机制:建立规则审核机制,确保规则变更的合理性
  • 发布流程:建立规范的发布流程,包括测试、审核、发布、监控等环节
  • 回滚机制:建立快速回滚机制,当规则出现问题时能够快速回滚
  • 监控告警:监控规则执行情况,及时发现和解决问题

总结与展望

总结

本方案通过SpringBoot + 规则版本对比 + 差异高亮的组合,实现了规则版本的可视化对比和差异高亮显示,有效降低了规则上线的风险。主要优势包括:

  1. 可视化对比:通过Web界面直观地展示规则差异,便于理解和审核
  2. 差异高亮:使用颜色和符号标记差异内容,提高可读性
  3. 版本管理:完善的规则版本管理,支持版本追溯和回滚
  4. 降低风险:通过规则对比和测试,降低规则上线的风险
  5. 提高效率:自动化的规则对比和差异高亮,提高规则管理的效率

展望

  1. 智能对比:引入AI技术,智能识别规则变更的影响范围和风险
  2. 自动化测试:自动生成测试用例,自动执行规则测试
  3. 实时监控:实时监控规则执行情况,及时发现和解决问题
  4. 预测分析:基于历史数据,预测规则变更的影响和效果
  5. 可视化展示:提供更丰富的可视化展示方式,如图表、流程图等

结语

规则版本对比和差异高亮是规则管理的重要功能,能够有效降低规则上线的风险,提高规则管理的效率。通过本方案的实现,我们可以让规则变更更加透明和可控,确保规则的质量和稳定性。

希望本方案能够为您的项目提供参考,帮助您实现更加安全、高效的规则管理系统。


公众号:服务端技术精选

如果您有任何问题或建议,欢迎在评论区留言讨论。


标题:SpringBoot + 规则版本对比 + 差异高亮:新旧规则效果一目了然,降低上线风险!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/06/1772636807424.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消