Case 36: Leave, OT & travel approval routing
This case corresponds to Scenario 36 in USE-CASES.md (Requirement 44: leave/overtime/business-trip approval routing), in the OA collaboration domain. Leave, overtime, and business-trip requests share a single unified entry point. The rule script owns only one concern — “which conditions route to which approver” — while balance checks, certificate gating, countersigning, overseas escalation, and executive peer-approval avoidance are handled in the Service layer as business backstops. The final output includes first/final approver roles, countersign roles, the approval SLA, and rejection reasons.
Industry scenario
Section titled “Industry scenario”Employee approval requests are carried uniformly by ApprovalRequest: the request type approvalType is one of LEAVE, OVERTIME, or TRIP; leave is further classified by leaveType into PERSONAL, SICK, and ANNUAL, with duration measured in hours at 8 hours per day. The request also carries the trip budget amount, the applicant’s level (regular employee / team lead / department manager / executive), whether the overtime is retroactive, whether a hospital certificate is attached for sick leave, whether the annual-leave balance is sufficient, and whether the trip is overseas.
All tiered-approval policies are externalized in the rule script: leave is tiered by leave type — annual leave over 80 hours goes to the VP, over 24 hours to the department manager, otherwise the team lead; sick leave over 40 hours goes to the VP, over 24 hours to the department manager; personal leave over 40 hours goes to the VP, over 8 hours to the department manager. Trips are tiered by budget: above 30,000 to the CEO, above 10,000 to the CFO, above 3,000 to the VP, otherwise the department manager. Overtime escalates to the department manager when it is retroactive or exceeds 16 hours in a single request.
Beyond pure routing, the Service carries the backstop policies of a real OA system: insufficient annual-leave balance is rejected outright; sick leave longer than 3 working days (24 hours) without a hospital certificate is rejected outright; long sick leave and retroactive overtime are countersigned by HR (to disburse sick-pay and re-verify raw attendance records); an overseas trip whose budget only reaches the department-manager tier is escalated to the VP with a note to complete foreign-affairs/security filing; executive requests uniformly lift the final approval to the CEO to avoid peer self-approval. Finally, the approval SLA is derived from the final approver role (CEO 48 / CFO 36 / VP 24 / department manager 16 / team lead 8 hours).
The decision result ApprovalRoutingDecision records the request ID, status (ROUTED for routed and pending approval / REJECTED for returned for correction), first and final approver roles, the countersign role list, the approval SLA, routing notes, and the rejection reason — ready to drive OA workflow node transitions and message notifications.
Business pain points
Section titled “Business pain points”- Many fine-grained tier thresholds: the three leave types each have their own hour thresholds (80/24, 40/24, 40/8), and trips add three amount lines (30,000 / 10,000 / 3,000). Hard-coded in workflow code, these are extremely error-prone and hard to audit.
- Exception policies tangled with routing: sick-leave certificate gating, annual-leave balance checks, HR countersigning for long sick leave, retroactive-overtime re-verification, and overseas escalation filing are mixed into the same workflow node as the tiering logic — changing one risks breaking the rest.
- Compliance recusal is hard to enforce: executive requests must not be self-approved by peers; final approval must be lifted to the CEO. Such organizational recusal rules are scattered across workflow configurations and are often missed.
- Inconsistent approval SLAs: different final approver roles map to different SLAs; adjusting a time limit (e.g., VP from 24 to 12 hours) requires a code release.
- Retroactive overtime opens room for attendance fraud: overtime submitted more than 3 calendar days late requires higher-level approval plus HR re-verification of raw records; normal and retroactive submissions must be routed separately.
- Policy changes depend on IT scheduling: labor policies and travel standards may change yearly; business departments cannot adjust thresholds themselves, so decoupling rules from code is a strong demand.
Solution architecture
Section titled “Solution architecture”| Component | File | Responsibility |
|---|---|---|
| Input model | model/ApprovalRequest.java |
Unified leave/overtime/trip request fields: type, leave type, hours, budget, level, and gating flags |
| Decision model | model/ApprovalRoutingDecision.java |
Routing status, first/final approver roles, countersign list, SLA, routing notes, and rejection reason |
| Rule interface | ApprovalRoutingRules.java |
5 rule methods: approver tiering for the three request types, sick-leave certificate gating, role SLA |
| Rule script | resources/demo/approval-routing.xml |
Leave-type hour tiers, budget tiers, overtime retroactivity tier, and SLA limits all externalized |
| Orchestration service | service/ApprovalRoutingService.java |
Balance/certificate hard vetoes, countersigning, overseas escalation, executive lift, SLA back-calculation |
| Tests | ApprovalRoutingServiceTest.java |
6 real business shapes, 11 requests covering tiering, rejection, countersigning, and escalation |
Rule method breakdown
Section titled “Rule method breakdown”| Rule method | Inputs (@Param) |
Policy | Returns |
|---|---|---|---|
leaveApprover |
leaveType, hours |
Annual >80h VP / >24h department manager, else team lead; sick >40h VP / >24h department manager, else team lead; personal >40h VP / >8h department manager, else team lead |
String approver role |
tripApprover |
amount |
Trip budget ≥30000 CEO / ≥10000 CFO / ≥3000 VP / else department manager |
String approver role |
overtimeApprover |
hours, retroactive |
Retroactive or over 16 hours → department manager, else team lead | String approver role |
sickCertificateRequired |
hours |
Sick leave over 3 days (24 hours) requires a hospital certificate | boolean |
slaHours |
role |
Approval SLA by final approver role: CEO 48 / CFO 36 / VP 24 / department manager 16 / others 8 hours | int |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the following files into the JQuick-Java test project by package structure: the two models into src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test into src/test/java/com/github/paohaijiao/demo/, the Service into demo/service/, and the XML into src/test/resources/demo/. Then run:
mvn test -Dtest=ApprovalRoutingServiceTestApprovalRequest.java
Section titled “ApprovalRequest.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 员工审批申请要素(请假/加班/出差统一入口) */@Data@NoArgsConstructor@AllArgsConstructorpublic class ApprovalRequest {
/** * 申请单号 */ private String requestId;
private String employeeId; private String employeeName;
/** * 申请类型:LEAVE 请假 / OVERTIME 加班 / TRIP 出差 */ private String approvalType;
/** * 请假类型:PERSONAL 事假 / SICK 病假 / ANNUAL 年假 */ private String leaveType;
/** * 申请时长(小时,请假按每天 8 小时折算) */ private double hours;
/** * 出差预算金额(元) */ private double tripAmount;
/** * 申请人职级:REGULAR 普通员工 / TEAM_LEAD 主管 / DEPT_MANAGER 部门经理 / EXECUTIVE 高管 */ private String employeeLevel;
/** * 加班是否为跨期补录(超过 3 个自然日提交) */ private boolean retroactiveOvertime;
/** * 病假是否附医院证明 */ private boolean sickCertificate;
/** * 年假余额是否充足 */ private boolean annualLeaveEnough;
/** * 是否境外出差 */ private boolean overseasTrip;}ApprovalRoutingDecision.java
Section titled “ApprovalRoutingDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 审批路由决策 */@Datapublic class ApprovalRoutingDecision {
/** * 申请单号 */ private String requestId;
/** * ROUTED 已路由待审批 / REJECTED 退回补正 */ private String status;
/** * 首审角色:TEAM_LEAD / DEPT_MANAGER / VP / CFO / CEO */ private String firstApproverRole;
/** * 终审角色 */ private String finalApproverRole;
/** * 加签角色列表(HR / VP / FINANCE 等) */ private List<String> countersignRoles = new ArrayList<String>();
/** * 审批时限(小时) */ private int slaHours;
/** * 路由说明 */ private List<String> routeNotes = new ArrayList<String>();
/** * 退回原因 */ private String rejectReason;
public void addCountersign(String role) { this.countersignRoles.add(role); }
public void addNote(String note) { this.routeNotes.add(note); }}ApprovalRoutingRules.java
Section titled “ApprovalRoutingRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 36:请假/加班/出差审批分级路由规则 */public interface ApprovalRoutingRules {
/** * 请假分级审批人: * 年假 >80h VP / >24h 部门经理,其余主管; * 病假 >40h VP / >24h 部门经理,其余主管; * 事假 >40h VP / >8h 部门经理,其余主管 */ String leaveApprover(@Param("leaveType") String leaveType, @Param("hours") double hours);
/** * 出差预算分级审批人:≥3 万 CEO / ≥1 万 CFO / ≥3000 VP / 其余部门经理 */ String tripApprover(@Param("amount") double amount);
/** * 加班审批人:跨期补录或超 16 小时 → 部门经理,其余主管 */ String overtimeApprover(@Param("hours") double hours, @Param("retroactive") boolean retroactive);
/** * 病假超 3 天(24 小时)须医院证明 */ boolean sickCertificateRequired(@Param("hours") double hours);
/** * 按终审角色返回审批时限(小时) */ int slaHours(@Param("role") String role);}approval-routing.xml
Section titled “approval-routing.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.ApprovalRoutingRules">
<java name="leaveApprover" returnClass="java.lang.String"> <![CDATA[ str def leaveApprover(str:leaveType, double:hours) { if (leaveType == "ANNUAL") { if (hours > 80.0) { return "VP"; } else if (hours > 24.0) { return "DEPT_MANAGER"; } else { return "TEAM_LEAD"; } } else if (leaveType == "SICK") { if (hours > 40.0) { return "VP"; } else if (hours > 24.0) { return "DEPT_MANAGER"; } else { return "TEAM_LEAD"; } } else { if (hours > 40.0) { return "VP"; } else if (hours > 8.0) { return "DEPT_MANAGER"; } else { return "TEAM_LEAD"; } } } ]]> </java>
<java name="tripApprover" returnClass="java.lang.String"> <![CDATA[ str def tripApprover(double:amount) { if (amount >= 30000.0) { return "CEO"; } else if (amount >= 10000.0) { return "CFO"; } else if (amount >= 3000.0) { return "VP"; } else { return "DEPT_MANAGER"; } } ]]> </java>
<java name="overtimeApprover" returnClass="java.lang.String"> <![CDATA[ str def overtimeApprover(double:hours, boolean:retroactive) { if (retroactive) { return "DEPT_MANAGER"; } else if (hours > 16.0) { return "DEPT_MANAGER"; } else { return "TEAM_LEAD"; } } ]]> </java>
<java name="sickCertificateRequired" returnClass="boolean"> <![CDATA[ boolean def sickCertificateRequired(double:hours) { if (hours > 24.0) { return true; } else { return false; } } ]]> </java>
<java name="slaHours" returnClass="int"> <![CDATA[ int def slaHours(str:role) { if (role == "CEO") { return 48; } else if (role == "CFO") { return 36; } else if (role == "VP") { return 24; } else if (role == "DEPT_MANAGER") { return 16; } else { return 8; } } ]]> </java>
</javas>ApprovalRoutingService.java
Section titled “ApprovalRoutingService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.ApprovalRoutingRules;import com.github.paohaijiao.demo.model.ApprovalRequest;import com.github.paohaijiao.demo.model.ApprovalRoutingDecision;
/** * 场景 36:请假/加班/出差审批智能流转 */public class ApprovalRoutingService {
private final ApprovalRoutingRules rules;
public ApprovalRoutingService(ApprovalRoutingRules rules) { this.rules = rules; }
public ApprovalRoutingDecision route(ApprovalRequest req) { ApprovalRoutingDecision d = new ApprovalRoutingDecision(); d.setRequestId(req.getRequestId());
if ("LEAVE".equals(req.getApprovalType())) { if (!routeLeave(req, d)) { return d; } } else if ("OVERTIME".equals(req.getApprovalType())) { routeOvertime(req, d); } else { routeTrip(req, d); }
applyExecutiveEscalation(req, d); d.setSlaHours(rules.slaHours(d.getFinalApproverRole())); d.setStatus("ROUTED"); return d; }
private boolean routeLeave(ApprovalRequest req, ApprovalRoutingDecision d) { if ("ANNUAL".equals(req.getLeaveType()) && !req.isAnnualLeaveEnough()) { d.setStatus("REJECTED"); d.setRejectReason("年假余额不足,请核减假期或改走事假流程"); return false; } if ("SICK".equals(req.getLeaveType()) && rules.sickCertificateRequired(req.getHours()) && !req.isSickCertificate()) { d.setStatus("REJECTED"); d.setRejectReason("病假超过 3 个工作日(24 小时),须上传医院诊断证明后再提交"); return false; } String approver = rules.leaveApprover(req.getLeaveType(), req.getHours()); d.setFirstApproverRole(approver); d.setFinalApproverRole(approver); if ("SICK".equals(req.getLeaveType()) && rules.sickCertificateRequired(req.getHours())) { d.addCountersign("HR"); d.addNote("长病假由 " + approver + " 审批并加签 HR 核发病假工资"); } return true; }
private void routeOvertime(ApprovalRequest req, ApprovalRoutingDecision d) { String approver = rules.overtimeApprover(req.getHours(), req.isRetroactiveOvertime()); d.setFirstApproverRole(approver); d.setFinalApproverRole(approver); if (req.isRetroactiveOvertime()) { d.addCountersign("HR"); d.addNote("跨期补录加班由 " + approver + " 审批并加签 HR 复核考勤原始记录"); } }
private void routeTrip(ApprovalRequest req, ApprovalRoutingDecision d) { String approver = rules.tripApprover(req.getTripAmount()); if (req.isOverseasTrip() && "DEPT_MANAGER".equals(approver)) { approver = "VP"; d.addNote("境外出差预算审批由部门经理升级至 VP"); } d.setFirstApproverRole(approver); d.setFinalApproverRole(approver); if (req.isOverseasTrip() && !"DEPT_MANAGER".equals(approver)) { d.addNote("境外出差须同步完成外事/安全审批备案"); } }
private void applyExecutiveEscalation(ApprovalRequest req, ApprovalRoutingDecision d) { if ("EXECUTIVE".equals(req.getEmployeeLevel()) && !"CEO".equals(d.getFinalApproverRole())) { d.addNote("高管申请规避同级自审,终审由 " + d.getFinalApproverRole() + " 上提至 CEO"); d.setFinalApproverRole("CEO"); } }}ApprovalRoutingServiceTest.java
Section titled “ApprovalRoutingServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.ApprovalRequest;import com.github.paohaijiao.demo.model.ApprovalRoutingDecision;import com.github.paohaijiao.demo.service.ApprovalRoutingService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 36:请假/加班/出差审批流转 * <p> * 6 组形态:短时事假主管直审、年假分级与余额退回、长病假证明门控与 HR 加签、 * 加班分级与跨期补录、出差预算与境外升级、大额差旅与高管同级规避。 */public class ApprovalRoutingServiceTest {
private ApprovalRoutingService service;
@Before public void setUp() { ApprovalRoutingRules rules = JQuickJava.create() .createApi(ApprovalRoutingRules.class, "demo/approval-routing.xml"); this.service = new ApprovalRoutingService(rules); }
/** * 普通员工 4 小时事假 → 主管 8 小时内审结 */ @Test public void shouldRouteShortPersonalLeaveToTeamLead() { ApprovalRequest req = leaveReq("A001", "PERSONAL", 4.0, "REGULAR");
ApprovalRoutingDecision d = service.route(req);
Assert.assertEquals("ROUTED", d.getStatus()); Assert.assertEquals("TEAM_LEAD", d.getFirstApproverRole()); Assert.assertEquals("TEAM_LEAD", d.getFinalApproverRole()); Assert.assertEquals(8, d.getSlaHours()); Assert.assertEquals(0, d.getCountersignRoles().size()); }
/** * 6 天年假余额足 → 部门经理;余额不足 → 退回 */ @Test public void shouldRouteAnnualLeaveByDaysAndRejectInsufficientBalance() { ApprovalRequest enough = leaveReq("A002", "ANNUAL", 48.0, "REGULAR"); enough.setAnnualLeaveEnough(true); ApprovalRoutingDecision d1 = service.route(enough); Assert.assertEquals("ROUTED", d1.getStatus()); Assert.assertEquals("DEPT_MANAGER", d1.getFinalApproverRole()); Assert.assertEquals(16, d1.getSlaHours());
ApprovalRequest shortBalance = leaveReq("A003", "ANNUAL", 48.0, "REGULAR"); shortBalance.setAnnualLeaveEnough(false); ApprovalRoutingDecision d2 = service.route(shortBalance); Assert.assertEquals("REJECTED", d2.getStatus()); Assert.assertTrue(d2.getRejectReason().contains("年假余额不足")); }
/** * 6 天病假有证明 → VP 审批并加签 HR;无证明 → 退回补证 */ @Test public void shouldRequireCertificateAndCountersignHrForLongSickLeave() { ApprovalRequest certified = leaveReq("A004", "SICK", 48.0, "REGULAR"); certified.setSickCertificate(true); ApprovalRoutingDecision d1 = service.route(certified); Assert.assertEquals("VP", d1.getFinalApproverRole()); Assert.assertEquals(24, d1.getSlaHours()); Assert.assertTrue(d1.getCountersignRoles().contains("HR")); Assert.assertTrue(d1.getRouteNotes().get(0).contains("加签 HR 核发病假工资"));
ApprovalRequest noProof = leaveReq("A005", "SICK", 48.0, "REGULAR"); noProof.setSickCertificate(false); ApprovalRoutingDecision d2 = service.route(noProof); Assert.assertEquals("REJECTED", d2.getStatus()); Assert.assertTrue(d2.getRejectReason().contains("医院诊断证明")); }
/** * 20 小时常规加班 → 部门经理;8 小时跨期补录 → 部门经理加签 HR */ @Test public void shouldRouteOvertimeByHoursAndRetroactiveFlag() { ApprovalRequest normal = base("A006", "OVERTIME", "REGULAR"); normal.setHours(20); ApprovalRoutingDecision d1 = service.route(normal); Assert.assertEquals("DEPT_MANAGER", d1.getFinalApproverRole()); Assert.assertEquals(0, d1.getCountersignRoles().size());
ApprovalRequest retro = base("A007", "OVERTIME", "REGULAR"); retro.setHours(8); retro.setRetroactiveOvertime(true); ApprovalRoutingDecision d2 = service.route(retro); Assert.assertEquals("DEPT_MANAGER", d2.getFinalApproverRole()); Assert.assertTrue(d2.getCountersignRoles().contains("HR")); Assert.assertTrue(d2.getRouteNotes().get(0).contains("加签 HR 复核考勤原始记录")); }
/** * 境内 8000 元出差 → VP;2000 元境外出差 → 升级 VP */ @Test public void shouldRouteTripByBudgetAndEscalateOverseas() { ApprovalRequest domestic = base("A008", "TRIP", "REGULAR"); domestic.setTripAmount(8000); ApprovalRoutingDecision d1 = service.route(domestic); Assert.assertEquals("VP", d1.getFinalApproverRole()); Assert.assertEquals(24, d1.getSlaHours());
ApprovalRequest overseas = base("A009", "TRIP", "REGULAR"); overseas.setTripAmount(2000); overseas.setOverseasTrip(true); ApprovalRoutingDecision d2 = service.route(overseas); Assert.assertEquals("VP", d2.getFinalApproverRole()); Assert.assertTrue(d2.getRouteNotes().get(0).contains("境外出差预算审批由部门经理升级至 VP")); }
/** * 5 万差旅 → CEO;高管 1.2 万差旅(本应 CFO)→ 上提 CEO 规避自审 */ @Test public void shouldRouteLargeBudgetToCeoAndEscalateExecutive() { ApprovalRequest large = base("A010", "TRIP", "REGULAR"); large.setTripAmount(50000); ApprovalRoutingDecision d1 = service.route(large); Assert.assertEquals("CEO", d1.getFinalApproverRole()); Assert.assertEquals(48, d1.getSlaHours());
ApprovalRequest exec = base("A011", "TRIP", "EXECUTIVE"); exec.setTripAmount(12000); ApprovalRoutingDecision d2 = service.route(exec); Assert.assertEquals("CEO", d2.getFinalApproverRole()); Assert.assertTrue(d2.getRouteNotes().get(0).contains("上提至 CEO")); }
private ApprovalRequest leaveReq(String id, String leaveType, double hours, String level) { ApprovalRequest r = base(id, "LEAVE", level); r.setLeaveType(leaveType); r.setHours(hours); r.setSickCertificate(false); r.setAnnualLeaveEnough(false); return r; }
private ApprovalRequest base(String id, String type, String level) { ApprovalRequest r = new ApprovalRequest(); r.setRequestId(id); r.setEmployeeId("E-" + id); r.setEmployeeName("测试员工"); r.setApprovalType(type); r.setLeaveType("PERSONAL"); r.setHours(0); r.setTripAmount(0); r.setEmployeeLevel(level); r.setRetroactiveOvertime(false); r.setSickCertificate(false); r.setAnnualLeaveEnough(false); r.setOverseasTrip(false); return r; }}Run & verified results
Section titled “Run & verified results”Run at the project root:
mvn test -Dtest=ApprovalRoutingServiceTestThe tests cover 6 business shapes with 11 requests in total; the assertions correspond one-to-one with the source code:
| Test method | Business shape | Key assertions |
|---|---|---|
shouldRouteShortPersonalLeaveToTeamLead |
Regular employee, 4-hour personal leave, direct team-lead approval | Status ROUTED; first/final approver both TEAM_LEAD; SLA 8 hours; 0 countersigns |
shouldRouteAnnualLeaveByDaysAndRejectInsufficientBalance |
48-hour (6-day) annual leave: one with sufficient balance, one without | Sufficient balance → final approver DEPT_MANAGER, SLA 16; insufficient balance → status REJECTED, reason contains “年假余额不足” |
shouldRequireCertificateAndCountersignHrForLongSickLeave |
48-hour sick leave: one with certificate, one without | With certificate → final approver VP, SLA 24, countersign includes HR, note contains “加签 HR 核发病假工资”; without certificate → REJECTED, reason contains “医院诊断证明” |
shouldRouteOvertimeByHoursAndRetroactiveFlag |
20-hour regular overtime and 8-hour retroactive overtime | Regular overtime → final approver DEPT_MANAGER, no countersign; retroactive → final approver DEPT_MANAGER with HR countersign, note contains “加签 HR 复核考勤原始记录” |
shouldRouteTripByBudgetAndEscalateOverseas |
Domestic 8,000 trip and overseas 2,000 trip | Domestic → final approver VP, SLA 24; overseas low-budget escalated from department manager to VP, note contains “境外出差预算审批由部门经理升级至 VP” |
shouldRouteLargeBudgetToCeoAndEscalateExecutive |
50,000 large trip and an executive’s 12,000 trip | 50,000 → final approver CEO, SLA 48; executive’s 12,000 (would be CFO) lifted to CEO, note contains “上提至 CEO” |
Technical highlights
Section titled “Technical highlights”- One method, one policy — all thresholds externalized: the 5 rule methods each answer exactly one question (leave approver, trip approver, overtime approver, whether a sick-leave certificate is required, role SLA). Leave-type hour tiers, budget tiers, and SLA limits all live in the XML — policy changes take effect by editing the XML and rebuilding the proxy, with no recompilation of business code.
- Service-side hard-veto short circuit: insufficient annual-leave balance and long sick leave without a certificate are marked
REJECTEDand returned before any tiering rule is invoked — invalid requests never enter the approval route, and rejection reasons are precise and explainable. - Service-side escalation backstops: the “department-manager tier escalated to VP” rule for overseas trips and the “final approval always lifted to CEO” rule for executives are organizational compliance policies, applied in the Java orchestration layer as a second-pass correction over rule results, keeping the rule script purely about amounts and hours.
- Countersigning linked with SLA: long sick leave and retroactive overtime automatically add an HR countersign and generate routing notes; once the final approver role is rewritten by escalation logic, the SLA is immediately recalculated for the new role, keeping the time limit consistent with the actual final approver.
@Paramname binding: interface parameters are injected into the script by name via@Param("leaveType")etc., so input-model evolution does not affect the script; string comparisons in the script use==directly, and boolean decisions use positive-condition earlyreturn(as insickCertificateRequired), fitting the engine’s syntax constraints.- Proxy reuse, ASM acceleration, Java 8 / domestic-JDK friendly: the test creates the
ApprovalRoutingRulesproxy once in@Beforeand reuses it; the dynamic-proxy + ASM invocation chain has no heavy dependencies and can embed directly into OA systems still running on JDK 8 or domestic (Xinchuang) JDKs.