跳转到内容

案例 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 组部门预算形态,含红色双笔对照
规则方法 入参(@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/。放入后执行:

Terminal window
mvn test -Dtest=BudgetAlertServiceTest
package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 部门预算执行状态与单笔支出申请
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 预算预警与单笔支出处置决策
*/
@Data
public 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);
}
}
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);
}
<?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>
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()
+ " 元支出升级至财务总监/分管领导专项审批");
}
}
}
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();
}
}

在工程根目录执行:

Terminal window
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 与国产化环境的财务系统。