diff --git a/inquiry-agent/agent/ledger/http_ledger.py b/inquiry-agent/agent/ledger/http_ledger.py
index 92984c8..5a59331 100644
--- a/inquiry-agent/agent/ledger/http_ledger.py
+++ b/inquiry-agent/agent/ledger/http_ledger.py
@@ -118,15 +118,28 @@ class HttpLedger:
facts: dict[str, str],
business_line: str = "",
) -> dict[str, Any]:
+ """
+ 打主账查 TMS。查价可能慢于默认 10s:单独放宽,仍禁止无限等。
+
+ 若仍超时,上层 query_tms_resilient 会回读主账:已 persistHit 有价则按成功续办。
+ """
body: dict[str, Any] = {"work_order_no": work_order_no, "facts": facts}
line = (business_line or str((facts or {}).get("业务线") or "")).strip()
if line:
body["businessLine"] = line
- out = self._post(
- "/inquiry/agent/tms/query",
- body,
- idem=f"tms:{work_order_no}",
- )
+ # TMS 出站 + persistHit 常超过默认 10s,超时会被误判成接口异常。
+ prev_timeout = self._client.timeout_seconds
+ self._client.timeout_seconds = max(float(prev_timeout or 10), 45.0)
+ self._client.close()
+ try:
+ out = self._post(
+ "/inquiry/agent/tms/query",
+ body,
+ idem=f"tms:{work_order_no}",
+ )
+ finally:
+ self._client.timeout_seconds = prev_timeout
+ self._client.close()
if "classification" not in out and not out.get("ok"):
return {
"ok": False,
diff --git a/inquiry-agent/agent/policy/air_text_flow.py b/inquiry-agent/agent/policy/air_text_flow.py
index 0247839..286ca08 100644
--- a/inquiry-agent/agent/policy/air_text_flow.py
+++ b/inquiry-agent/agent/policy/air_text_flow.py
@@ -1212,6 +1212,7 @@ class AirTextInquiryFlow:
from agent.policy.system_exception import (
confirm_tms_quote_if_tech,
query_tms_resilient,
+ recover_tms_query_from_ledger,
)
tms = query_tms_resilient(self._ledger, work_order_no=no, facts=facts)
@@ -1238,12 +1239,17 @@ class AirTextInquiryFlow:
):
self.drop_current_bookmark(sender_id)
return "system_exception"
- if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
- logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
- reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
- sess.phase = "tms_tech"
- self._save(sess)
- return "tms_tech"
+ # 主账已落价但回包超时:按成功续办,禁止再报接口异常
+ recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
+ if recovered is not None:
+ tms = recovered
+ else:
+ if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
+ logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
+ reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
+ sess.phase = "tms_tech"
+ self._save(sess)
+ return "tms_tech"
if not tms.get("has_price"):
# ok=true 才是 TMS 已查过、确认没价
self._emit_tms_miss(reply, no)
diff --git a/inquiry-agent/agent/policy/land_text_flow.py b/inquiry-agent/agent/policy/land_text_flow.py
index 3dd7ac0..527b3de 100644
--- a/inquiry-agent/agent/policy/land_text_flow.py
+++ b/inquiry-agent/agent/policy/land_text_flow.py
@@ -390,7 +390,10 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
_ = retrying
no = (sess.work_order_no or "").strip()
if not tms.get("ok"):
- from agent.policy.system_exception import confirm_tms_quote_if_tech
+ from agent.policy.system_exception import (
+ confirm_tms_quote_if_tech,
+ recover_tms_query_from_ledger,
+ )
if confirm_tms_quote_if_tech(
ledger=self._ledger,
@@ -402,11 +405,15 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
):
self.drop_current_bookmark(sess.sender_id)
return "system_exception"
- reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
- sess.phase = "tms_tech"
- self._save(sess)
- self._park_tms_retry(sess)
- return "tms_tech"
+ recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
+ if recovered is not None:
+ tms = recovered
+ else:
+ reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
+ sess.phase = "tms_tech"
+ self._save(sess)
+ self._park_tms_retry(sess)
+ return "tms_tech"
self._clear_parked_confirm(sess.sender_id)
if not tms.get("has_price"):
self._emit_tms_miss(reply, no)
diff --git a/inquiry-agent/agent/policy/sea_text_flow.py b/inquiry-agent/agent/policy/sea_text_flow.py
index 5bd626e..619c3a2 100644
--- a/inquiry-agent/agent/policy/sea_text_flow.py
+++ b/inquiry-agent/agent/policy/sea_text_flow.py
@@ -416,6 +416,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
from agent.policy.system_exception import (
confirm_tms_quote_if_tech,
query_tms_resilient,
+ recover_tms_query_from_ledger,
)
tms = query_tms_resilient(
@@ -443,12 +444,16 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
):
self.drop_current_bookmark(sender_id)
return "system_exception"
- if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
- logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
- reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
- sess.phase = "tms_tech"
- self._save(sess)
- return "tms_tech"
+ recovered = recover_tms_query_from_ledger(self._ledger, work_order_no=no)
+ if recovered is not None:
+ tms = recovered
+ else:
+ if tms.get("classification") == "TMS_QUERY_INCOMPLETE":
+ logger.warning("tms.query 未出站 wo=%s class=%s", no, tms.get("classification"))
+ reply("TMS 查询失败,请稍后重试。工单已创建:" + no)
+ sess.phase = "tms_tech"
+ self._save(sess)
+ return "tms_tech"
if not tms.get("has_price"):
self._emit_tms_miss(reply, no)
sess.phase = "tms_miss"
diff --git a/inquiry-agent/agent/policy/system_exception.py b/inquiry-agent/agent/policy/system_exception.py
index fc6d4f0..4a53cf2 100644
--- a/inquiry-agent/agent/policy/system_exception.py
+++ b/inquiry-agent/agent/policy/system_exception.py
@@ -310,8 +310,85 @@ def reply_paused_ticket(*, ticket: Any, reply: ReplyFn) -> None:
reply(copy.system_exception_fallback(kind, names=names))
+def _quote_has_usable_total(quote: dict[str, Any] | None) -> bool:
+ """报价包是否已有可用于出卡的总价。"""
+ src = dict(quote or {})
+ total = str(src.get("total") or src.get("salePrice") or src.get("total_amount") or "").strip()
+ return bool(total and total != "-")
+
+
+def recover_tms_query_from_ledger(ledger: Any, *, work_order_no: str) -> dict[str, Any] | None:
+ """
+ 查价超时/失败后回读主账:若已 persistHit 落成已报价+销售价,按查价成功续办。
+
+ 产品口径:主账已经查到价,只是智能体等回包超时,不能标 TMS异常、不能给销售发接口异常,
+ 应直接推当前工单报价。无价才走真正的系统异常。
+ """
+ no = (work_order_no or "").strip()
+ if not no or ledger is None:
+ return None
+ getter = getattr(ledger, "get_for_agent", None)
+ snap: Any = None
+ if callable(getter):
+ try:
+ snap = getter(work_order_no=no, sender_id="")
+ except TypeError:
+ try:
+ snap = getter(work_order_no=no)
+ except Exception: # noqa: BLE001
+ snap = None
+ except Exception: # noqa: BLE001
+ snap = None
+ if not isinstance(snap, dict) or not snap:
+ getter2 = getattr(ledger, "get_ticket", None)
+ if callable(getter2):
+ try:
+ snap = getter2(work_order_no=no)
+ except Exception: # noqa: BLE001
+ snap = None
+ if not isinstance(snap, dict) or not snap:
+ return None
+ status = str(snap.get("status") or "").strip()
+ tms_hit = snap.get("tms_hit") in (1, "1", True, "true")
+ sale = str(snap.get("sale_price") or snap.get("salePrice") or "").strip()
+ quote = dict(snap.get("quote") or {})
+ tms_quote = dict(snap.get("tms_quote") or snap.get("tmsQuote") or {})
+ if not _quote_has_usable_total(quote) and _quote_has_usable_total(tms_quote):
+ quote = dict(tms_quote)
+ if not _quote_has_usable_total(quote) and sale and sale != "-":
+ quote = dict(quote)
+ quote["total"] = sale
+ quote.setdefault("source", "TMS")
+ quote.setdefault("source_label", "TMS标准报价")
+ priced = status == "已报价" and (
+ tms_hit or (sale and sale != "-") or _quote_has_usable_total(quote)
+ )
+ if not priced:
+ return None
+ if not _quote_has_usable_total(quote):
+ return None
+ logger.warning(
+ "tms.query 超时/失败但主账已有价,按成功续办 wo=%s total=%s",
+ no,
+ quote.get("total"),
+ )
+ return {
+ "ok": True,
+ "has_price": True,
+ "classification": "HAS_PRICE",
+ "quote": quote,
+ "recovered": True,
+ "source": "LEDGER_AFTER_TIMEOUT",
+ }
+
+
def query_tms_resilient(ledger: Any, **kwargs: Any) -> dict[str, Any]:
- """TMS 查价:最多 3 次,ok=true(含无价)即成功。"""
+ """
+ TMS 查价:最多 3 次,ok=true(含无价)即成功。
+
+ 三次仍失败时,再读主账:若查价过程中已落库有价(常见于智能体超时、主账已 persistHit),
+ 按成功回包返回,避免误标 TMS异常。
+ """
getter = getattr(ledger, "query_tms", None)
if not callable(getter):
return {"ok": False, "classification": "TMS_TECH_FAILURE", "has_price": False, "quote": {}}
@@ -319,7 +396,14 @@ def query_tms_resilient(ledger: Any, **kwargs: Any) -> dict[str, Any]:
lambda: getter(**kwargs),
is_ok=lambda x: bool((x or {}).get("ok")),
)
- return out if isinstance(out, dict) else {"ok": False, "classification": "TMS_TECH_FAILURE"}
+ result = out if isinstance(out, dict) else {"ok": False, "classification": "TMS_TECH_FAILURE"}
+ if result.get("ok"):
+ return result
+ no = str(kwargs.get("work_order_no") or "").strip()
+ recovered = recover_tms_query_from_ledger(ledger, work_order_no=no) if no else None
+ if recovered is not None:
+ return recovered
+ return result
def confirm_tms_quote_if_tech(
@@ -332,9 +416,20 @@ def confirm_tms_quote_if_tech(
ticket_status: str = "询价中",
facts: dict[str, Any] | None = None,
) -> bool:
- """技术失败则标 TMS异常并回话告警。返回是否已按系统异常处理。"""
+ """
+ 技术失败则标 TMS异常并回话告警。返回是否已按系统异常处理。
+
+ 主账其实已有价时不再标异常(兜底:调用方应优先走 query_tms_resilient 恢复)。
+ """
if not is_tms_tech_failure(tms):
return False
+ # 最后一道闸:主账其实已有价时禁止再打 TMS异常(正常应已在 query_tms_resilient 恢复)。
+ if recover_tms_query_from_ledger(ledger, work_order_no=work_order_no) is not None:
+ logger.warning(
+ "confirm_tms_quote_if_tech 跳过标异常:主账已有价 wo=%s",
+ work_order_no,
+ )
+ return False
err = str(tms.get("classification") or tms.get("error") or "TMS_TECH_FAILURE").strip()
confirm_system_exception(
event=SystemExceptionEvent(
diff --git a/inquiry-agent/agent/quote_templates/match.py b/inquiry-agent/agent/quote_templates/match.py
index 7835248..05e2f6f 100644
--- a/inquiry-agent/agent/quote_templates/match.py
+++ b/inquiry-agent/agent/quote_templates/match.py
@@ -178,7 +178,8 @@ _GENERIC_KEYWORDS = frozenset(
_LAND_KEYWORD_ALIASES = {
"国内拼车": ("国内运输拼车", "拼车"),
"国内零担": ("国内运输拼车", "国内长途/零担", "零担"),
- "国内整车": ("国内运输整车", "整车"),
+ # 「整车」不能当国内整车别名:跨境整车 / 中港整车都含这两个字。
+ "国内整车": ("国内运输整车",),
"跨境整车": ("跨境整车",),
"跨境集拼": ("跨境集拼",),
"跨境零担": ("跨境集拼",),
@@ -198,6 +199,19 @@ def _keyword_needles(kw: str) -> list[str]:
return out
+def _needle_in(text: str, needle: str) -> bool:
+ """
+ 陆运针:普通包含。单独「整车」不命中跨境/中港,避免国内整车模板抢走跨境单。
+ """
+ hay = (text or "").lower()
+ n = (needle or "").strip().lower()
+ if not hay or not n or n not in hay:
+ return False
+ if n == "整车" and ("跨境" in hay or "中港" in hay):
+ return False
+ return True
+
+
def _keyword_score(raw: Any, haystack: str, type_val: str) -> tuple[int, int]:
"""
运输类型上的命中优先,其次全文词长。
@@ -212,11 +226,11 @@ def _keyword_score(raw: Any, haystack: str, type_val: str) -> tuple[int, int]:
if k in _GENERIC_KEYWORDS:
continue
needles = _keyword_needles(kw)
- if type_val and any(n in type_val for n in needles):
+ if type_val and any(_needle_in(type_val, n) for n in needles):
type_hits += 1
length += len(k)
any_hit = True
- elif any(n in haystack for n in needles):
+ elif any(_needle_in(haystack, n) for n in needles):
length += len(k)
any_hit = True
if not any_hit:
diff --git a/inquiry-agent/tests/test_quote_template_match.py b/inquiry-agent/tests/test_quote_template_match.py
index 6b0cdcb..88095fe 100644
--- a/inquiry-agent/tests/test_quote_template_match.py
+++ b/inquiry-agent/tests/test_quote_template_match.py
@@ -143,6 +143,61 @@ class QuoteTemplateMatchTests(unittest.TestCase):
self.assertIsNotNone(hit)
self.assertEqual(hit.name, "跨境整车")
+ def test_land_admin_domestic_ftl_keyword_does_not_steal_cross(self) -> None:
+ """后台「国内整车」不得靠「整车」二字抢走跨境整车(WO202610030032)。"""
+ hit = pick_quote_template(
+ templates=[
+ {
+ "templateId": "dom",
+ "name": "国内陆运整车报价单",
+ "bizType": "陆运",
+ "keywords": ["国内整车"],
+ "isDefault": False,
+ },
+ {
+ "templateId": "xb",
+ "name": "跨境整车报价单",
+ "bizType": "陆运",
+ "keywords": ["东南亚"],
+ "isDefault": False,
+ },
+ ],
+ business_line="LAND",
+ facts={
+ "运输类型": "跨境整车",
+ "线路类别": "东南亚",
+ "运输分类": "普件(东南亚、中亚、中欧涉及国内+国外段)",
+ "起运港": "上海",
+ "目的港": "春疆货场",
+ },
+ )
+ self.assertIsNotNone(hit)
+ self.assertEqual(hit.name, "跨境整车报价单")
+
+ def test_land_admin_domestic_ftl_still_hits_domestic_type(self) -> None:
+ hit = pick_quote_template(
+ templates=[
+ {
+ "templateId": "dom",
+ "name": "国内陆运整车报价单",
+ "bizType": "陆运",
+ "keywords": ["国内整车"],
+ "isDefault": False,
+ },
+ {
+ "templateId": "xb",
+ "name": "跨境整车报价单",
+ "bizType": "陆运",
+ "keywords": ["东南亚"],
+ "isDefault": False,
+ },
+ ],
+ business_line="LAND",
+ facts={"运输类型": "国内运输整车", "线路类别": "国内长途/零担"},
+ )
+ self.assertIsNotNone(hit)
+ self.assertEqual(hit.name, "国内陆运整车报价单")
+
def test_land_falls_back_to_default(self) -> None:
hit = pick_quote_template(
templates=[
diff --git a/inquiry-agent/tests/test_system_exception.py b/inquiry-agent/tests/test_system_exception.py
index aa7fa40..be14d8a 100644
--- a/inquiry-agent/tests/test_system_exception.py
+++ b/inquiry-agent/tests/test_system_exception.py
@@ -22,8 +22,11 @@ from agent.policy.system_exception import (
SystemExceptionEvent,
call_with_retries,
confirm_system_exception,
+ confirm_tms_quote_if_tech,
extract_work_order_nos,
is_tms_tech_failure,
+ query_tms_resilient,
+ recover_tms_query_from_ledger,
should_mark_ticket,
ticket_is_paused,
)
@@ -196,6 +199,89 @@ class TestSystemExceptionRules(unittest.TestCase):
self.assertEqual(out.get("classification"), "TMS_TECH_FAILURE")
+class TestTmsTimeoutRecoverPriced(unittest.TestCase):
+ """查到价但智能体超时:主账已有价则按成功续办,不标 TMS异常。"""
+
+ def setUp(self) -> None:
+ self.ledger = MemoryLedger()
+ created = self.ledger.create_ticket(
+ sender_id="sales1",
+ business_line="AIR",
+ facts={"起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶"},
+ )
+ self.wo = str(created.get("work_order_no") or "")
+ self.ledger.upsert_quote(
+ work_order_no=self.wo,
+ quote={
+ "total": "CNY 5001.00",
+ "currency": "CNY",
+ "source": "TMS",
+ "source_label": "TMS标准报价",
+ },
+ to_status="已报价",
+ )
+
+ def test_recover_from_ledger_when_already_quoted(self) -> None:
+ got = recover_tms_query_from_ledger(self.ledger, work_order_no=self.wo)
+ self.assertIsNotNone(got)
+ assert got is not None
+ self.assertTrue(got.get("ok"))
+ self.assertTrue(got.get("has_price"))
+ self.assertTrue(got.get("recovered"))
+ self.assertIn("5001", str((got.get("quote") or {}).get("total") or ""))
+
+ def test_query_resilient_recovers_after_tech_fail(self) -> None:
+ self.ledger.query_tms = ( # type: ignore[method-assign]
+ lambda **_kwargs: {
+ "ok": False,
+ "classification": "TMS_TECH_FAILURE",
+ "has_price": False,
+ "quote": {},
+ }
+ )
+ out = query_tms_resilient(
+ self.ledger, work_order_no=self.wo, facts={"起运港": "南京", "目的港": "吉隆坡"}
+ )
+ self.assertTrue(out.get("ok"))
+ self.assertTrue(out.get("has_price"))
+ self.assertTrue(out.get("recovered"))
+ self.assertEqual(out.get("classification"), "HAS_PRICE")
+
+ def test_confirm_skips_mark_when_ledger_priced(self) -> None:
+ replies: list[str] = []
+ marked = confirm_tms_quote_if_tech(
+ ledger=self.ledger,
+ reply=replies.append,
+ work_order_no=self.wo,
+ tms={"ok": False, "classification": "TMS_TECH_FAILURE"},
+ sender_id="sales1",
+ ticket_status="询价中",
+ )
+ self.assertFalse(marked)
+ self.assertEqual(replies, [])
+ snap = self.ledger.get_for_agent(work_order_no=self.wo, sender_id="sales1")
+ self.assertFalse(str(snap.get("system_exception") or "").strip())
+
+ def test_true_tech_fail_still_marks_when_no_price(self) -> None:
+ created = self.ledger.create_ticket(
+ sender_id="sales2",
+ business_line="AIR",
+ facts={"起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶"},
+ )
+ wo2 = str(created.get("work_order_no") or "")
+ replies: list[str] = []
+ marked = confirm_tms_quote_if_tech(
+ ledger=self.ledger,
+ reply=replies.append,
+ work_order_no=wo2,
+ tms={"ok": False, "classification": "TMS_TECH_FAILURE"},
+ sender_id="sales2",
+ ticket_status="询价中",
+ )
+ self.assertTrue(marked)
+ self.assertTrue(any("TMS接口异常" in x for x in replies))
+
+
class TestConfirmSystemException(unittest.TestCase):
def setUp(self) -> None:
self.ledger = MemoryLedger()
diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryDashboardServiceImpl.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryDashboardServiceImpl.java
index 7e21429..bed9e9a 100644
--- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryDashboardServiceImpl.java
+++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/service/impl/InquiryDashboardServiceImpl.java
@@ -5,10 +5,14 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.inquiry.domain.TicketStatuses;
+import org.jeecg.modules.inquiry.entity.InquiryQuote;
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
import org.jeecg.modules.inquiry.entity.InquiryTicket;
+import org.jeecg.modules.inquiry.entity.InquiryTicketStatusLog;
+import org.jeecg.modules.inquiry.mapper.InquiryQuoteMapper;
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
+import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
import org.jeecg.modules.inquiry.service.IInquiryDashboardService;
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +26,7 @@ import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
@@ -34,8 +39,11 @@ import java.util.stream.Collectors;
/**
* 看板实时聚合实现(无快照依赖)。
*
- * 计算口径:询价总数、待处理、TMS 命中率、异常完成率、手工报价占比、
- * 已成交、成交率、反馈率;内部协同时长本期无协同表时返回「-」。
+ * 九指标与原型看板一致(5+4 栅格):询价总量、报价工单、标准报价命中率、
+ * 平均报价销售额、手工工单占比、待处理工单、报价成功率、报价有效率、
+ * 内部报价平均响应。问号 tip 只写计算公式,禁止「本期无协同明细 / N单无勾选…」占位。
+ * 内部报价平均响应 = AVG(首次 PRODUCT 报价时间 − 分发时间),
+ * 分发=bind_collab_group/activate_air_group。
* 人员排名与工单列表同一套发起人解析(企微 id → 员工姓名),禁止只认库内空 sales_name。
* 成本趋势:近 7 日工单按线路聚合均价(成本价优先,缺省用销售价),非浏览器直连 TMS。
* 线程:只读查询,无写锁。
@@ -46,6 +54,9 @@ public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("MM-dd");
private static final int COST_ROUTE_LIMIT = 16;
+ /** 协同「分发」:海运/陆运建群、空运群激活。 */
+ private static final List COLLAB_DISPATCH_ACTIONS = Arrays.asList(
+ "bind_collab_group", "activate_air_group");
private static final String[] COST_COLORS = {
"#2563eb", "#3b82f6", "#0891b2", "#0e7490", "#6366f1", "#8b5cf6",
"#a855f7", "#c026d3", "#db2777", "#e11d48", "#dc2626", "#ea580c",
@@ -62,6 +73,10 @@ public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
private InquiryTicketMapper ticketMapper;
@Autowired
private InquiryStaffRoleMapper staffRoleMapper;
+ @Autowired
+ private InquiryTicketStatusLogMapper statusLogMapper;
+ @Autowired
+ private InquiryQuoteMapper quoteMapper;
@Override
public Map metrics(String range) {
@@ -94,28 +109,24 @@ public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
long pending = total - deal - lost - closed;
long tmsHit = tickets.stream().filter(t -> t.getTmsHit() != null && t.getTmsHit() == 1).count();
long manual = tickets.stream().filter(t -> t.getManualQuote() != null && t.getManualQuote() == 1).count();
- long hasEventExc = tickets.stream().filter(t -> hasExc(t.getEventException())).count();
- long hasSysExc = tickets.stream().filter(t -> hasExc(t.getSystemException())).count();
- long abnormalTotal = hasEventExc + hasSysExc;
- // 简化:已关闭且曾有异常视为异常处理完成
- long abnormalDone = tickets.stream()
- .filter(t -> TicketStatuses.CLOSED.equals(t.getStatus()))
- .filter(t -> hasExc(t.getEventException()) || hasExc(t.getSystemException())
- || (t.getCloseReason() != null && t.getCloseReason().contains("异常")))
- .count();
+
+ String avgResp = avgCollabResponse(tickets);
+ String avgSale = avgHighlightedSale(tickets);
List