落地多段联运私聊:齐了才建单拉群,附件只收格子值,核对改正不再整单重开。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 15:08:15 +08:00
co-authored by Cursor
parent 6e3e09a316
commit 2334524bb8
30 changed files with 4033 additions and 40 deletions
@@ -224,6 +224,22 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
return out;
}
/**
* 按企微 userid 取员工管理姓名。建单和智能体读单都走这里,禁止把岗位词「销售」当人名。
* 查不到返回空,调用方再用 userid。副作用:只读员工表。
*/
private String resolveStaffDisplayName(String salesWecomId) {
String uid = StringUtils.trimToEmpty(salesWecomId);
if (uid.isEmpty()) {
return "";
}
InquiryStaffRole row = staffRoleMapper.selectOne(new LambdaQueryWrapper<InquiryStaffRole>()
.eq(InquiryStaffRole::getWecomId, uid)
.eq(InquiryStaffRole::getStatus, "active")
.last("LIMIT 1"));
return row == null ? "" : StringUtils.trimToEmpty(row.getName());
}
/** 批量按企微 userid 取员工姓名,避免列表 N+1。 */
private Map<String, String> loadStaffNames(List<InquiryTicket> tickets) {
Set<String> wecomIds = new HashSet<>();
@@ -479,6 +495,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
InquiryTicket ticket = new InquiryTicket();
ticket.setWorkOrderNo(nextWorkOrderNo());
ticket.setSalesWecomId(req.getSalesWecomId());
ticket.setSalesName(resolveStaffDisplayName(req.getSalesWecomId()));
ticket.setBusinessLine(req.getBusinessLine().trim().toUpperCase());
ticket.setBizTypeLabel(bizLabel(ticket.getBusinessLine()));
ticket.setOrigin(facts.get("起运港"));
@@ -805,8 +822,13 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("ok", true);
out.put("status", status);
out.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
out.put("sales_name", StringUtils.defaultString(ticket.getSalesName()));
out.put("salesName", StringUtils.defaultString(ticket.getSalesName()));
String salesWecomId = StringUtils.defaultString(ticket.getSalesWecomId());
out.put("salesWecomId", salesWecomId);
out.put("sales_wecom_id", salesWecomId);
String salesName = InquiryTicketDisplaySupport.resolveSalesDisplayName(
ticket.getSalesName(), salesWecomId, resolveStaffDisplayName(salesWecomId));
out.put("sales_name", salesName);
out.put("salesName", salesName);
out.put("facts", facts);
out.put("quote", quote);
out.put("tms_quote", tmsQuote);
@@ -137,6 +137,40 @@ public final class QuoteTemplateMatcher {
/**
* 业务线名、过宽的「国内」不当关键词。
*/
/**
* 后台陆运模板写「国内拼车」,询价字段是「国内运输拼车」。
*/
private static final Map<String, String[]> LAND_KEYWORD_ALIASES = new LinkedHashMap<>();
static {
LAND_KEYWORD_ALIASES.put("国内拼车", new String[] {"国内运输拼车", "拼车"});
LAND_KEYWORD_ALIASES.put("国内零担", new String[] {"国内运输拼车", "国内长途/零担", "零担"});
LAND_KEYWORD_ALIASES.put("国内整车", new String[] {"国内运输整车", "整车"});
LAND_KEYWORD_ALIASES.put("跨境整车", new String[] {"跨境整车"});
LAND_KEYWORD_ALIASES.put("跨境集拼", new String[] {"跨境集拼"});
LAND_KEYWORD_ALIASES.put("跨境零担", new String[] {"跨境集拼"});
}
static boolean hitNeedle(String text, String kw) {
if (text == null || text.isEmpty() || kw == null || kw.isEmpty()) {
return false;
}
String hay = text.toLowerCase(Locale.ROOT);
if (hay.contains(kw.toLowerCase(Locale.ROOT))) {
return true;
}
String[] aliases = LAND_KEYWORD_ALIASES.get(kw);
if (aliases == null) {
return false;
}
for (String alias : aliases) {
if (hay.contains(alias.toLowerCase(Locale.ROOT))) {
return true;
}
}
return false;
}
static boolean skipKeyword(String kw) {
if (kw == null || kw.isEmpty()) {
return true;
@@ -161,11 +195,11 @@ public final class QuoteTemplateMatcher {
continue;
}
String k = kw.toLowerCase(Locale.ROOT);
if (!type.isEmpty() && type.contains(k)) {
if (hitNeedle(type, kw) ) {
typeHits++;
length += k.length();
any = true;
} else if (hay.contains(k)) {
} else if (hitNeedle(hay, kw)) {
length += k.length();
any = true;
}
@@ -133,6 +133,21 @@ public class QuoteTemplateMatcherTest {
assertEquals("跨境整车", hit.get("name"));
}
@Test
public void landAdminKeywordDomesticLclMatchesTransportType() {
List<Map<String, Object>> list = new ArrayList<>();
list.add(tpl("中港", "陆运", false, "中港", "陆运"));
list.add(tpl("陆运国内运输拼车报价单", "陆运", false, "国内拼车", "国内零担"));
Map<String, Object> facts = new HashMap<>();
facts.put("运输类型", "国内运输拼车");
facts.put("起运港", "广州");
facts.put("目的港", "深圳");
Map<String, Object> hit = QuoteTemplateMatcher.pick(list, "LAND", facts, null);
assertNotNull(hit);
assertEquals("keyword", hit.get("matchType"));
assertEquals("陆运国内运输拼车报价单", hit.get("name"));
}
@Test
public void landFallsBackToDefault() {
List<Map<String, Object>> list = new ArrayList<>();