案例 37:部门预算合规与超支预警
本案例对应 USE-CASES.md 场景 37(诉求 46:部门预算合规与超支预警),属于财务管理领域。规则脚本围绕一个部门年度预算快照,分别给出占用率分级、执行节奏、年末预测、单笔支出处置与风险分五个独立口径;Service 负责百分比与超支金额计算、预警文案组装,输出一份可直接推送给部门负责人与财务总监的预算预警决策。
部门预算执行状态由 BudgetStatus 承载:年度预算、实际已支出、已承诺金额(已签合同/在途采购单)、当前月份、年末预测支出,以及本次单笔申请支出的金额、支出类别(CAPEX 资本性 / OPEX 运营 / ADMIN 行政 / MARKETING 市场)、是否应急、CAPEX 是否已完成立项审批。财务部门每月需要据此判断哪些部门该亮灯、单笔支出该不该放行。
规则集给出三条预警主线:其一是占用率分级,按「实际支出 + 承诺金额」对年度预算的占比划红线——100% 红、90% 橙、70% 黄,其余绿色;其二是执行节奏,比较支出进度与时间进度,偏差超过 ±15% 判定为超前消耗或执行滞后;其三是年末预测,预测支出超过预算 10% 为严重超支(SEVERE)、超过预算为轻度超支(MILD)。
单笔支出处置串联预算等级与支出属性:CAPEX 未立项一律拦截、已立项放行;红色等级下应急支出升级专项审批、非应急支出直接拦截;橙色等级下单笔超过年度预算 5% 升级、否则警示放行;黄色警示放行;绿色直接放行。风险分以等级为底(红 100/橙 70/黄 40/绿 10),超前消耗与预测严重超支各加 10 分,最高可达 120 分。
决策结果 BudgetAlertDecision 输出占用率级别与百分比、执行节奏、预测等级、预测超支金额、单笔处置动作(APPROVE / APPROVE_WITH_WARNING / ESCALATE / BLOCK)、风险分以及一组人话预警信息,例如「预算占用率已达 104%,进入超支状态,冻结非应急支出」「预算执行严重滞后,请排查项目停滞或采购积压」。
- 预算控制只在事后报表体现:占用率超线往往在月结后才发现,缺乏在单笔支出申请环节实时拦截/升级的能力,钱花出去了才报警。
- 承诺金额容易被遗漏:已签合同、在途采购单虽未付款但实际已占用预算,只统计实际支出会让部门看起来「还很健康」。
- 执行节奏失衡无人发现:支出超前于时间进度隐含年末超支风险,支出严重滞后又可能意味着项目停滞或采购积压,两种偏差都需要主动预警。
- CAPEX 与费用性支出管控口径不同:资本性支出必须先立项,应急支出在红色预算下需要保留通道但要升级,规则分散在各种财务制度里。
- 阈值随管控尺度频繁调整:70%/90%/100% 亮灯线、5% 单笔升级线、±15% 节奏偏差、10% 严重超支线,每年预算政策都可能变动,硬编码维护成本高。
- 风险缺少统一量化口径:财务负责人需要一个跨部门可比的风险分来排优先级,等级、节奏、预测三类信号需要合成一个数值。
| 组成 | 文件 | 职责 |
|---|---|---|
| 入参模型 | model/BudgetStatus.java |
部门预算执行快照与单笔支出申请要素 |
| 决策模型 | model/BudgetAlertDecision.java |
四色等级、占用率、节奏、预测、超支额、处置动作、风险分、预警清单 |
| 规则接口 | BudgetAlertRules.java |
5 个规则方法:分级、节奏、预测、单笔处置、风险分 |
| 规则脚本 | resources/demo/budget-alert.xml |
亮灯线、偏差带、超支线、处置矩阵与分值全部外置 |
| 编排服务 | service/BudgetAlertService.java |
百分比/超支额计算、5% 判定、预警文案组装 |
| 测试 | BudgetAlertServiceTest.java |
6 组部门预算形态,含红色双笔对照 |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 入参(@Param) |
规则口径 | 返回 |
|---|---|---|---|
utilizationLevel |
spent、committed、budget |
占用率(实际 + 承诺)≥100% RED / ≥90% ORANGE / ≥70% YELLOW / 其余 GREEN |
String 等级 |
burnStatus |
spent、budget、month |
支出进度与时间进度偏差超 ±15% 为超前(OVER_BURN)/滞后(UNDER_BURN),否则 NORMAL |
String 节奏 |
forecastStatus |
forecast、budget |
预测 ≥预算×110% SEVERE / >预算 MILD / 其余 OK |
String 预测等级 |
singleExpenseAction |
level、category、emergency、capexApproved、overFivePercent |
CAPEX 未立项 BLOCK、已立项 APPROVE;RED 应急 ESCALATE、非应急 BLOCK;ORANGE 超年度预算 5% ESCALATE、否则警示放行;YELLOW 警示放行;GREEN 放行 | String 处置动作 |
riskScore |
level、burn、forecast |
底分 RED 100 / ORANGE 70 / YELLOW 40 / GREEN 10;OVER_BURN 加 10,SEVERE 加 10 |
int 风险分 |
将下列文件按包结构放入 JQuick-Java 测试工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 demo/service/,XML 放入 src/test/resources/demo/。放入后执行:
mvn test -Dtest=BudgetAlertServiceTestBudgetStatus.java
Section titled “BudgetStatus.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 部门预算执行状态与单笔支出申请 */@Data@NoArgsConstructor@AllArgsConstructorpublic class BudgetStatus {
/** * 部门编码 */ private String deptCode;
/** * 部门名称 */ private String deptName;
/** * 年度预算 */ private double annualBudget;
/** * 实际已支出 */ private double actualSpent;
/** * 已承诺金额(已签合同/在途采购单) */ private double committedAmount;
/** * 当前月份(1-12) */ private int month;
/** * 年末预测支出 */ private double forecastYearEnd;
/** * 本次单笔申请支出金额 */ private double singleExpenseAmount;
/** * 支出类别:CAPEX 资本性 / OPEX 运营 / ADMIN 行政 / MARKETING 市场 */ private String expenseCategory;
/** * 是否应急支出 */ private boolean emergency;
/** * CAPEX 是否已完成立项审批 */ private boolean capexApproved;}BudgetAlertDecision.java
Section titled “BudgetAlertDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 预算预警与单笔支出处置决策 */@Datapublic class BudgetAlertDecision {
/** * 部门编码 */ private String deptCode;
/** * 预算占用率级别:GREEN / YELLOW / ORANGE / RED */ private String utilizationLevel;
/** * 预算占用率(百分比,含承诺金额) */ private int utilizationPercent;
/** * 执行节奏:NORMAL 正常 / OVER_BURN 超前消耗 / UNDER_BURN 执行滞后 */ private String burnStatus;
/** * 年末预测:OK / MILD 轻度超支 / SEVERE 严重超支(≥10%) */ private String forecastStatus;
/** * 预测超支金额 */ private double overrunAmount;
/** * 单笔支出处置:APPROVE 放行 / APPROVE_WITH_WARNING 警示放行 / * ESCALATE 升级审批 / BLOCK 拦截 */ private String singleExpenseAction;
/** * 风险分(0-120) */ private int riskScore;
/** * 预警信息 */ private List<String> alerts = new ArrayList<String>();
public void addAlert(String alert) { this.alerts.add(alert); }}BudgetAlertRules.java
Section titled “BudgetAlertRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 37:预算占用分级、执行节奏、预测超支、单笔处置与风险分 */public interface BudgetAlertRules {
/** * 预算占用率分级(实际+承诺):≥100% RED / ≥90% ORANGE / ≥70% YELLOW / 其余 GREEN */ String utilizationLevel(@Param("spent") double spent, @Param("committed") double committed, @Param("budget") double budget);
/** * 支出进度与时间进度偏差超 ±15% 为超前/滞后 */ String burnStatus(@Param("spent") double spent, @Param("budget") double budget, @Param("month") int month);
/** * 年末预测超支:超预算 10% SEVERE,超预算 MILD,其余 OK */ String forecastStatus(@Param("forecast") double forecast, @Param("budget") double budget);
/** * 单笔支出处置: * CAPEX 未立项 BLOCK;RED 级应急 ESCALATE、非应急 BLOCK; * ORANGE 超年度预算 5% ESCALATE、否则警示放行;YELLOW 警示放行;GREEN 放行 */ String singleExpenseAction(@Param("level") String level, @Param("category") String category, @Param("emergency") boolean emergency, @Param("capexApproved") boolean capexApproved, @Param("overFivePercent") boolean overFivePercent);
/** * 风险分:RED100/ORANGE70/YELLOW40/GREEN10,超前消耗与预测严重超支各加 10 */ int riskScore(@Param("level") String level, @Param("burn") String burn, @Param("forecast") String forecast);}budget-alert.xml
Section titled “budget-alert.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.BudgetAlertRules">
<java name="utilizationLevel" returnClass="java.lang.String"> <![CDATA[ str def utilizationLevel(double:spent, double:committed, double:budget) { double occupied = spent + committed; if (occupied * 100 >= budget * 100) { return "RED"; } else if (occupied * 100 >= budget * 90) { return "ORANGE"; } else if (occupied * 100 >= budget * 70) { return "YELLOW"; } else { return "GREEN"; } } ]]> </java>
<java name="burnStatus" returnClass="java.lang.String"> <![CDATA[ str def burnStatus(double:spent, double:budget, int:month) { double diff = spent * 12 - month * budget; if (diff > budget * 1.8) { return "OVER_BURN"; } else if (diff < budget * -1.8) { return "UNDER_BURN"; } else { return "NORMAL"; } } ]]> </java>
<java name="forecastStatus" returnClass="java.lang.String"> <![CDATA[ str def forecastStatus(double:forecast, double:budget) { if (forecast * 100 >= budget * 110) { return "SEVERE"; } else if (forecast > budget) { return "MILD"; } else { return "OK"; } } ]]> </java>
<java name="singleExpenseAction" returnClass="java.lang.String"> <![CDATA[ str def singleExpenseAction(str:level, str:category, boolean:emergency, boolean:capexApproved, boolean:overFivePercent) { if (category == "CAPEX") { if (capexApproved) { return "APPROVE"; } else { return "BLOCK"; } } else if (level == "RED") { if (emergency) { return "ESCALATE"; } else { return "BLOCK"; } } else if (level == "ORANGE") { if (overFivePercent) { return "ESCALATE"; } else { return "APPROVE_WITH_WARNING"; } } else if (level == "YELLOW") { return "APPROVE_WITH_WARNING"; } else { return "APPROVE"; } } ]]> </java>
<java name="riskScore" returnClass="int"> <![CDATA[ int def riskScore(str:level, str:burn, str:forecast) { int score = 10; if (level == "RED") { score = 100; } else if (level == "ORANGE") { score = 70; } else if (level == "YELLOW") { score = 40; } if (burn == "OVER_BURN") { score = score + 10; } if (forecast == "SEVERE") { score = score + 10; } return score; } ]]> </java>
</javas>BudgetAlertService.java
Section titled “BudgetAlertService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.BudgetAlertRules;import com.github.paohaijiao.demo.model.BudgetAlertDecision;import com.github.paohaijiao.demo.model.BudgetStatus;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 37:部门预算超支预警与单笔支出处置 */public class BudgetAlertService {
private final BudgetAlertRules rules;
public BudgetAlertService(BudgetAlertRules rules) { this.rules = rules; }
public BudgetAlertDecision evaluate(BudgetStatus s) { BudgetAlertDecision d = new BudgetAlertDecision(); d.setDeptCode(s.getDeptCode());
String level = rules.utilizationLevel( s.getActualSpent(), s.getCommittedAmount(), s.getAnnualBudget()); d.setUtilizationLevel(level); int percent = (int) Math.round((s.getActualSpent() + s.getCommittedAmount()) * 100.0 / s.getAnnualBudget()); d.setUtilizationPercent(percent);
String burn = rules.burnStatus(s.getActualSpent(), s.getAnnualBudget(), s.getMonth()); d.setBurnStatus(burn);
String forecast = rules.forecastStatus(s.getForecastYearEnd(), s.getAnnualBudget()); d.setForecastStatus(forecast); double overrun = s.getForecastYearEnd() > s.getAnnualBudget() ? s.getForecastYearEnd() - s.getAnnualBudget() : 0.0; d.setOverrunAmount(BigDecimal.valueOf(overrun).setScale(2, RoundingMode.HALF_UP).doubleValue());
boolean overFivePercent = s.getSingleExpenseAmount() * 100 > s.getAnnualBudget() * 5; String action = rules.singleExpenseAction(level, s.getExpenseCategory(), s.isEmergency(), s.isCapexApproved(), overFivePercent); d.setSingleExpenseAction(action); d.setRiskScore(rules.riskScore(level, burn, forecast));
attachAlerts(s, d, level, percent, burn, forecast, overrun, action); return d; }
private void attachAlerts(BudgetStatus s, BudgetAlertDecision d, String level, int percent, String burn, String forecast, double overrun, String action) { if ("RED".equals(level)) { d.addAlert("预算占用率已达 " + percent + "%,进入超支状态,冻结非应急支出"); } else if ("ORANGE".equals(level)) { d.addAlert("预算占用率 " + percent + "%,超过 90% 橙色预警线"); } else if ("YELLOW".equals(level)) { d.addAlert("预算占用率 " + percent + "%,超过 70% 关注线,请控制支出节奏"); } if ("OVER_BURN".equals(burn)) { d.addAlert("支出进度显著快于时间进度(偏差超 15%),存在年末超支风险"); } else if ("UNDER_BURN".equals(burn)) { d.addAlert("预算执行严重滞后(偏差超 15%),请排查项目停滞或采购积压"); } if ("SEVERE".equals(forecast)) { d.addAlert("年末预测严重超支,超预算金额 " + overrun + " 元(≥10%),须上报预算调整"); } else if ("MILD".equals(forecast)) { d.addAlert("年末预测轻度超支,超预算金额 " + overrun + " 元"); } if ("BLOCK".equals(action)) { if ("CAPEX".equals(s.getExpenseCategory())) { d.addAlert("单笔 " + s.getSingleExpenseAmount() + " 元资本性支出未完成立项,系统拦截"); } else { d.addAlert("预算已超支,单笔 " + s.getSingleExpenseAmount() + " 元非应急支出系统拦截"); } } else if ("ESCALATE".equals(action)) { d.addAlert("单笔 " + s.getSingleExpenseAmount() + " 元支出升级至财务总监/分管领导专项审批"); } }}BudgetAlertServiceTest.java
Section titled “BudgetAlertServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.BudgetAlertDecision;import com.github.paohaijiao.demo.model.BudgetStatus;import com.github.paohaijiao.demo.service.BudgetAlertService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 37:部门预算超支预警 * <p> * 6 组形态:健康预算直接放行、黄色关注轻度超支、橙色超前消耗升级、 * 红色超支拦截与应急升级、已立项资本支出放行、预算执行严重滞后预警。 */public class BudgetAlertServiceTest {
private BudgetAlertService service;
@Before public void setUp() { BudgetAlertRules rules = JQuickJava.create() .createApi(BudgetAlertRules.class, "demo/budget-alert.xml"); this.service = new BudgetAlertService(rules); }
/** * 8 月占用 68%、节奏正常、预测不超支 → GREEN 直接放行 */ @Test public void shouldApproveWhenBudgetHealthy() { BudgetStatus s = base("D001", 1000000, 650000, 30000, 8, 950000); s.setSingleExpenseAmount(20000); s.setExpenseCategory("OPEX");
BudgetAlertDecision d = service.evaluate(s);
Assert.assertEquals("GREEN", d.getUtilizationLevel()); Assert.assertEquals(68, d.getUtilizationPercent()); Assert.assertEquals("NORMAL", d.getBurnStatus()); Assert.assertEquals("OK", d.getForecastStatus()); Assert.assertEquals(0.0, d.getOverrunAmount(), 0.0001); Assert.assertEquals("APPROVE", d.getSingleExpenseAction()); Assert.assertEquals(10, d.getRiskScore()); Assert.assertEquals(0, d.getAlerts().size()); }
/** * 占用 75% YELLOW、预测轻度超支 5 万、单笔 3% → 警示放行 */ @Test public void shouldWarnOnYellowUtilizationAndMildOverrun() { BudgetStatus s = base("D002", 1000000, 700000, 50000, 8, 1050000); s.setSingleExpenseAmount(30000); s.setExpenseCategory("ADMIN");
BudgetAlertDecision d = service.evaluate(s);
Assert.assertEquals("YELLOW", d.getUtilizationLevel()); Assert.assertEquals(75, d.getUtilizationPercent()); Assert.assertEquals("NORMAL", d.getBurnStatus()); Assert.assertEquals("MILD", d.getForecastStatus()); Assert.assertEquals(50000.0, d.getOverrunAmount(), 0.0001); Assert.assertEquals("APPROVE_WITH_WARNING", d.getSingleExpenseAction()); Assert.assertEquals(40, d.getRiskScore()); }
/** * 8 月占用 93% ORANGE、支出超前、预测超支 8 万、单笔 6% → 升级 */ @Test public void shouldEscalateOnOrangeOverBurnLargeExpense() { BudgetStatus s = base("D003", 1000000, 880000, 50000, 8, 1080000); s.setSingleExpenseAmount(60000); s.setExpenseCategory("OPEX");
BudgetAlertDecision d = service.evaluate(s);
Assert.assertEquals("ORANGE", d.getUtilizationLevel()); Assert.assertEquals(93, d.getUtilizationPercent()); Assert.assertEquals("OVER_BURN", d.getBurnStatus()); Assert.assertEquals("MILD", d.getForecastStatus()); Assert.assertEquals(80000.0, d.getOverrunAmount(), 0.0001); Assert.assertEquals("ESCALATE", d.getSingleExpenseAction()); Assert.assertEquals(80, d.getRiskScore()); Assert.assertTrue(join(d).contains("超过 90% 橙色预警线")); }
/** * 9 月占用 104% RED、预测严重超支 15%:未立项 CAPEX 拦截,应急 OPEX 升级 */ @Test public void shouldBlockRedBudgetButEscalateEmergency() { BudgetStatus capex = base("D004", 1000000, 980000, 60000, 9, 1150000); capex.setSingleExpenseAmount(50000); capex.setExpenseCategory("CAPEX"); capex.setCapexApproved(false); BudgetAlertDecision d1 = service.evaluate(capex); Assert.assertEquals("RED", d1.getUtilizationLevel()); Assert.assertEquals(104, d1.getUtilizationPercent()); Assert.assertEquals("OVER_BURN", d1.getBurnStatus()); Assert.assertEquals("SEVERE", d1.getForecastStatus()); Assert.assertEquals(150000.0, d1.getOverrunAmount(), 0.0001); Assert.assertEquals("BLOCK", d1.getSingleExpenseAction()); Assert.assertEquals(120, d1.getRiskScore()); Assert.assertTrue(join(d1).contains("资本性支出未完成立项,系统拦截"));
BudgetStatus emergency = base("D005", 1000000, 980000, 60000, 9, 1150000); emergency.setSingleExpenseAmount(80000); emergency.setExpenseCategory("OPEX"); emergency.setEmergency(true); BudgetAlertDecision d2 = service.evaluate(emergency); Assert.assertEquals("ESCALATE", d2.getSingleExpenseAction()); Assert.assertTrue(join(d2).contains("升级至财务总监/分管领导专项审批")); }
/** * 6 月占用 40%、已立项 CAPEX 10 万 → 放行 */ @Test public void shouldApproveApprovedCapexInHealthyBudget() { BudgetStatus s = base("D006", 500000, 200000, 0, 6, 480000); s.setSingleExpenseAmount(100000); s.setExpenseCategory("CAPEX"); s.setCapexApproved(true);
BudgetAlertDecision d = service.evaluate(s);
Assert.assertEquals("GREEN", d.getUtilizationLevel()); Assert.assertEquals("NORMAL", d.getBurnStatus()); Assert.assertEquals("APPROVE", d.getSingleExpenseAction()); Assert.assertEquals(10, d.getRiskScore()); }
/** * 8 月仅支出 25% → UNDER_BURN 执行滞后预警 */ @Test public void shouldAlertUnderBurnExecutionLag() { BudgetStatus s = base("D007", 800000, 200000, 0, 8, 600000); s.setExpenseCategory("OPEX");
BudgetAlertDecision d = service.evaluate(s);
Assert.assertEquals("GREEN", d.getUtilizationLevel()); Assert.assertEquals("UNDER_BURN", d.getBurnStatus()); Assert.assertTrue(join(d).contains("预算执行严重滞后")); }
private BudgetStatus base(String dept, double budget, double spent, double committed, int month, double forecast) { BudgetStatus s = new BudgetStatus(); s.setDeptCode(dept); s.setDeptName("测试部门"); s.setAnnualBudget(budget); s.setActualSpent(spent); s.setCommittedAmount(committed); s.setMonth(month); s.setForecastYearEnd(forecast); s.setSingleExpenseAmount(0); s.setExpenseCategory("OPEX"); s.setEmergency(false); s.setCapexApproved(false); return s; }
private String join(BudgetAlertDecision d) { StringBuilder sb = new StringBuilder(); for (String a : d.getAlerts()) { sb.append(a).append(';'); } return sb.toString(); }}在工程根目录执行:
mvn test -Dtest=BudgetAlertServiceTest测试覆盖 6 组部门预算形态(红色等级下构造两笔单笔申请对照处置),断言与源码逐一对应:
| 测试方法 | 业务形态 | 关键断言 |
|---|---|---|
shouldApproveWhenBudgetHealthy |
8 月占用 68%、节奏正常、预测 95 万不超支,单笔 2 万 OPEX | GREEN、占用率 68%、NORMAL、OK、超支额 0、APPROVE、风险分 10、预警 0 条 |
shouldWarnOnYellowUtilizationAndMildOverrun |
占用 75% 黄色、预测轻度超支 5 万、单笔占预算 3% | YELLOW、75%、MILD、超支额 50000、APPROVE_WITH_WARNING、风险分 40 |
shouldEscalateOnOrangeOverBurnLargeExpense |
8 月占用 93%、超前消耗、预测超支 8 万、单笔占预算 6% | ORANGE、93%、OVER_BURN、ESCALATE、风险分 80(70+超前 10);预警含「超过 90% 橙色预警线」 |
shouldBlockRedBudgetButEscalateEmergency |
9 月占用 104%、预测严重超支 15%:未立项 CAPEX 与应急 OPEX 两笔 | CAPEX 笔 RED、104%、SEVERE、超支 150000、BLOCK、风险分 120,预警含「资本性支出未完成立项,系统拦截」;应急笔 ESCALATE,预警含「升级至财务总监/分管领导专项审批」 |
shouldApproveApprovedCapexInHealthyBudget |
6 月占用 40%,已立项 CAPEX 10 万 | GREEN、NORMAL、APPROVE、风险分 10 |
shouldAlertUnderBurnExecutionLag |
8 月仅支出 25%,执行严重滞后 | GREEN、UNDER_BURN,预警含「预算执行严重滞后」 |
- 一方法一口径:占用率、执行节奏、年末预测、单笔处置、风险分各为一个独立规则方法,预警链路的每一步都可单独调参、单独回溯,财务口径审计时直接对照 XML 即可。
- 交叉相乘消除浮点口径争议:
utilizationLevel用occupied * 100 >= budget * 100/90/70、forecastStatus用forecast * 100 >= budget * 110表达百分比比较,避免脚本中浮点除法与舍入误差;节奏偏差把年化 ±15% 换算为budget * 1.8的整数倍阈值(12 个月 × 15%)。 - 规则外置热更新:70%/90%/100% 亮灯线、5% 单笔升级线、风险底分全部在 XML 中,预算政策年度调整时改脚本重建代理即可,无需动财务系统主程序。
- Service 金额兜底:超支额非负截断后用
BigDecimal+HALF_UP保留两位小数;「单笔是否超年度预算 5%」在 Java 侧算成布尔再传入规则,规则只做分档映射,不接触金额精度。 - 硬否决与应急通道并存:CAPEX 未立项与红色非应急支出在规则层直接
BLOCK,红色应急支出保留ESCALATE通道,既冻结超支又不停摆救火采购;预警文案由 Service 按命中项逐条组装。 - 代理复用、ASM、Java 8 信创友好:
@Before中一次创建代理全程复用,高频预算校验无重复加载开销;引擎基于动态代理与 ASM、无重型依赖,可嵌入 JDK 8 与国产化环境的财务系统。