feat: 事件异常等待字段与小数超时小时

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-21 18:19:22 +08:00
co-authored by Cursor
parent 1c742353b8
commit 9e27bada94
11 changed files with 307 additions and 13 deletions
@@ -164,6 +164,51 @@ public class InquiryAgentController {
return Result.OK(out);
}
@ApiOperation("开始事件异常等待(不改六态)")
@PostMapping("/ticket/startEventWait")
public Result<Map<String, Object>> startEventWait(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.startEventWait(
firstStr(body, "workOrderNo", "work_order_no"),
firstStr(body, "waitKind", "wait_kind")));
}
@ApiOperation("清空事件异常等待(不改六态、不清摘要)")
@PostMapping("/ticket/clearEventWait")
public Result<Map<String, Object>> clearEventWait(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.clearEventWait(firstStr(body, "workOrderNo", "work_order_no")));
}
@ApiOperation("列出未告警的事件异常等待(Worker 巡检)")
@PostMapping("/ticket/listEventWaits")
public Result<java.util.List<Map<String, Object>>> listEventWaits(
@RequestBody(required = false) Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.listEventWaits());
}
@ApiOperation("记下事件异常摘要并标记已告警(不改六态)")
@PostMapping("/ticket/recordEventException")
public Result<Map<String, Object>> recordEventException(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.recordEventException(
firstStr(body, "workOrderNo", "work_order_no"),
firstStr(body, "eventException", "event_exception")));
}
@ApiOperation("智能体续办读单(只读,校验发起人)")
@PostMapping("/ticket/get")
public Result<Map<String, Object>> get(@RequestBody Map<String, Object> body,
@@ -24,7 +24,7 @@ import java.util.Map;
* <p>
* 只提供全量列表与改小时数;无 add/delete,维度由 SQL 种子固定。
* 每条有唯一 timeoutCode(如 sea_sales),智能体单码调用;roleCode 仅为角色维度可重复。
* 本接口供管理后台使用;工单超时判定尚未接入。
* 本接口供管理后台使用;智能体按 timeoutCode 读小时数做超时判定。
* </p>
*/
@Slf4j
@@ -81,6 +81,9 @@ public class InquiryResponseTimeoutController
if (body.getTimeoutHours() == null) {
return Result.error("超时小时不能为空");
}
if (body.getTimeoutHours().compareTo(java.math.BigDecimal.ZERO) < 0) {
return Result.error("超时小时不能为负数");
}
try {
responseTimeoutService.updateTimeoutHours(body.getId(), body.getTimeoutHours(), currentUsername());
return Result.OK("已保存");
@@ -19,7 +19,7 @@ import java.util.Date;
* <p>
* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。
* timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。
* 本表是配置真相;工单超时判定后续再读本表,本期仅 CRUD 小时数。
* 本表是配置真相;工单超时判定读 timeoutHours(允许小数,0=不监测)。
* </p>
*/
@Data
@@ -49,8 +49,8 @@ public class InquiryResponseTimeout implements Serializable {
@ApiModelProperty("运输类型(海运/陆运产品有值,其余空串)")
private String transportType;
@ApiModelProperty("超时小时,整数≥0")
private Integer timeoutHours;
@ApiModelProperty("超时小时,允许小数,0=不监测")
private java.math.BigDecimal timeoutHours;
@ApiModelProperty("展示排序")
private Integer sortNo;
@@ -116,6 +116,17 @@ public class InquiryTicket implements Serializable {
@ApiModelProperty("事件异常摘要")
private String eventException;
@ApiModelProperty("事件异常当前等待种类")
private String eventWaitKind;
@ApiModelProperty("事件异常当前等待起点")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date eventWaitStartedAt;
@ApiModelProperty("当前等待是否已告警 0/1")
private Integer eventWaitAlerted;
@ApiModelProperty("系统异常摘要")
private String systemException;
@@ -15,10 +15,10 @@ public interface IInquiryResponseTimeoutService extends IService<InquiryResponse
* 仅更新超时小时数;维度字段不可改。
*
* @param id 配置行 id
* @param timeoutHours 新小时数(≥0)
* @param timeoutHours 新小时数(≥0,允许小数,0=不监测)
* @param updateBy 操作人登录名
*/
void updateTimeoutHours(String id, int timeoutHours, String updateBy);
void updateTimeoutHours(String id, java.math.BigDecimal timeoutHours, String updateBy);
/**
* 把陆运产品「普件,…」运输类型对齐产品确认的括号文案。
@@ -136,4 +136,24 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
* 只改到期行,不加全局锁。
*/
java.util.List<java.util.Map<String, Object>> claimDueNegotiate();
/**
* 开始盯事件异常等待。种类相同不重置起点;种类不同重置并清已告警。不改六态。
*/
Map<String, Object> startEventWait(String workOrderNo, String waitKind);
/**
* 清空当前事件异常等待三列,不改 eventException 摘要、不改六态。
*/
Map<String, Object> clearEventWait(String workOrderNo);
/**
* 列出未告警且可盯的等待中工单,供 Worker 巡检。
*/
java.util.List<java.util.Map<String, Object>> listEventWaits();
/**
* 追加事件异常摘要并标记当前等待已告警。不改六态。
*/
Map<String, Object> recordEventException(String workOrderNo, String eventException);
}
@@ -71,8 +71,8 @@ public class InquiryResponseTimeoutServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTimeoutHours(String id, int timeoutHours, String updateBy) {
if (timeoutHours < 0) {
public void updateTimeoutHours(String id, java.math.BigDecimal timeoutHours, String updateBy) {
if (timeoutHours == null || timeoutHours.compareTo(java.math.BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("超时小时不能为负数");
}
InquiryResponseTimeout row = getById(id);
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.inquiry.domain.AirGroupActivateRule;
import org.jeecg.modules.inquiry.domain.EventExceptionRule;
import org.jeecg.modules.inquiry.domain.TicketStatuses;
import org.jeecg.modules.inquiry.dto.AgentTicketCreateRequest;
import org.jeecg.modules.inquiry.dto.TicketAbnormalRequest;
@@ -101,7 +102,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
if ("-".equals(filters.get("eventException"))) {
qw.and(w -> w.isNull(InquiryTicket::getEventException).or().eq(InquiryTicket::getEventException, "").or().eq(InquiryTicket::getEventException, "-"));
} else {
qw.eq(InquiryTicket::getEventException, filters.get("eventException"));
qw.like(InquiryTicket::getEventException, filters.get("eventException"));
}
}
if (StringUtils.isNotBlank(filters.get("systemException"))) {
@@ -1405,6 +1406,204 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
}
/**
* 开始盯当前等待。可盯状态才写;同种类不重置起点。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> startEventWait(String workOrderNo, String waitKind) {
Map<String, Object> out = new LinkedHashMap<>();
String no = StringUtils.trimToEmpty(workOrderNo);
String kind = StringUtils.trimToEmpty(waitKind);
InquiryTicket ticket = findByWorkOrderNo(no);
if (ticket == null) {
out.put("ok", false);
out.put("error", "not_found");
return out;
}
if (!EventExceptionRule.isWatchable(ticket.getStatus()) || kind.isEmpty()) {
out.put("ok", false);
out.put("error", "not_watchable");
out.put("status", ticket.getStatus());
return out;
}
Date now = new Date();
EventExceptionRule.EventWaitState next = EventExceptionRule.applyStart(
ticket.getEventWaitKind(), ticket.getEventWaitStartedAt(),
ticket.getEventWaitAlerted(), kind, now);
ticket.setEventWaitKind(next.kind);
ticket.setEventWaitStartedAt(next.startedAt);
ticket.setEventWaitAlerted(next.alerted);
ticket.setUpdateTime(now);
updateById(ticket);
out.put("ok", true);
out.put("work_order_no", no);
out.put("eventWaitKind", next.kind);
out.put("eventWaitAlerted", next.alerted);
return out;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> clearEventWait(String workOrderNo) {
Map<String, Object> out = new LinkedHashMap<>();
InquiryTicket ticket = findByWorkOrderNo(StringUtils.trimToEmpty(workOrderNo));
if (ticket == null) {
out.put("ok", false);
out.put("error", "not_found");
return out;
}
ticket.setEventWaitKind(null);
ticket.setEventWaitStartedAt(null);
ticket.setEventWaitAlerted(0);
ticket.setUpdateTime(new Date());
updateById(ticket);
out.put("ok", true);
out.put("work_order_no", ticket.getWorkOrderNo());
return out;
}
@Override
public List<Map<String, Object>> listEventWaits() {
List<InquiryTicket> rows = list(new LambdaQueryWrapper<InquiryTicket>()
.isNotNull(InquiryTicket::getEventWaitKind)
.ne(InquiryTicket::getEventWaitKind, "")
.and(w -> w.isNull(InquiryTicket::getEventWaitAlerted)
.or().eq(InquiryTicket::getEventWaitAlerted, 0))
.notIn(InquiryTicket::getStatus,
TicketStatuses.DEAL, TicketStatuses.CLOSED, TicketStatuses.HANDOFF));
List<Map<String, Object>> out = new ArrayList<>();
if (rows == null) {
return out;
}
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (InquiryTicket ticket : rows) {
if (ticket == null || !EventExceptionRule.isWatchable(ticket.getStatus())) {
continue;
}
Map<String, Object> one = new LinkedHashMap<>();
one.put("workOrderNo", ticket.getWorkOrderNo());
one.put("work_order_no", ticket.getWorkOrderNo());
one.put("status", ticket.getStatus());
one.put("businessLine", StringUtils.defaultString(ticket.getBusinessLine()));
one.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
one.put("eventWaitKind", ticket.getEventWaitKind());
one.put("event_wait_kind", ticket.getEventWaitKind());
if (ticket.getEventWaitStartedAt() != null) {
String started = fmt.format(ticket.getEventWaitStartedAt());
one.put("eventWaitStartedAt", started);
one.put("event_wait_started_at", started);
}
one.put("salesWecomId", StringUtils.defaultString(ticket.getSalesWecomId()));
one.put("sales_wecom_id", StringUtils.defaultString(ticket.getSalesWecomId()));
one.put("salesName", StringUtils.defaultString(ticket.getSalesName()));
one.put("sales_name", StringUtils.defaultString(ticket.getSalesName()));
one.put("collabChatId", StringUtils.defaultString(ticket.getCollabChatId()));
one.put("collab_chat_id", StringUtils.defaultString(ticket.getCollabChatId()));
List<String> members = parseMemberIds(ticket.getCollabMemberIds());
one.put("collabMemberIds", members);
one.put("collab_member_ids", members);
one.put("eventException", StringUtils.defaultString(ticket.getEventException()));
Map<String, String> facts = new LinkedHashMap<>();
if (StringUtils.isNotBlank(ticket.getFactsJson())) {
JSONObject parsed = JSON.parseObject(ticket.getFactsJson());
if (parsed != null) {
for (String key : parsed.keySet()) {
if (parsed.get(key) != null) {
facts.put(key, String.valueOf(parsed.get(key)));
}
}
}
}
one.put("facts", facts);
if (ticket.getNextFollowupAt() != null) {
one.put("nextFollowupAt", fmt.format(ticket.getNextFollowupAt()));
one.put("next_followup_at", fmt.format(ticket.getNextFollowupAt()));
}
one.put("watchers", buildEventWatchers(ticket, members));
out.add(one);
}
return out;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> recordEventException(String workOrderNo, String eventException) {
Map<String, Object> out = new LinkedHashMap<>();
InquiryTicket ticket = findByWorkOrderNo(StringUtils.trimToEmpty(workOrderNo));
if (ticket == null) {
out.put("ok", false);
out.put("error", "not_found");
return out;
}
String type = StringUtils.trimToEmpty(eventException);
ticket.setEventException(EventExceptionRule.appendSummary(ticket.getEventException(), type));
ticket.setEventWaitAlerted(1);
ticket.setUpdateTime(new Date());
updateById(ticket);
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "event_exception", "agent", type);
out.put("ok", true);
out.put("work_order_no", ticket.getWorkOrderNo());
out.put("eventException", ticket.getEventException());
return out;
}
private List<Map<String, Object>> buildEventWatchers(InquiryTicket ticket, List<String> memberIds) {
List<Map<String, Object>> out = new ArrayList<>();
Set<String> seen = new HashSet<>();
List<String> ids = new ArrayList<>();
if (StringUtils.isNotBlank(ticket.getSalesWecomId())) {
ids.add(ticket.getSalesWecomId().trim());
}
if (memberIds != null) {
ids.addAll(memberIds);
}
if (ids.isEmpty()) {
return out;
}
List<InquiryStaffRole> rows = staffRoleMapper.selectList(new LambdaQueryWrapper<InquiryStaffRole>()
.in(InquiryStaffRole::getWecomId, ids)
.eq(InquiryStaffRole::getStatus, "active"));
Map<String, InquiryStaffRole> byId = new HashMap<>();
if (rows != null) {
for (InquiryStaffRole row : rows) {
if (row != null && StringUtils.isNotBlank(row.getWecomId())) {
byId.put(row.getWecomId().trim(), row);
}
}
}
if (StringUtils.isNotBlank(ticket.getSalesWecomId())) {
String sid = ticket.getSalesWecomId().trim();
InquiryStaffRole sales = byId.get(sid);
Map<String, Object> one = new LinkedHashMap<>();
one.put("wecomId", sid);
one.put("name", sales != null ? StringUtils.defaultString(sales.getName())
: StringUtils.defaultString(ticket.getSalesName()));
one.put("roleCode", sales != null ? StringUtils.defaultString(sales.getRoleCode()) : "sales");
one.put("notifyEventException", sales != null && sales.getNotifyEventException() != null
? sales.getNotifyEventException() : 0);
out.add(one);
seen.add(sid);
}
for (String uid : memberIds == null ? Collections.<String>emptyList() : memberIds) {
if (StringUtils.isBlank(uid) || seen.contains(uid)) {
continue;
}
InquiryStaffRole row = byId.get(uid);
if (row == null) {
continue;
}
Map<String, Object> one = new LinkedHashMap<>();
one.put("wecomId", uid);
one.put("name", StringUtils.defaultString(row.getName()));
one.put("roleCode", StringUtils.defaultString(row.getRoleCode()));
one.put("notifyEventException", row.getNotifyEventException() == null ? 0 : row.getNotifyEventException());
out.add(one);
seen.add(uid);
}
return out;
}
private void appendLog(InquiryTicket ticket, String from, String to, String action, String operator, String remark) {
InquiryTicketStatusLog log = new InquiryTicketStatusLog();
log.setTicketId(ticket.getId());
@@ -0,0 +1,7 @@
-- 超时小时改为小数,0 表示该规则不监测。
-- 仅改 inquiry_robot;幂等说明:重复执行若已是 decimal 会报 1060/1064,可忽略。
SET NAMES utf8mb4;
ALTER TABLE `inq_response_timeout`
MODIFY COLUMN `timeout_hours` decimal(10,4) NOT NULL DEFAULT 0 COMMENT '超时小时,允许小数,0=不监测';
@@ -0,0 +1,9 @@
-- 工单事件异常当前等待:种类、起点、是否已告警。
-- 仅改 inquiry_robot。列已存在则跳过(手工执行前请 DESCRIBE inq_ticket)。
SET NAMES utf8mb4;
ALTER TABLE `inq_ticket`
ADD COLUMN `event_wait_kind` varchar(32) NULL COMMENT '事件异常当前等待' AFTER `event_exception`,
ADD COLUMN `event_wait_started_at` datetime NULL COMMENT '当前等待起点' AFTER `event_wait_kind`,
ADD COLUMN `event_wait_alerted` tinyint NULL DEFAULT 0 COMMENT '当前等待是否已告警' AFTER `event_wait_started_at`;
@@ -82,14 +82,14 @@ async function loadList() {
onMounted(() => loadList());
function onHoursInput(id, raw) {
const n = Math.floor(Number(raw));
const n = Number(raw);
draftHours.value[id] = Number.isFinite(n) && n >= 0 ? n : 0;
}
async function commitHours(row) {
const hours = draftHours.value[row.id];
if (hours == null || !Number.isFinite(hours) || hours < 0) {
message.warning('超时小时须为 ≥0 的整数');
message.warning('超时小时须为 ≥0 的数字,支持小数');
draftHours.value[row.id] = row.timeoutHours;
return;
}
@@ -113,7 +113,7 @@ async function commitHours(row) {
<template>
<div class="sys-setting-page">
<div class="toolbar-bar">
<span class="hint">固定规则,仅可改超时小时;编码唯一只读,供智能体调用;失焦或回车保存</span>
<span class="hint">固定规则,仅可改超时小时(支持小数,0=不监测);编码唯一只读,供智能体调用;失焦或回车保存</span>
</div>
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
@@ -151,7 +151,7 @@ async function commitHours(row) {
class="field hours-input"
type="number"
min="0"
step="1"
step="0.01"
:value="draftHours[row.id]"
:disabled="savingId === row.id"
@input="onHoursInput(row.id, $event.target.value)"