落地空运单聊询价字段与线路选择。

必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 15:02:32 +08:00
co-authored by Cursor
parent 8afe909230
commit cfbce2fd69
53 changed files with 3470 additions and 372 deletions
+129 -2
View File
@@ -79,7 +79,7 @@ class AirGroupCollabTests(unittest.TestCase):
]
)
def _create_air(self, *, sender="sales1", quote=None, status="已报价") -> str:
def _create_air(self, *, sender="sales1", quote=None, status="已报价", quote_date="2026-09-16") -> str:
created = self.ledger.create_ticket(
sender_id=sender,
business_line="AIR",
@@ -91,7 +91,7 @@ class AirGroupCollabTests(unittest.TestCase):
"毛重": "100",
"体积": "2",
"包装类型": "纸箱",
"报价日期": "2026-09-16",
"报价日期": quote_date,
},
)
wo = created["work_order_no"]
@@ -122,6 +122,25 @@ class AirGroupCollabTests(unittest.TestCase):
def _texts(self) -> list[str]:
return [x.get("content") or "" for x in self.groups.sent]
def test_attach_bot_client_keeps_req_id_for_quote(self) -> None:
"""这次 @ 的 req_id 要带给桥,群里才能引用原消息。"""
from agent.channel.aibot.reply import AibotReplyClient
from agent.handlers.air_group_collab import _attach_bot_client
engine = AirTextInquiryFlow(ledger=self.ledger, group_client=None)
msg = InboundMessage(
sender_id="air1",
message_id="m_quote",
content="锁舱",
chat_id="wr_air_1",
chat_type="group",
raw={"source": "aibot", "aibot_req_id": "cb_lock_1"},
)
_attach_bot_client(engine, msg)
client = engine.group_client()
self.assertIsInstance(client, AibotReplyClient)
self.assertEqual(client._callback_req_id, "cb_lock_1")
def test_intent_lock_is_not_hardcoded(self) -> None:
self.assertEqual(classify_air_group_text("帮我锁舱 WO202609160001", allow_network=False), INTENT_LOCK)
self.assertEqual(classify_air_group_text("WO202609160001释放舱位", allow_network=False), INTENT_RELEASE)
@@ -495,6 +514,7 @@ class AirGroupCollabTests(unittest.TestCase):
)
self.assertEqual(phase, "lock_ok")
self.assertTrue(any("执行成功" in t for t in self._texts()))
self.assertTrue(any("舱位选项:AIR-OPT-01" in t for t in self._texts()))
self.assertFalse(any("还没有可锁的 TMS 报价" in t for t in self._texts()))
def test_manual_quote_without_quote_id_can_lock(self) -> None:
@@ -884,6 +904,113 @@ class AirGroupCollabTests(unittest.TestCase):
)
self.assertEqual(phase, "not_air")
def test_lock_fail_tells_operator_and_tms_reason(self) -> None:
"""锁舱失败群里要回操作人和 TMS 原因,不能被系统异常兜底盖掉。"""
wo = self._create_air()
self.ledger._cabin_lock_result = {
"ok": False,
"error": "lock_fail",
"failReason": "该航班日期不存在可锁定的配载记录",
}
phase = handle_air_group(
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_fail", mid="lf1"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_lock",
)
self.assertEqual(phase, "system_exception")
joined = "\n".join(self._texts())
self.assertIn(f"工单{wo} 空运锁舱失败。", joined)
self.assertIn("操作人:HangXian", joined)
self.assertIn("锁舱失败的原因:该航班日期不存在可锁定的配载记录", joined)
self.assertIn("已通知IT运维排查,请稍等。", joined)
self.assertNotIn("TMS接口异常,IT运维排查中", joined)
self.assertNotIn("报价选项不存在", joined)
self.assertNotIn("与工单不匹配", joined)
def test_lock_without_quote_still_shows_tms_reason(self) -> None:
"""没有报价也可以锁舱;群里只回 TMS 原因,不要说选项不匹配。"""
wo = self._create_air(quote={}, status="询价中")
self.ledger._cabin_lock_result = {
"ok": False,
"error": "lock_fail",
"failReason": "该航班日期不存在可锁定的配载记录",
}
phase = handle_air_group(
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_no_quote", mid="lnq1"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_lock",
)
self.assertEqual(phase, "system_exception")
joined = "\n".join(self._texts())
self.assertIn("锁舱失败的原因:该航班日期不存在可锁定的配载记录", joined)
self.assertNotIn("报价选项不存在", joined)
self.assertNotIn("与工单不匹配", joined)
def test_paused_lock_retries_and_shows_tms_reason(self) -> None:
"""上次锁舱失败标了系统异常后,再锁仍要打 TMS,不能只回兜底。"""
wo = self._create_air()
self.ledger.mark_system_exception(
work_order_no=wo,
system_exception="TMS",
system_exception_reason="空运锁舱接口调用失败",
step="tms_lock",
payload={},
)
hits = {"n": 0}
orig = self.ledger.lock_cabin
def counted(**kwargs):
hits["n"] += 1
return orig(**kwargs)
self.ledger.lock_cabin = counted # type: ignore[method-assign]
self.ledger._cabin_lock_result = {
"ok": False,
"error": "lock_fail",
"failReason": "该航班日期不存在可锁定的配载记录",
}
phase = handle_air_group(
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_paused", mid="lp1"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_lock",
)
self.assertNotEqual(phase, "system_exception_paused")
self.assertGreaterEqual(hits["n"], 1)
joined = "\n".join(self._texts())
self.assertIn("锁舱失败的原因:该航班日期不存在可锁定的配载记录", joined)
self.assertNotIn("TMS接口异常,IT运维排查中", joined)
def test_release_fail_tells_operator_and_tms_reason(self) -> None:
"""释放失败同理:操作人、原因、已通知IT,不要系统异常兜底。"""
wo = self._create_air()
handle_air_group(
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_rl_fail", mid="rf0"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_lock",
)
self.ledger._cabin_release_result = {
"ok": False,
"error": "release_fail",
"failReason": "舱位不存在",
}
phase = handle_air_group(
self._msg(sender="air1", text=f"{wo}释放舱位", chat="wr_air_rl_fail", mid="rf1"),
flow=self.flow,
ledger=self.ledger,
injected_intent="air_release",
)
self.assertEqual(phase, "system_exception")
joined = "\n".join(self._texts())
self.assertIn(f"工单{wo} 空运释放舱位失败。", joined)
self.assertIn("操作人:HangXian", joined)
self.assertIn("释放舱位失败的原因:舱位不存在", joined)
self.assertIn("已通知IT运维排查,请稍等。", joined)
self.assertNotIn("TMS接口异常,IT运维排查中", joined)
if __name__ == "__main__":
unittest.main()