Skip to content

Case 5: Ticket SLA grading & escalation

This case corresponds to Scenario 5: Ticket SLA grading & escalation (production-grade template) in the JQuick-Java repository’s USE-CASES.md, in the operations & customer service domain. It manages SLA compliance for help-desk and ops tickets: assemble first-response, resolution, and escalation deadline matrices by priority, customer tier, and security attributes, then compute a breach risk score from reopen count, assignee load, and history, and decide whether to auto-escalate or require manager involvement.

SLA (service-level agreement) is the promise of response timeliness that an enterprise makes for customer tickets, widely used in contact centers, IT service desks, and ops duty systems. Tickets are classified by type as incident (INCIDENT), request (REQUEST), problem (PROBLEM), or change (CHANGE), and each ticket carries fields such as priority (P0–P3), customer tier (VIP/GOLD/NORMAL), source channel, whether it is security-related, reopen count, current assignee open-ticket count, whether there is a history of SLA breach, and elapsed minutes.

The decision first determines the service level: P0 tickets or VIP customers get the VIP level directly, P1 gets GOLD, and the rest get STANDARD. Then a “priority × service level” two-dimensional matrix assembles three deadline tiers: first-response deadlines for P0/P1/P2/others are 10/20/60/240 minutes respectively, with security-related P0/P1 tightened further to 5 minutes; resolution deadlines under the VIP matrix are 120 minutes for P0, 240 for P1, and 1440 for P2, doubled for non-VIP; escalation deadlines are likewise read from the dual matrix (only 10 minutes for VIP P0).

Running in parallel is breach risk prediction. The risk score is built from three leading factors: priority weight (P0 40 / P1 30 / P2 15 / P3 5), reopen risk (12 points per reopen, capped at 36), and assignee load (20/12/6 points for open tickets ≥20/≥12/≥6). A risk score ≥60 or a history of SLA breach marks HIGH; ≥35 marks MEDIUM; otherwise LOW — a historical breach is a hard lift regardless of score, so a 48-point ticket is still HIGH.

The final output includes the service level, the three deadline tiers (first response, resolution, escalation), the composite risk score and breach risk level, whether auto-escalation is triggered (elapsed time reached the escalation deadline), whether manager involvement is needed (P0, VIP customer, or security-related ticket), and a handling-action suggestion list: for security-related tickets, synchronously notify the security duty officer and emergency group; for P0, immediately start a group chat and notify the duty manager; for expired tickets, auto-escalate; for reopen counts ≥2, suggest a root-cause review; for assignee open tickets ≥12, suggest reassignment or reinforcement — ready for consumption by ticket dispatch and notification systems.

  • Complex, hard-coded deadline matrices: 4 priorities × 3 customer tiers × 3 deadline types (first response / resolution / escalation), plus security-related tightening rules, are scattered across ticket-system timers, notification scripts, and shift schedules — hard to maintain and easy to misconfigure.
  • Differentiated SLA commitments that change frequently: large customer contracts negotiate shorter response deadlines; during peak protection periods and promotions, P0/P1 deadlines are tightened temporarily; with hard coding, every adjustment requires a ticket-system release.
  • Breach can only be judged after the fact, with no early warning: the traditional approach only alerts when a ticket actually expires; leading risk indicators such as reopen count and assignee load are not part of the decision, so by the time a breach occurs the customer has already complained.
  • Escalation and manager involvement rely on manual screen-watching: the duty lead decides by experience which tickets to escalate and which to report to a manager; P0 and security incidents are easy to miss during night shifts or peak hours, and a 10-minute escalation window is fleeting.
  • Coupled risk factors resist independent tuning: priority weights, reopen deductions, load thresholds, and historical-breach hard lifts are written together; adjusting any factor requires regressing the whole pipeline, and no one can decompose how a HIGH-risk ticket’s score was built.
  • Legacy ITSM and domestic (Xinchuang) constraints: many contact-center and ITSM systems still run on JDK 8 or domestic JDKs, making it hard to introduce a separately deployed heavy service-management platform.

This case uses the six-file structure of “domain model + rule interface & XML rule script + orchestration service + tests”: service-level grading, dual deadline matrices, risk weights, and thresholds are all externalized in XML, while the Java side assembles the deadlines, aggregates the risk score, makes escalation decisions, and assembles actions.

Layer Responsibility Files
Domain model Ticket snapshot, SLA decision result Ticket.java, SlaDecision.java
Rule set & XML Service-level grading, deadline matrices, risk factors, escalation and manager decisions TicketSlaRules.java, ticket-sla.xml
Orchestration service Deadline matrix assembly, risk-score aggregation, escalation decision, action assembly TicketSlaService.java
Tests 6 groups of ticket business shapes TicketSlaServiceTest.java

The rule interface has 10 methods: 3 level/weight mappings, 3 deadline matrix lookups, 3 risk scoring methods, and 2 escalation decisions:

Rule method Policy Score / output
slaLevel P0 or VIP customer → VIP; P1 → GOLD; otherwise STANDARD VIP / GOLD / STANDARD
responseMinutes P0 10 / P1 20 / P2 60 / others 240 minutes; security-related P0/P1 tightened to 5 minutes 5 ~ 240 minutes
resolveMinutes VIP / non-VIP × priority dual matrix: P0 120/240, P1 240/480, P2 1440/2880, others 4320/7200 Minutes
escalateMinutes VIP / non-VIP × priority dual matrix: P0 10/30, P1 30/60, P2 120/240, others 720/1440 Minutes
priorityWeight P0 40 / P1 30 / P2 15 / P3 5 5 ~ 40
scoreReopen 12 points per reopen, capped at 36 0 ~ 36
scoreLoad Assignee open tickets ≥20/≥12/≥6 → 20/12/6, otherwise 0 0 ~ 20
breachRisk Historical breach or risk score ≥60 → HIGH; ≥35 → MEDIUM; otherwise LOW HIGH / MEDIUM / LOW
needEscalate Escalation triggered when elapsed time reaches the escalation deadline (P0/security deadlines are only 5–10 minutes, effectively immediate) boolean
needManager P0, VIP customer, or security-related ticket requires manager involvement boolean

Place the six files into the corresponding src/test directories of a Maven project following the original package structure: model classes Ticket.java and SlaDecision.java under com.github.paohaijiao.demo.model; the rule interface TicketSlaRules.java and test class TicketSlaServiceTest.java under com.github.paohaijiao.demo; the orchestration service TicketSlaService.java under com.github.paohaijiao.demo.service; and the rule script ticket-sla.xml under src/test/resources/demo/. Keep the package names exactly as in the source; the model classes depend on Lombok’s @Data annotation. Run mvn test -Dtest=TicketSlaServiceTest at the project root to execute all 6 test cases.

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 客服工单要素(SLA 分级与升级所需的工单快照)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Ticket {
/**
* 工单号
*/
private String ticketNo;
/**
* 工单标题
*/
private String title;
/**
* 优先级:P0-最高 / P1 / P2 / P3
*/
private String priority;
/**
* 工单类型:INCIDENT-故障 / REQUEST-请求 / PROBLEM-问题 / CHANGE-变更
*/
private String category;
/**
* 客户等级:VIP / GOLD / NORMAL
*/
private String customerLevel;
/**
* 来源渠道:APP / WEB / PHONE
*/
private String createChannel;
/**
* 是否安全类工单
*/
private boolean securityRelated;
/**
* 重开次数
*/
private int reopenedTimes;
/**
* 处理人当前在办工单数
*/
private int assigneeLoad;
/**
* 是否历史发生过 SLA 违约
*/
private boolean hasSlaBreachHistory;
/**
* 已耗时(分钟)
*/
private int elapsedMinutes;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 工单 SLA 决策结果
*/
@Data
public class SlaDecision {
/**
* 工单号
*/
private String ticketNo;
/**
* 服务水平等级:VIP / GOLD / STANDARD
*/
private String slaLevel;
/**
* 首响时限(分钟)
*/
private int responseMinutes;
/**
* 解决时限(分钟)
*/
private int resolveMinutes;
/**
* 升级时限(分钟)
*/
private int escalateMinutes;
/**
* 综合风险分
*/
private int riskScore;
/**
* 违约风险:HIGH / MEDIUM / LOW
*/
private String breachRisk;
/**
* 是否触发升级
*/
private boolean needEscalate;
/**
* 是否需要经理介入
*/
private boolean needManager;
/**
* 处理动作建议
*/
private List<String> actions = new ArrayList<String>();
public void addAction(String action) {
this.actions.add(action);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 5(生产级样板):工单 SLA 分级与升级规则集
* <p>
* 规则按「服务水平分级 + 时限矩阵 + 违约风险」拆分:
* <ul>
* <li>时限类:首响、解决、升级三档时限,按优先级与客户等级取值</li>
* <li>风险类:优先级权重、重开工单、处理人负载共同决定违约风险</li>
* </ul>
* 规则脚本见 {@code src/test/resources/demo/ticket-sla.xml}。
*/
public interface TicketSlaRules {
/**
* 服务水平等级:VIP / GOLD / STANDARD
*/
String slaLevel(@Param("priority") String priority,
@Param("customerLevel") String customerLevel);
/**
* 首次响应时限(分钟)
*/
int responseMinutes(@Param("priority") String priority,
@Param("securityRelated") boolean securityRelated);
/**
* 解决时限(分钟)
*/
int resolveMinutes(@Param("priority") String priority,
@Param("slaLevel") String slaLevel);
/**
* 升级时限(分钟)
*/
int escalateMinutes(@Param("priority") String priority,
@Param("slaLevel") String slaLevel);
/**
* 优先级权重
*/
int priorityWeight(@Param("priority") String priority);
/**
* 重开工单风险分(每次 12 分,封顶 36)
*/
int scoreReopen(@Param("reopenedTimes") int reopenedTimes);
/**
* 处理人负载风险分
*/
int scoreLoad(@Param("assigneeLoad") int assigneeLoad);
/**
* 违约风险:HIGH / MEDIUM / LOW
*/
String breachRisk(@Param("riskScore") int riskScore,
@Param("hasSlaBreachHistory") boolean hasSlaBreachHistory);
/**
* 是否触发升级
*/
boolean needEscalate(@Param("elapsedMinutes") int elapsedMinutes,
@Param("escalateMinutes") int escalateMinutes);
/**
* 是否需要经理介入
*/
boolean needManager(@Param("priority") String priority,
@Param("customerLevel") String customerLevel,
@Param("securityRelated") boolean securityRelated);
}
<?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.TicketSlaRules">
<!-- 服务水平等级 -->
<java name="slaLevel" returnClass="java.lang.String">
<![CDATA[
str def slaLevel(str:priority, str:customerLevel) {
if (priority == "P0" || customerLevel == "VIP") {
return "VIP";
} else if (priority == "P1") {
return "GOLD";
} else {
return "STANDARD";
}
}
]]>
</java>
<!-- 首次响应时限 -->
<java name="responseMinutes" returnClass="int">
<![CDATA[
int def responseMinutes(str:priority, boolean:securityRelated) {
int minutes = 240;
if (priority == "P0") {
minutes = 10;
} else if (priority == "P1") {
minutes = 20;
} else if (priority == "P2") {
minutes = 60;
}
if (securityRelated && (priority == "P0" || priority == "P1")) {
minutes = 5;
}
return minutes;
}
]]>
</java>
<!-- 解决时限 -->
<java name="resolveMinutes" returnClass="int">
<![CDATA[
int def resolveMinutes(str:priority, str:slaLevel) {
if (slaLevel == "VIP") {
if (priority == "P0") {
return 120;
} else if (priority == "P1") {
return 240;
} else if (priority == "P2") {
return 1440;
} else {
return 4320;
}
} else {
if (priority == "P0") {
return 240;
} else if (priority == "P1") {
return 480;
} else if (priority == "P2") {
return 2880;
} else {
return 7200;
}
}
}
]]>
</java>
<!-- 升级时限 -->
<java name="escalateMinutes" returnClass="int">
<![CDATA[
int def escalateMinutes(str:priority, str:slaLevel) {
if (slaLevel == "VIP") {
if (priority == "P0") {
return 10;
} else if (priority == "P1") {
return 30;
} else if (priority == "P2") {
return 120;
} else {
return 720;
}
} else {
if (priority == "P0") {
return 30;
} else if (priority == "P1") {
return 60;
} else if (priority == "P2") {
return 240;
} else {
return 1440;
}
}
}
]]>
</java>
<!-- 优先级权重 -->
<java name="priorityWeight" returnClass="int">
<![CDATA[
int def priorityWeight(str:priority) {
if (priority == "P0") {
return 40;
} else if (priority == "P1") {
return 30;
} else if (priority == "P2") {
return 15;
} else {
return 5;
}
}
]]>
</java>
<!-- 重开工单风险分 -->
<java name="scoreReopen" returnClass="int">
<![CDATA[
int def scoreReopen(int:reopenedTimes) {
int score = reopenedTimes * 12;
if (score > 36) {
score = 36;
}
return score;
}
]]>
</java>
<!-- 处理人负载风险分 -->
<java name="scoreLoad" returnClass="int">
<![CDATA[
int def scoreLoad(int:assigneeLoad) {
if (assigneeLoad >= 20) {
return 20;
} else if (assigneeLoad >= 12) {
return 12;
} else if (assigneeLoad >= 6) {
return 6;
} else {
return 0;
}
}
]]>
</java>
<!-- 违约风险 -->
<java name="breachRisk" returnClass="java.lang.String">
<![CDATA[
str def breachRisk(int:riskScore, boolean:hasSlaBreachHistory) {
if (hasSlaBreachHistory) {
return "HIGH";
} else if (riskScore >= 60) {
return "HIGH";
} else if (riskScore >= 35) {
return "MEDIUM";
} else {
return "LOW";
}
}
]]>
</java>
<!-- 是否触发升级 -->
<java name="needEscalate" returnClass="boolean">
<![CDATA[
boolean def needEscalate(int:elapsedMinutes, int:escalateMinutes) {
if (elapsedMinutes >= escalateMinutes) {
return true;
} else {
return false;
}
}
]]>
</java>
<!-- 是否需要经理介入 -->
<java name="needManager" returnClass="boolean">
<![CDATA[
boolean def needManager(str:priority, str:customerLevel, boolean:securityRelated) {
if (priority == "P0" || customerLevel == "VIP" || securityRelated) {
return true;
} else {
return false;
}
}
]]>
</java>
</javas>
package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.TicketSlaRules;
import com.github.paohaijiao.demo.model.SlaDecision;
import com.github.paohaijiao.demo.model.Ticket;
/**
* 场景 5:工单 SLA 分级与升级服务
* <p>
* 职责划分:
* <ul>
* <li><b>Java 侧</b>:时限矩阵装配、风险分汇总、升级判定、处理动作组装</li>
* <li><b>规则侧</b>:服务水平分级、时限取值、权重与风险阈值,全部外置在 XML 中</li>
* </ul>
*/
public class TicketSlaService {
private final TicketSlaRules rules;
public TicketSlaService(TicketSlaRules rules) {
this.rules = rules;
}
/**
* 计算工单 SLA 策略
*
* @param ticket 工单快照
* @return SLA 决策
*/
public SlaDecision evaluate(Ticket ticket) {
SlaDecision decision = new SlaDecision();
decision.setTicketNo(ticket.getTicketNo());
String level = rules.slaLevel(ticket.getPriority(), ticket.getCustomerLevel());
int escalate = rules.escalateMinutes(ticket.getPriority(), level);
int riskScore = rules.priorityWeight(ticket.getPriority())
+ rules.scoreReopen(ticket.getReopenedTimes())
+ rules.scoreLoad(ticket.getAssigneeLoad());
decision.setSlaLevel(level);
decision.setResponseMinutes(rules.responseMinutes(ticket.getPriority(), ticket.isSecurityRelated()));
decision.setResolveMinutes(rules.resolveMinutes(ticket.getPriority(), level));
decision.setEscalateMinutes(escalate);
decision.setRiskScore(riskScore);
decision.setBreachRisk(rules.breachRisk(riskScore, ticket.isHasSlaBreachHistory()));
decision.setNeedEscalate(rules.needEscalate(ticket.getElapsedMinutes(), escalate));
decision.setNeedManager(rules.needManager(ticket.getPriority(), ticket.getCustomerLevel(),
ticket.isSecurityRelated()));
buildActions(ticket, decision);
return decision;
}
private void buildActions(Ticket ticket, SlaDecision decision) {
if (ticket.isSecurityRelated()) {
decision.addAction("安全类工单,同步通知安全值班与应急群");
}
if ("P0".equals(ticket.getPriority())) {
decision.addAction("P0 工单,立即拉群并通知值班经理");
}
if (decision.isNeedEscalate()) {
decision.addAction("已耗时 " + ticket.getElapsedMinutes() + " 分钟,超过升级时限,自动升级");
}
if (ticket.getReopenedTimes() >= 2) {
decision.addAction("工单重开 " + ticket.getReopenedTimes() + " 次,建议复盘根因");
}
if (ticket.getAssigneeLoad() >= 12) {
decision.addAction("处理人在办工单 " + ticket.getAssigneeLoad() + " 件,建议改派或增援");
}
if (decision.isNeedManager()) {
decision.addAction("按策略需经理介入关注");
}
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.SlaDecision;
import com.github.paohaijiao.demo.model.Ticket;
import com.github.paohaijiao.demo.service.TicketSlaService;
import com.github.paohaijiao.xml.JQuickJava;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 场景 5 生产级样板:工单 SLA 分级与升级
* <p>
* 覆盖 6 组工单形态:P0 安全故障、P3 低优请求、VIP 客户重开工单、
* 中等负载重开工单、历史违约复现、安全类普通工单。
*/
public class TicketSlaServiceTest {
private TicketSlaService service;
@Before
public void setUp() {
TicketSlaRules rules = JQuickJava.create()
.createApi(TicketSlaRules.class, "demo/ticket-sla.xml");
this.service = new TicketSlaService(rules);
}
/**
* P0 安全故障:VIP 客户、安全类 → 5 分钟首响、10 分钟升级、经理介入
*/
@Test
public void shouldHandleP0SecurityIncident() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826001");
ticket.setTitle("支付网关不可用");
ticket.setPriority("P0");
ticket.setCategory("INCIDENT");
ticket.setCustomerLevel("VIP");
ticket.setCreateChannel("PHONE");
ticket.setSecurityRelated(true);
ticket.setReopenedTimes(0);
ticket.setAssigneeLoad(8);
ticket.setHasSlaBreachHistory(false);
ticket.setElapsedMinutes(20);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("VIP", decision.getSlaLevel());
Assert.assertEquals(5, decision.getResponseMinutes());
Assert.assertEquals(120, decision.getResolveMinutes());
Assert.assertEquals(10, decision.getEscalateMinutes());
Assert.assertEquals(46, decision.getRiskScore());
Assert.assertEquals("MEDIUM", decision.getBreachRisk());
Assert.assertTrue(decision.isNeedEscalate());
Assert.assertTrue(decision.isNeedManager());
Assert.assertTrue(decision.getActions().contains("安全类工单,同步通知安全值班与应急群"));
}
/**
* P3 低优请求:无负载、无重开 → 标准水平,无升级
*/
@Test
public void shouldKeepLowPriorityRequestStandard() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826002");
ticket.setTitle("修改头像失败");
ticket.setPriority("P3");
ticket.setCategory("REQUEST");
ticket.setCustomerLevel("NORMAL");
ticket.setCreateChannel("APP");
ticket.setSecurityRelated(false);
ticket.setReopenedTimes(0);
ticket.setAssigneeLoad(3);
ticket.setHasSlaBreachHistory(false);
ticket.setElapsedMinutes(100);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("STANDARD", decision.getSlaLevel());
Assert.assertEquals(240, decision.getResponseMinutes());
Assert.assertEquals(7200, decision.getResolveMinutes());
Assert.assertEquals(1440, decision.getEscalateMinutes());
Assert.assertEquals(5, decision.getRiskScore());
Assert.assertEquals("LOW", decision.getBreachRisk());
Assert.assertFalse(decision.isNeedEscalate());
Assert.assertFalse(decision.isNeedManager());
}
/**
* VIP 重开工单:P1、重开 2 次、处理人负载 15 → 高风险、自动升级、经理介入
*/
@Test
public void shouldEscalateReopenedVipTicket() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826003");
ticket.setTitle("订单状态不同步");
ticket.setPriority("P1");
ticket.setCategory("PROBLEM");
ticket.setCustomerLevel("VIP");
ticket.setCreateChannel("WEB");
ticket.setSecurityRelated(false);
ticket.setReopenedTimes(2);
ticket.setAssigneeLoad(15);
ticket.setHasSlaBreachHistory(false);
ticket.setElapsedMinutes(45);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("VIP", decision.getSlaLevel());
Assert.assertEquals(20, decision.getResponseMinutes());
Assert.assertEquals(240, decision.getResolveMinutes());
Assert.assertEquals(30, decision.getEscalateMinutes());
Assert.assertEquals(66, decision.getRiskScore());
Assert.assertEquals("HIGH", decision.getBreachRisk());
Assert.assertTrue(decision.isNeedEscalate());
Assert.assertTrue(decision.isNeedManager());
Assert.assertTrue(decision.getActions().contains("工单重开 2 次,建议复盘根因"));
}
/**
* 中等负载重开:P2、重开 3 次、负载 6 → 中风险、未到升级时限
*/
@Test
public void shouldAssessMediumRiskTicket() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826004");
ticket.setTitle("报表导出超时");
ticket.setPriority("P2");
ticket.setCategory("INCIDENT");
ticket.setCustomerLevel("GOLD");
ticket.setCreateChannel("WEB");
ticket.setSecurityRelated(false);
ticket.setReopenedTimes(3);
ticket.setAssigneeLoad(6);
ticket.setHasSlaBreachHistory(false);
ticket.setElapsedMinutes(100);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("STANDARD", decision.getSlaLevel());
Assert.assertEquals(60, decision.getResponseMinutes());
Assert.assertEquals(2880, decision.getResolveMinutes());
Assert.assertEquals(240, decision.getEscalateMinutes());
Assert.assertEquals(57, decision.getRiskScore());
Assert.assertEquals("MEDIUM", decision.getBreachRisk());
Assert.assertFalse(decision.isNeedEscalate());
Assert.assertFalse(decision.isNeedManager());
}
/**
* 历史违约复现:P1、曾违约 → 风险直接判 HIGH,并触发升级
*/
@Test
public void shouldFlagTicketWithBreachHistory() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826005");
ticket.setTitle("账单对账差异");
ticket.setPriority("P1");
ticket.setCategory("PROBLEM");
ticket.setCustomerLevel("NORMAL");
ticket.setCreateChannel("PHONE");
ticket.setSecurityRelated(false);
ticket.setReopenedTimes(1);
ticket.setAssigneeLoad(6);
ticket.setHasSlaBreachHistory(true);
ticket.setElapsedMinutes(70);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("GOLD", decision.getSlaLevel());
Assert.assertEquals(480, decision.getResolveMinutes());
Assert.assertEquals(60, decision.getEscalateMinutes());
Assert.assertEquals(48, decision.getRiskScore());
Assert.assertEquals("HIGH", decision.getBreachRisk());
Assert.assertTrue(decision.isNeedEscalate());
Assert.assertFalse(decision.isNeedManager());
}
/**
* 安全类普通工单:P2、安全类 → 无需经理介入但安全类触发经理介入
*/
@Test
public void shouldHandleSecurityRelatedNormalTicket() {
Ticket ticket = new Ticket();
ticket.setTicketNo("TK20260826006");
ticket.setTitle("异常登录告警");
ticket.setPriority("P2");
ticket.setCategory("INCIDENT");
ticket.setCustomerLevel("NORMAL");
ticket.setCreateChannel("APP");
ticket.setSecurityRelated(true);
ticket.setReopenedTimes(0);
ticket.setAssigneeLoad(6);
ticket.setHasSlaBreachHistory(false);
ticket.setElapsedMinutes(250);
SlaDecision decision = service.evaluate(ticket);
Assert.assertEquals("STANDARD", decision.getSlaLevel());
Assert.assertEquals(60, decision.getResponseMinutes());
Assert.assertEquals(21, decision.getRiskScore());
Assert.assertEquals("LOW", decision.getBreachRisk());
Assert.assertTrue(decision.isNeedEscalate());
Assert.assertTrue(decision.isNeedManager());
}
}

Run this case with the following command at the project root:

Terminal window
mvn test -Dtest=TicketSlaServiceTest

The test class has 6 @Test methods covering six ticket shapes: P0 security incident, low-priority request, VIP reopened ticket, medium-load reopened ticket, historical-breach recurrence, and security-related normal ticket:

Test method Constructed business shape Key assertions / expected decision
shouldHandleP0SecurityIncident Payment gateway down, P0, VIP customer, security-related, 8 open tickets, 20 minutes elapsed VIP, first response 5 (security tightened), resolution 120, escalation 10 minutes; risk score 46 (40+0+6) MEDIUM; already past escalation deadline so immediate escalation, manager needed, actions include “notify security duty and emergency group”
shouldKeepLowPriorityRequestStandard Avatar update failure, P3, NORMAL, 3 open tickets, 100 minutes elapsed STANDARD, first response 240, resolution 7200, escalation 1440 minutes; risk score 5 LOW; 100 minutes not past escalation deadline, no escalation, no manager
shouldEscalateReopenedVipTicket Order status out of sync, P1, VIP, reopened 2 times, 15 open tickets, 45 minutes elapsed VIP (customer tier lift), first response 20, resolution 240, escalation 30 minutes; risk score 66 (30+24+12) HIGH; 45 minutes past deadline so escalation, manager needed, actions include “reopened 2 times, suggest root-cause review”
shouldAssessMediumRiskTicket Report export timeout, P2, GOLD customer, reopened 3 times, 6 open tickets, 100 minutes elapsed STANDARD (GOLD is not VIP), first response 60, resolution 2880, escalation 240 minutes; reopen score 36 hits cap, risk score 57 (15+36+6) MEDIUM; 100 minutes not past 240-minute deadline so no escalation, no manager
shouldFlagTicketWithBreachHistory Billing reconciliation discrepancy, P1, NORMAL, reopened 1 time, 6 open tickets, SLA breach history, 70 minutes elapsed GOLD, resolution 480, escalation 60 minutes; risk score 48 (30+12+6) below 60 but historical breach hard-lifts to HIGH; 70 minutes past deadline so escalation, no manager (not P0/VIP/security)
shouldHandleSecurityRelatedNormalTicket Abnormal login alert, P2, NORMAL, security-related, 6 open tickets, 250 minutes elapsed STANDARD, first response 60 (security tightening only for P0/P1); risk score 21 LOW; 250 minutes past 240-minute escalation deadline so escalation; security-related triggers manager involvement

P0 security incident (VIP customer, security-related):

服务水平: VIP 首响: 5 分钟 解决: 120 分钟 升级时限: 10 分钟
风险分: 46 (MEDIUM) 需立即升级: true 需经理介入: true

VIP with multiple reopens (3+ reopens, high assignee load):

风险分: 66 (HIGH) 需立即升级: true
  • Deadline matrices fully externalized in XML, no release for contract or peak-protection adjustments: the 10/20/60/240 first-response, the VIP/non-VIP dual matrices for resolution and escalation, and the security-related P0/P1 tightening to 5 minutes all live in ticket-sla.xml — large-customer differentiated commitments or peak-protection tightening only require editing the XML and rebuilding the proxy.
  • Three risk factors, one method per policy, independently tunable: priority weight, reopen risk, and assignee load are each an independent rule method; the reopen score is 12 per reopen with an in-script if (score > 36) cap at 36, and load is tiered at 20/12/6 open tickets — score composition is clear and each factor can be calibrated against operations data.
  • Historical breach hard lift, manager involvement pre-positioned: in breachRisk, historical breach takes precedence over score, so a 48-point ticket is still HIGH; needManager ORs P0, VIP customer, and security-related upfront, so a P2 normal ticket that is security-related still gets manager involvement — not inferred indirectly from the score.
  • Escalation decision decoupled from the deadline matrix: needEscalate only checks “elapsed time ≥ escalation deadline”, so whether escalation is immediate is entirely determined by the matrix-computed deadline — P0/security deadlines are only 5–10 minutes, and the same mechanism naturally achieves “immediate escalation” without a special branch for urgent tickets.
  • Boolean rules in positive return form, fitting script syntax: decisions like needEscalate, needManager, and slaLevel are written in the positive form if (...) { return true; } else { return false; } without relying on negation operators; the Service then assembles actions such as group-chat alerts, security duty notifications, root-cause reviews, and reassignment reinforcement per hit condition.
  • @Param name binding + proxy reuse + ASM invocation chain: rule methods only take scalars; the ticket object is decomposed by the Service; the proxy is created once and reused across all 6 cases; the engine targets Java 8 with no heavy dependencies and embeds into ITSM/contact-center legacy systems and domestic (Xinchuang) JDKs, supporting high-frequency ticket online evaluation.