落地陆运群内协同:按填写样例拉群并只列本单协同项,进群把私聊已抽到的字段带上。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 13:38:02 +08:00
co-authored by Cursor
parent be95d823a2
commit 982b3a2317
25 changed files with 1451 additions and 315 deletions
@@ -19,6 +19,8 @@ public final class TmsLandEnumMapper {
TYPE.put("国内运输整车", "DOMESTIC_FTL");
TYPE.put("跨境集拼", "CROSS_BORDER_LTL");
TYPE.put("跨境整车", "CROSS_BORDER_FTL");
TYPE.put("中港整车", "CHINA_HK");
TYPE.put("中港零担/集拼", "CHINA_HK");
TYPE.put("中港", "CHINA_HK");
TYPE.put("DOMESTIC_LTL", "DOMESTIC_LTL");
TYPE.put("DOMESTIC_FTL", "DOMESTIC_FTL");
@@ -15,9 +15,10 @@ import java.util.regex.Pattern;
/**
* 陆运 TMS 查价段:按运输类型拼出站字段。
* <p>
* 职责:把工单 facts 里的中文键写成 TMS 段;运输分类、中港装载方式只留在工单,不进出站。
* 拼车/跨境集拼带体积重量;国内整车/跨境整车带「车型/数量」一项再拆成出站车型与台数;
* 中港整车不带车型/数量,中港零担/集拼才带。通关口岸有值才放。
* 职责:把工单 facts 里的中文键写成 TMS 段;运输分类只留在工单,不进出站。
* 拼车/跨境集拼带体积重量;国内整车/跨境整车/中港零担带「车型/数量」一项再拆成出站车型与台数;
* 中港整车不带车型/数量。通关口岸有值才放。
* 中港整车、中港零担/集拼出站类型都是 CHINA_HK。
* 类型/线路出站必须是 TMS 6.1.1 英文枚举,线路键名 routeCategory。
* 禁止:按空运要包装件数;把分类当查价条件;直连 TMS。
*/
@@ -26,9 +27,9 @@ public final class TmsLandSegmentMapper {
/** 体积+重量那一套。 */
private static final Set<String> LCL = new HashSet<String>(
Arrays.asList("国内运输拼车", "跨境集拼"));
/** 车型/数量那一套。中港不在此列,按装载方式另判。 */
/** 车型/数量那一套。中港零担走这里;中港整车另判。 */
private static final Set<String> FTL = new HashSet<String>(
Arrays.asList("国内运输整车", "跨境整车"));
Arrays.asList("国内运输整车", "跨境整车", "中港零担/集拼"));
private static final Pattern VEHICLE_SPLIT = Pattern.compile("\\s*[/xX×**]\\s*");
private static final Pattern TAIL_QTY = Pattern.compile("^(.*?)[\\s,,]+(\\d+)\\s*(台|辆)?$");
@@ -37,7 +38,7 @@ public final class TmsLandSegmentMapper {
}
/**
* @param facts 工单/智能体中文 facts(含运输类型、线路类别、货物、中港装载方式)
* @param facts 工单/智能体中文 facts(含运输类型、线路类别、货物)
* @return 可并入查价 segment 的键;不含 landClass / hkLoadType
*/
public static Map<String, Object> build(Map<String, Object> facts) {
@@ -45,7 +46,6 @@ public final class TmsLandSegmentMapper {
Map<String, Object> seg = new LinkedHashMap<String, Object>();
String type = text(source.get("运输类型"));
String route = text(source.get("线路类别"));
String hkLoad = text(source.get("中港装载方式"));
put(seg, "landTransportType", TmsLandEnumMapper.transportType(type));
put(seg, "routeCategory", TmsLandEnumMapper.routeCategory(route));
put(seg, "origin", text(source.get("起运港")));
@@ -54,9 +54,9 @@ public final class TmsLandSegmentMapper {
if (LCL.contains(type)) {
put(seg, "volumeCbm", TmsV2RequestMapper.asPositiveDecimal(source.get("体积")));
put(seg, "weightKg", TmsV2RequestMapper.asPositiveDecimal(source.get("毛重")));
} else if ("中港".equals(type) && !"零担/集拼".equals(hkLoad)) {
} else if ("中港整车".equals(type)) {
// 中港整车:询价不要车型/数量,出站也不带
} else if (FTL.contains(type) || "中港".equals(type)) {
} else if (FTL.contains(type)) {
putVehicleQty(source, seg);
}
String customs = text(source.get("通关口岸"));
@@ -68,9 +68,9 @@ public final class TmsLandSegmentMapper {
/**
* 陆运出站拦网:工单号、报价日期、起运目的品名、类型、线路,再加当前类型货物。
* 不要包装、不要运输分类、不要中港装载。中港整车不要车型/数量。
* 不要包装、不要运输分类。中港整车不要车型/数量。
*
* @param facts 用于读中港装载方式;禁止把装载方式写进 segment
* @param facts 用于读运输类型;禁止把分类写进 segment
*/
public static List<String> missingQuery(String workOrderNo, String quoteDate,
Map<String, Object> segment,
@@ -93,14 +93,14 @@ public final class TmsLandSegmentMapper {
}
String typeEnum = text(seg.get("landTransportType"));
String cnType = text(facts == null ? null : facts.get("运输类型"));
String hkLoad = text(facts == null ? null : facts.get("中港装载方式"));
boolean lcl = LCL.contains(cnType)
|| "DOMESTIC_LTL".equals(typeEnum)
|| "CROSS_BORDER_LTL".equals(typeEnum);
boolean hk = "中港".equals(cnType) || "CHINA_HK".equals(typeEnum);
boolean hkFull = "中港整车".equals(cnType);
boolean ftl = FTL.contains(cnType)
|| "DOMESTIC_FTL".equals(typeEnum)
|| "CROSS_BORDER_FTL".equals(typeEnum);
|| "CROSS_BORDER_FTL".equals(typeEnum)
|| "中港零担/集拼".equals(cnType);
if (lcl) {
if (seg.get("volumeCbm") == null) {
missing.add("volumeCbm");
@@ -108,9 +108,9 @@ public final class TmsLandSegmentMapper {
if (seg.get("weightKg") == null) {
missing.add("weightKg");
}
} else if (hk && !"零担/集拼".equals(hkLoad)) {
} else if (hkFull) {
// 中港整车不拦车型/数量
} else if (ftl || hk) {
} else if (ftl) {
if (blank(seg.get("vehicleType"))) {
missing.add("vehicleType");
}
@@ -15,6 +15,12 @@ public class TmsLandEnumMapperTest {
assertEquals("DOMESTIC_CITY_DIST", TmsLandEnumMapper.routeCategory("国内城配/拖车/打包"));
}
@Test
public void hkSplitTypesBothMapChinaHk() {
assertEquals("CHINA_HK", TmsLandEnumMapper.transportType("中港整车"));
assertEquals("CHINA_HK", TmsLandEnumMapper.transportType("中港零担/集拼"));
}
@Test
public void alreadyEnumStays() {
assertEquals("DOMESTIC_LTL", TmsLandEnumMapper.transportType("DOMESTIC_LTL"));
@@ -38,15 +38,15 @@ public class TmsLandSegmentMapperTest {
@Test
public void landFtlUsesVehicleNotVolume() {
Map<String, Object> facts = new HashMap<String, Object>();
facts.put("运输类型", "中港");
facts.put("运输类型", "中港零担/集拼");
facts.put("线路类别", "中港/中亚/中欧");
facts.put("中港装载方式", "零担/集拼");
facts.put("起运港", "广州");
facts.put("目的港", "香港");
facts.put("品名", "衣服");
facts.put("车型/数量", "9米6/1");
facts.put("体积", "99");
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
assertEquals("CHINA_HK", seg.get("landTransportType"));
assertEquals("9米6", seg.get("vehicleType"));
assertNull(seg.get("volumeCbm"));
assertFalse(seg.containsKey("hkLoadType"));
@@ -56,13 +56,13 @@ public class TmsLandSegmentMapperTest {
@Test
public void hkFullTruckOmitsVehicle() {
Map<String, Object> facts = new HashMap<String, Object>();
facts.put("运输类型", "中港");
facts.put("运输类型", "中港整车");
facts.put("线路类别", "中港/中亚/中欧");
facts.put("中港装载方式", "整车");
facts.put("起运港", "广州");
facts.put("目的港", "香港");
facts.put("品名", "衣服");
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
assertEquals("CHINA_HK", seg.get("landTransportType"));
assertNull(seg.get("vehicleType"));
assertNull(seg.get("quantity"));
java.util.List<String> missing = TmsLandSegmentMapper.missingQuery(