From 9e27bada948d73e1e2abbcf5880caceb80d8458c Mon Sep 17 00:00:00 2001 From: jillion <439912596@qq.com> Date: Mon, 21 Sep 2026 18:19:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=8B=E4=BB=B6=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E5=AD=97=E6=AE=B5=E4=B8=8E=E5=B0=8F=E6=95=B0?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E5=B0=8F=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../controller/InquiryAgentController.java | 45 ++++ .../InquiryResponseTimeoutController.java | 5 +- .../entity/InquiryResponseTimeout.java | 6 +- .../modules/inquiry/entity/InquiryTicket.java | 11 + .../IInquiryResponseTimeoutService.java | 4 +- .../service/IInquiryWorkOrderService.java | 20 ++ .../InquiryResponseTimeoutServiceImpl.java | 4 +- .../impl/InquiryWorkOrderServiceImpl.java | 201 +++++++++++++++++- ..._inq_response_timeout_hours_decimal_v1.sql | 7 + .../db/migration_inq_ticket_event_wait_v1.sql | 9 + .../src/views/inquiry/systemSetting/index.vue | 8 +- 11 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_hours_decimal_v1.sql create mode 100644 inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_ticket_event_wait_v1.sql diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryAgentController.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryAgentController.java index 4380ead..fd0cd29 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryAgentController.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryAgentController.java @@ -164,6 +164,51 @@ public class InquiryAgentController { return Result.OK(out); } + @ApiOperation("开始事件异常等待(不改六态)") + @PostMapping("/ticket/startEventWait") + public Result> startEventWait(@RequestBody Map 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> clearEventWait(@RequestBody Map 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>> listEventWaits( + @RequestBody(required = false) Map body, + HttpServletRequest http) { + if (!authed(http)) { + return Result.error(401, "unauthorized"); + } + return Result.OK(workOrderService.listEventWaits()); + } + + @ApiOperation("记下事件异常摘要并标记已告警(不改六态)") + @PostMapping("/ticket/recordEventException") + public Result> recordEventException(@RequestBody Map 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> get(@RequestBody Map body, diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryResponseTimeoutController.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryResponseTimeoutController.java index b4c872e..3b25509 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryResponseTimeoutController.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryResponseTimeoutController.java @@ -24,7 +24,7 @@ import java.util.Map; *

* 只提供全量列表与改小时数;无 add/delete,维度由 SQL 种子固定。 * 每条有唯一 timeoutCode(如 sea_sales),智能体单码调用;roleCode 仅为角色维度可重复。 - * 本接口供管理后台使用;工单超时判定尚未接入。 + * 本接口供管理后台使用;智能体按 timeoutCode 读小时数做超时判定。 *

*/ @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("已保存"); diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryResponseTimeout.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryResponseTimeout.java index 4dcc7f8..6d9f090 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryResponseTimeout.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryResponseTimeout.java @@ -19,7 +19,7 @@ import java.util.Date; *

* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。 * timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。 - * 本表是配置真相;工单超时判定后续再读本表,本期仅 CRUD 小时数。 + * 本表是配置真相;工单超时判定读 timeoutHours(允许小数,0=不监测)。 *

*/ @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; diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryTicket.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryTicket.java index 3380714..2d10446 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryTicket.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/entity/InquiryTicket.java @@ -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; diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/IInquiryResponseTimeoutService.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/IInquiryResponseTimeoutService.java index 4105aa5..9ee11fa 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/IInquiryResponseTimeoutService.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/IInquiryResponseTimeoutService.java @@ -15,10 +15,10 @@ public interface IInquiryResponseTimeoutService extends IService { * 只改到期行,不加全局锁。 */ java.util.List> claimDueNegotiate(); + + /** + * 开始盯事件异常等待。种类相同不重置起点;种类不同重置并清已告警。不改六态。 + */ + Map startEventWait(String workOrderNo, String waitKind); + + /** + * 清空当前事件异常等待三列,不改 eventException 摘要、不改六态。 + */ + Map clearEventWait(String workOrderNo); + + /** + * 列出未告警且可盯的等待中工单,供 Worker 巡检。 + */ + java.util.List> listEventWaits(); + + /** + * 追加事件异常摘要并标记当前等待已告警。不改六态。 + */ + Map recordEventException(String workOrderNo, String eventException); } diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryResponseTimeoutServiceImpl.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryResponseTimeoutServiceImpl.java index 0fbd44b..3773256 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryResponseTimeoutServiceImpl.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryResponseTimeoutServiceImpl.java @@ -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); diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryWorkOrderServiceImpl.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryWorkOrderServiceImpl.java index 60d645e..0da3aaa 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryWorkOrderServiceImpl.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryWorkOrderServiceImpl.java @@ -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 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 startEventWait(String workOrderNo, String waitKind) { + Map 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 clearEventWait(String workOrderNo) { + Map 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> listEventWaits() { + List rows = list(new LambdaQueryWrapper() + .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> 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 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 members = parseMemberIds(ticket.getCollabMemberIds()); + one.put("collabMemberIds", members); + one.put("collab_member_ids", members); + one.put("eventException", StringUtils.defaultString(ticket.getEventException())); + Map 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 recordEventException(String workOrderNo, String eventException) { + Map 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> buildEventWatchers(InquiryTicket ticket, List memberIds) { + List> out = new ArrayList<>(); + Set seen = new HashSet<>(); + List 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 rows = staffRoleMapper.selectList(new LambdaQueryWrapper() + .in(InquiryStaffRole::getWecomId, ids) + .eq(InquiryStaffRole::getStatus, "active")); + Map 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 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.emptyList() : memberIds) { + if (StringUtils.isBlank(uid) || seen.contains(uid)) { + continue; + } + InquiryStaffRole row = byId.get(uid); + if (row == null) { + continue; + } + Map 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()); diff --git a/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_hours_decimal_v1.sql b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_hours_decimal_v1.sql new file mode 100644 index 0000000..ca1a1fe --- /dev/null +++ b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_response_timeout_hours_decimal_v1.sql @@ -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=不监测'; diff --git a/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_ticket_event_wait_v1.sql b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_ticket_event_wait_v1.sql new file mode 100644 index 0000000..d01896c --- /dev/null +++ b/inquiry-api/jeecg-module-inquiry/src/main/resources/db/migration_inq_ticket_event_wait_v1.sql @@ -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`; diff --git a/inquiry-backend/src/views/inquiry/systemSetting/index.vue b/inquiry-backend/src/views/inquiry/systemSetting/index.vue index a3d3d85..69fcdd3 100644 --- a/inquiry-backend/src/views/inquiry/systemSetting/index.vue +++ b/inquiry-backend/src/views/inquiry/systemSetting/index.vue @@ -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) {