案例 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 |
规则方法拆分
Section titled “规则方法拆分”规则接口共 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 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将六个文件按源码原有包结构放入 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 个用例。
Ticket.java
Section titled “Ticket.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 客服工单要素(SLA 分级与升级所需的工单快照) */@Data@NoArgsConstructor@AllArgsConstructorpublic 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;}SlaDecision.java
Section titled “SlaDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 工单 SLA 决策结果 */@Datapublic 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); }}TicketSlaRules.java
Section titled “TicketSlaRules.java”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);}ticket-sla.xml
Section titled “ticket-sla.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.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>TicketSlaService.java
Section titled “TicketSlaService.java”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("按策略需经理介入关注"); } }}TicketSlaServiceTest.java
Section titled “TicketSlaServiceTest.java”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()); }}在工程根目录执行以下命令运行本案例:
mvn test -Dtest=TicketSlaServiceTest测试覆盖形态
Section titled “测试覆盖形态”测试类共 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 需经理介入: trueVIP 多次重开(重开 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,支撑工单高频在线评估。