跳转到内容

案例 5:工单 SLA 分级与升级

本案例对应 JQuick-Java 仓库 USE-CASES.md 中的场景 5:工单 SLA 分级与升级(生产级样板),属于运维客服领域,解决客服与运维工单的服务水平管控:按优先级、客户等级与安全属性装配首响、解决、升级三档时限矩阵,再结合重开次数、处理人负载与历史违约记录计算违约风险分,决定是否自动升级、是否需要经理介入。

SLA(服务水平协议)是企业对客户工单响应时效的承诺,广泛存在于客服中心、IT 服务台与运维值班体系中。工单按性质分为故障(INCIDENT)、请求(REQUEST)、问题(PROBLEM)、变更(CHANGE),每笔工单带有优先级(P0~P3)、客户等级(VIP/GOLD/NORMAL)、来源渠道、是否安全类、重开次数、当前处理人在办工单数、是否有历史 SLA 违约记录以及已经耗时等快照字段。

决策首先确定服务水平等级:P0 工单或 VIP 客户直接享受 VIP 水平,P1 为 GOLD,其余为 STANDARD。随后按「优先级 × 服务水平」双维矩阵装配三档时限:首次响应时限 P0/P1/P2/其余分别为 10/20/60/240 分钟,其中安全类 P0/P1 进一步收紧到 5 分钟;解决时限在 VIP 矩阵下 P0 为 120 分钟、P1 为 240 分钟、P2 为 1440 分钟,非 VIP 矩阵整体放宽一倍;升级时限同样按双维矩阵取值(VIP 的 P0 仅 10 分钟)。

与时限并行的是违约风险预判。风险分由三个先行因子构成:优先级权重(P0 40 / P1 30 / P2 15 / P3 5)、重开风险(每重开一次 12 分、封顶 36 分)、处理人负载(在办 ≥20/≥12/6 件分别得 20/12/6 分)。风险分 ≥60 或有历史违约记录即为 HIGH,≥35 为 MEDIUM,否则 LOW——历史违约是不看分数的硬拉高项,48 分也会直接判 HIGH。

最终输出物包括服务水平等级、首响/解决/升级三档时限、综合风险分与违约风险等级、是否触发自动升级(已耗时达到升级时限)、是否需要经理介入(P0、VIP 客户或安全类工单),以及处理动作建议清单:安全类同步通知安全值班与应急群、P0 立即拉群通知值班经理、超期自动升级、重开 ≥2 次建议复盘根因、处理人在办 ≥12 件建议改派增援等,可直接被工单调度与通知系统消费。

  • 时限矩阵复杂且硬编码:4 档优先级 × 3 类客户水平 × 首响/解决/升级三类时限,再叠加安全类收紧规则,散落在工单系统的定时器、通知脚本与排班表中,维护困难且容易配错。
  • SLA 承诺差异化、调整频繁:大客户合同单独约定更短的响应时限,重保期、大促期间还要临时收紧 P0/P1 时限,硬编码方式下每次调整都需要工单系统发版。
  • 违约只能事后判定、无法提前预警:传统做法只在工单真正超时时才告警,重开次数、处理人在办负载这些先行风险指标没有进入决策,等超时发生时客户已经投诉。
  • 升级与经理介入依赖人工盯屏:值班长靠经验判断哪些工单该升级、哪些该报经理,P0 与安全事件在夜间或高峰期容易漏盯,10 分钟的升级窗口稍纵即逝。
  • 风险因子耦合、难独立调参:优先级权重、重开扣分、负载阈值、历史违约拉高混写在一起,调整任一因子都要回归整条链路,也说不清一笔 HIGH 风险工单的分数构成。
  • 老旧 ITSM 与信创环境约束:大量客服与 ITSM 系统仍运行在 JDK 8 或国产化配套 JDK 上,难以额外引入独立部署的重型服务管理平台。

本案例采用「领域模型 + 规则接口与 XML 规则脚本 + 编排服务 + 测试」的六件套结构:服务水平分级、双维时限矩阵、风险权重与阈值全部外置在 XML 中,Java 侧负责时限装配、风险分汇总、升级判定与动作组装。

层次 职责 对应文件
领域模型 工单快照、SLA 决策结果 Ticket.java、SlaDecision.java
规则集与 XML 服务水平分级、时限矩阵、风险因子、升级与经理判定 TicketSlaRules.java、ticket-sla.xml
编排服务 时限矩阵装配、风险分汇总、升级判定、处理动作组装 TicketSlaService.java
测试 6 组工单业务形态 TicketSlaServiceTest.java

规则接口共 10 个方法:3 个水平/权重映射、3 个时限矩阵取值、3 个风险评分与 2 个升级判定:

规则方法 口径 分值/输出
slaLevel P0 或 VIP 客户 → VIP;P1 → GOLD;否则 STANDARD VIP / GOLD / STANDARD
responseMinutes P0 10 / P1 20 / P2 60 / 其余 240 分钟;安全类 P0/P1 收紧到 5 分钟 5 ~ 240 分钟
resolveMinutes VIP/非 VIP × 优先级双维矩阵:P0 120/240、P1 240/480、P2 1440/2880、其余 4320/7200 分钟
escalateMinutes VIP/非 VIP × 优先级双维矩阵:P0 10/30、P1 30/60、P2 120/240、其余 720/1440 分钟
priorityWeight P0 40 / P1 30 / P2 15 / P3 5 5 ~ 40
scoreReopen 每次重开 12 分,封顶 36 0 ~ 36
scoreLoad 处理人在办 ≥20/≥12/≥6 件 → 20/12/6,否则 0 0 ~ 20
breachRisk 历史违约或风险分 ≥60 → HIGH;≥35 → MEDIUM;否则 LOW HIGH / MEDIUM / LOW
needEscalate 已耗时达到升级时限即触发(P0/安全类时限仅 5~10 分钟,表现为立即升级) boolean
needManager P0、VIP 客户或安全类工单需经理介入 boolean

将六个文件按源码原有包结构放入 Maven 工程的 src/test 对应目录:模型类 Ticket.java、SlaDecision.java 位于 com.github.paohaijiao.demo.model;规则接口 TicketSlaRules.java 与测试类 TicketSlaServiceTest.java 位于 com.github.paohaijiao.demo;编排服务 TicketSlaService.java 位于 com.github.paohaijiao.demo.service;规则脚本 ticket-sla.xml 位于 src/test/resources/demo/。六个文件的包名保持源码原样,模型类依赖 Lombok 的 @Data 注解。在工程根目录执行 mvn test -Dtest=TicketSlaServiceTest 即可运行全部 6 个用例。

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());
}
}

在工程根目录执行以下命令运行本案例:

Terminal window
mvn test -Dtest=TicketSlaServiceTest

测试类共 6 个 @Test 方法,覆盖 P0 安全故障、低优请求、VIP 重开、中负载中风险、历史违约、安全类普通工单六类形态:

测试方法 构造的业务形态 关键断言/期望决策
shouldHandleP0SecurityIncident 支付网关不可用,P0、VIP 客户、安全类、在办 8 件、已耗时 20 分钟 VIP,首响 5(安全收紧)、解决 120、升级时限 10 分钟;风险分 46(40+0+6)MEDIUM;已超升级时限需立即升级、需经理介入,动作含「同步通知安全值班与应急群」
shouldKeepLowPriorityRequestStandard 修改头像失败,P3、NORMAL、在办 3 件、已耗时 100 分钟 STANDARD,首响 240、解决 7200、升级 1440 分钟;风险分 5 LOW;100 分钟未到升级时限,不升级、不需经理
shouldEscalateReopenedVipTicket 订单状态不同步,P1、VIP、重开 2 次、在办 15 件、已耗时 45 分钟 VIP(客户等级拉高),首响 20、解决 240、升级 30 分钟;风险分 66(30+24+12)HIGH;45 分钟超时需升级、需经理介入,动作含「重开 2 次,建议复盘根因」
shouldAssessMediumRiskTicket 报表导出超时,P2、GOLD 客户、重开 3 次、在办 6 件、已耗时 100 分钟 STANDARD(GOLD 不等于 VIP),首响 60、解决 2880、升级 240 分钟;重开分 36 触封顶,风险分 57(15+36+6)MEDIUM;100 分钟未到 240 分钟不升级、不需经理
shouldFlagTicketWithBreachHistory 账单对账差异,P1、NORMAL、重开 1 次、在办 6 件、有 SLA 违约史、已耗时 70 分钟 GOLD,解决 480、升级 60 分钟;风险分 48(30+12+6)虽不足 60,但历史违约硬拉高为 HIGH;70 分钟超时需升级,不需经理(非 P0/VIP/安全)
shouldHandleSecurityRelatedNormalTicket 异常登录告警,P2、NORMAL、安全类、在办 6 件、已耗时 250 分钟 STANDARD,首响 60(安全收紧仅对 P0/P1 生效);风险分 21 LOW;250 分钟超过 240 分钟升级时限需升级;安全类触发经理介入

P0 安全故障(VIP 客户、安全类):

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

VIP 多次重开(重开 3 次以上、处理人高负载):

风险分: 66 (HIGH) 需立即升级: true
  • 时限矩阵全部外置 XML,合同与重保调整无需发版:首响 10/20/60/240、解决与升级的 VIP/非 VIP 双维矩阵、安全类 P0/P1 收紧到 5 分钟均在 ticket-sla.xml 中,大客户差异化承诺或重保期收紧只改 XML、重建代理即生效。
  • 三类风险因子一方法一口径,可独立调参:优先级权重、重开风险、处理人负载各是一个独立规则方法,其中重开分每次 12 分、脚本内 if (score > 36) 封顶 36,负载按 20/12/6 件分档,分数构成清晰、可按运营数据单独校准。
  • 历史违约硬拉高、经理介入前置:breachRisk 中历史违约优先于分数判定,48 分也直接判 HIGH;needManager 把 P0、VIP 客户、安全类三条件做 OR 前置,安全类 P2 普通工单也必须经理介入,不靠分数间接推断。
  • 升级判定与时限矩阵解耦:needEscalate 只比较「已耗时 ≥ 升级时限」,是否立即升级完全由矩阵算出的时限决定——P0/安全类时限只有 5~10 分钟,同一套机制天然实现「立即升级」,无需为紧急工单写特判分支。
  • 布尔规则正向 return,贴合脚本语法:needEscalate、needManager、slaLevel 等判定均以 if (...) { return true; } else { return false; } 的正向写法表达,不依赖取反运算符;Service 侧再按命中条件组装拉群、安全值班、根因复盘、改派增援等动作。
  • @Param 按名绑定 + 代理复用 + ASM 调用链:规则方法只收标量,工单对象由 Service 拆解传入;代理一次创建、6 个用例复用;引擎 Java 8 起步、无重型依赖,可嵌入 ITSM/客服老旧系统与信创 JDK,支撑工单高频在线评估。