空运未填报价日期时不默认当天,查出舱位后由销售点选再写回日期。

包装类型里的托盘不再被抄进增值服务。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 18:35:52 +08:00
co-authored by Cursor
parent dda210c219
commit 7ef4a1b1cf
21 changed files with 1068 additions and 58 deletions
@@ -121,14 +121,15 @@ public class InquiryTmsQueryService {
segment.put("cargoReadyDate", first(facts.get("cargoReadyDate"), facts.get("货好时间")));
segment.put("placeOfReceipt", first(facts.get("placeOfReceipt"), facts.get("提货地址")));
segment.put("deliveryAddress", first(facts.get("deliveryAddress"), facts.get("收货地址")));
// 工单 added_services 在这里收成列表,后面的 mapper 原样放到 TMS addedServices。
List<String> addedServices = TmsV2RequestMapper.splitAddedServices(first(
facts.get("addedServices"), facts.get("added_services"), facts.get("增值服务")));
if (!addedServices.isEmpty()) {
segment.put("addedServices", addedServices);
}
}
String quoteDate = TmsV2RequestMapper.normalizeQuoteDate(facts.get("报价日期"));
if (quoteDate == null) {
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
fmt.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
quoteDate = fmt.format(new Date());
}
String quoteDate = resolveOutboundQuoteDate(line, facts.get("报价日期"));
List<String> missing;
if ("SEA".equals(line)) {
@@ -162,6 +163,10 @@ public class InquiryTmsQueryService {
String cacheKey = line + ":" + quoteDate + ":" + origin + ":" + destination
+ ":" + segment.get("weightKg") + ":" + segment.get("volumeCbm");
if ("AIR".equals(line) && segment.get("addedServices") != null) {
// 增值服务不同不能共用同一条运价缓存。
cacheKey += ":" + segment.get("addedServices");
}
if ("LAND".equals(line)) {
cacheKey += ":" + segment.get("landTransportType") + ":" + segment.get("routeCategory")
+ ":" + segment.get("vehicleType") + ":" + segment.get("quantity");
@@ -1324,17 +1329,36 @@ public class InquiryTmsQueryService {
return raw == null || String.valueOf(raw).trim().isEmpty();
}
/**
* 查价出站用的报价日期。
*
* 海运、陆运没说则填上海当天。空运没说保持 null,不传 quoteDate,
* 让 TMS 一次返回各航班日期的舱位。销售说了的日期原样规范化,不用当天盖掉。
*/
static String resolveOutboundQuoteDate(String line, Object raw) {
String quoteDate = TmsV2RequestMapper.normalizeQuoteDate(raw);
if (quoteDate != null) {
return quoteDate;
}
if ("AIR".equals(line)) {
return null;
}
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
fmt.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
return fmt.format(new Date());
}
/**
* 空运出站齐套检查。地名栏允许空串,禁止再拿 destination 拦查价。
* 报价日期非必填,空着也算出站齐套。
*/
static List<String> missingAirQuery(String workOrderNo, String quoteDate, Map<String, Object> segment) {
List<String> missing = new ArrayList<>();
if (workOrderNo == null || !workOrderNo.matches("WO\\d{12}")) {
missing.add("workOrderNo");
}
if (quoteDate == null || quoteDate.isEmpty()) {
missing.add("quoteDate");
}
// 报价日期非必填。空着不计入 missing;参数留给已有调用方,避免再按缺日期拦截。
log.debug("air.query dateOptional len={}", quoteDate == null ? 0 : quoteDate.length());
for (String key : new String[] {
"cargoName", "quantity", "weightKg", "volumeCbm", "packageType"
}) {
@@ -115,6 +115,12 @@ public final class TmsV2RequestMapper {
put(target, "cargoReadyDate", first(source, "cargoReadyDate", "货好时间"));
put(target, "placeOfReceipt", first(source, "placeOfReceipt", "提货地址"));
put(target, "deliveryAddress", first(source, "deliveryAddress", "收货地址"));
// 有增值服务才传列表。空列表不能出站,否则 TMS 会当成带了服务条件。
List<String> addedServices = splitAddedServices(
first(source, "addedServices", "added_services", "增值服务"));
if (!addedServices.isEmpty()) {
target.put("addedServices", addedServices);
}
}
/**
@@ -163,6 +169,40 @@ public final class TmsV2RequestMapper {
put(target, "customsPort", first(source, "customsPort"));
}
/**
* 空运增值服务拆成多项。
* <p>
* 工单里是 added_services 一句话,TMS 要 addedServices 列表。
* 顿号、中文逗号、英文逗号都拆;空格不拆,MUST GO 仍是一项。
* 已经是列表则逐项原样保留。空项丢掉,不改词。
*/
public static List<String> splitAddedServices(Object raw) {
List<String> out = new ArrayList<>();
if (raw instanceof List) {
for (Object item : (List<?>) raw) {
addServiceToken(out, item);
}
return out;
}
String text = textOf(raw);
if (text.isEmpty()) {
return out;
}
for (String part : text.split("[、,,]")) {
addServiceToken(out, part);
}
return out;
}
/** 去掉空白后并入列表。同一词只留第一次。 */
private static void addServiceToken(List<String> out, Object raw) {
String token = textOf(raw);
if (token.isEmpty() || out.contains(token)) {
return;
}
out.add(token);
}
private static String textOf(Object raw) {
return raw == null ? "" : String.valueOf(raw).trim();
}
@@ -92,4 +92,27 @@ public class InquiryTmsQueryServiceTest {
"WO202609220004", "2026-09-25", segment);
org.junit.jupiter.api.Assertions.assertTrue(missing.isEmpty(), String.valueOf(missing));
}
@Test
public void airQueryAllowsBlankQuoteDate() {
// 空运没填报价日期也算出站齐套,不能再补成当天才放行。
java.util.Map<String, Object> segment = new java.util.HashMap<String, Object>();
segment.put("cargoName", "茶叶");
segment.put("quantity", 1);
segment.put("weightKg", "1.5");
segment.put("volumeCbm", "100");
segment.put("packageType", "托盘");
java.util.List<String> missing = InquiryTmsQueryService.missingAirQuery(
"WO202609220004", null, segment);
org.junit.jupiter.api.Assertions.assertTrue(missing.isEmpty(), String.valueOf(missing));
org.junit.jupiter.api.Assertions.assertNull(
InquiryTmsQueryService.resolveOutboundQuoteDate("AIR", null));
org.junit.jupiter.api.Assertions.assertEquals(
"2026-10-08",
InquiryTmsQueryService.resolveOutboundQuoteDate("AIR", "2026-10-08"));
org.junit.jupiter.api.Assertions.assertNotNull(
InquiryTmsQueryService.resolveOutboundQuoteDate("SEA", null));
org.junit.jupiter.api.Assertions.assertNotNull(
InquiryTmsQueryService.resolveOutboundQuoteDate("LAND", ""));
}
}
@@ -193,6 +193,31 @@ public class TmsV2RequestMapperTest {
assertFalse(out.containsKey("tradeTerms"));
assertFalse(out.containsKey("贸易条款"));
assertNull(out.get("originCode"));
assertFalse(out.containsKey("addedServices"));
}
@Test
public void airAddedServicesSplitIntoList() {
Map<String, Object> seg = new HashMap<>();
seg.put("destinationCode", "KUL");
seg.put("cargoName", "茶叶");
seg.put("quantity", "10");
seg.put("weightKg", "100");
seg.put("volumeCbm", "2");
seg.put("packageType", "纸箱");
seg.put("added_services", "托盘、前置仓,带电");
Map<String, Object> internal = new HashMap<>();
internal.put("transportMode", "AIR");
internal.put("quoteDate", "2026-10-13");
internal.put("segments", Collections.singletonList(seg));
Map<String, Object> body = TmsV2RequestMapper.quote(internal);
@SuppressWarnings("unchecked")
List<Map<String, Object>> segs = (List<Map<String, Object>>) body.get("segments");
assertEquals(java.util.Arrays.asList("托盘", "前置仓", "带电"), segs.get(0).get("addedServices"));
assertFalse(segs.get(0).containsKey("added_services"));
assertEquals(
java.util.Arrays.asList("MUST GO", "带电"),
TmsV2RequestMapper.splitAddedServices("MUST GO、带电"));
}
@Test