落地陆运私聊文字询价:选项核对后再建单查价,并按线路拉产品进群。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+13
@@ -160,6 +160,19 @@ public class InquiryAgentController {
|
||||
return Result.OK(out);
|
||||
}
|
||||
|
||||
@ApiOperation("陆运拉群:按线路类别匹配启用的产品·陆运")
|
||||
@PostMapping("/staff/matchLand")
|
||||
public Result<Map<String, Object>> matchLand(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
String route = firstStr(body, "routeCategory", "线路类别");
|
||||
Map<String, Object> out = new java.util.LinkedHashMap<>();
|
||||
out.put("staff", staffRoleService.matchLandStaff(route));
|
||||
return Result.OK(out);
|
||||
}
|
||||
|
||||
@ApiOperation("按岗位列出启用员工(空运群 @ 航线·空运)")
|
||||
@PostMapping("/staff/listByRole")
|
||||
public Result<Map<String, Object>> listStaffByRole(@RequestBody Map<String, Object> body,
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ public interface IInquiryStaffRoleService extends IService<InquiryStaffRole> {
|
||||
*/
|
||||
java.util.List<java.util.Map<String, Object>> matchSeaStaff(String origin, String destination);
|
||||
|
||||
/**
|
||||
* 陆运拉群:启用的产品·陆运,按本单线路类别整词匹配负责线路。
|
||||
*/
|
||||
java.util.List<java.util.Map<String, Object>> matchLandStaff(String routeCategory);
|
||||
|
||||
/**
|
||||
* 列出该岗位启用员工。空运群 @ 航线用 roleCode=air。
|
||||
*/
|
||||
|
||||
+40
-3
@@ -534,10 +534,11 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
}
|
||||
out.put("quote_no", StringUtils.defaultString(workOrderNo));
|
||||
out.put("pol", first(src, "起运港", "pol", "origin"));
|
||||
out.put("origin", first(src, "起运地", "起运港", "origin", "pol"));
|
||||
out.put("origin", first(src, "起运地", "起运港", "始发站", "origin", "pol"));
|
||||
out.put("pod", first(src, "目的地", "目的港", "pod", "destination"));
|
||||
out.put("destination", first(src, "目的地", "目的港", "destination", "pod"));
|
||||
out.put("commodity", first(src, "品名", "commodity"));
|
||||
out.put("commodity", first(src, "品名", "货物品名", "commodity"));
|
||||
out.put("vehicle_qty", first(src, "车型/数量", "车型"));
|
||||
// 海运七项:合同键是 quantity / fcl_or_lcl / container_type_qty,工单事实是中文键
|
||||
out.put("quantity", first(src, "货量", "货物数量", "quantity"));
|
||||
out.put("fcl_or_lcl", first(src, "整柜或拼柜", "fcl_or_lcl"));
|
||||
@@ -584,6 +585,16 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
out.put("currency", currency);
|
||||
}
|
||||
out.put("sales_name", StringUtils.defaultString(salesWecomId));
|
||||
String eta = first(q, "eta", "transit_days", "transitDays");
|
||||
if (StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
|
||||
out.put("transit_days", eta);
|
||||
}
|
||||
for (Map.Entry<String, String> e : QuoteTemplateExcelSupport.LAND_TMS_KEY_TO_CODE.entrySet()) {
|
||||
String val = first(q, e.getKey(), e.getValue());
|
||||
if (StringUtils.isNotBlank(val) && !"-".equals(val) && StringUtils.isBlank(out.getString(e.getValue()))) {
|
||||
out.put(e.getValue(), val);
|
||||
}
|
||||
}
|
||||
JSONArray rows = QuoteTemplateExcelSupport.parseFeeRows(q.get("fee_lines"));
|
||||
if (rows.isEmpty()) {
|
||||
rows = QuoteTemplateExcelSupport.parseFeeRows(q.get("fee_rows"));
|
||||
@@ -592,7 +603,7 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
String freight = first(q, "freight");
|
||||
if (StringUtils.isNotBlank(freight) && !"-".equals(freight)) {
|
||||
JSONObject one = new JSONObject(true);
|
||||
one.put("name", "空运费");
|
||||
one.put("name", looksLandInquiry(src) ? "陆运费" : "空运费");
|
||||
one.put("amount", freight);
|
||||
if (StringUtils.isNotBlank(currency)) {
|
||||
one.put("currency", currency);
|
||||
@@ -608,12 +619,38 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject one = rows.getJSONObject(i);
|
||||
if (one == null) {
|
||||
continue;
|
||||
}
|
||||
String code = QuoteTemplateExcelSupport.landFactCodeForFeeName(one.getString("name"));
|
||||
String amount = StringUtils.trimToEmpty(one.getString("amount"));
|
||||
if (StringUtils.isNotBlank(code) && StringUtils.isNotBlank(amount) && !"-".equals(amount)
|
||||
&& StringUtils.isBlank(out.getString(code))) {
|
||||
out.put(code, amount);
|
||||
}
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
out.put("fee_rows", rows);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 询价事实里已有陆运选项/始发站,运费兜底名就不能再写成空运费。
|
||||
*/
|
||||
private static boolean looksLandInquiry(Map<String, Object> src) {
|
||||
if (src == null || src.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String line = first(src, "业务线", "business_line", "businessLine");
|
||||
if ("LAND".equalsIgnoreCase(line) || (line != null && line.contains("陆"))) {
|
||||
return true;
|
||||
}
|
||||
return StringUtils.isNotBlank(first(src, "始发站", "运输类型", "线路类别"));
|
||||
}
|
||||
|
||||
private static String first(Map<?, ?> src, String... keys) {
|
||||
if (src == null) {
|
||||
return "";
|
||||
|
||||
+11
@@ -144,6 +144,17 @@ public class InquiryStaffRoleServiceImpl extends ServiceImpl<InquiryStaffRoleMap
|
||||
return org.jeecg.modules.inquiry.staff.SeaStaffMatcher.match(rows, origin, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运拉群:库里启用的产品·陆运,再按线路类别整词过滤。
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> matchLandStaff(String routeCategory) {
|
||||
List<InquiryStaffRole> rows = list(new QueryWrapper<InquiryStaffRole>()
|
||||
.eq("role_code", "land")
|
||||
.eq("status", "active"));
|
||||
return org.jeecg.modules.inquiry.staff.LandStaffMatcher.match(rows, routeCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> listActiveByRole(String roleCode) {
|
||||
String code = StringUtils.trimToEmpty(roleCode).toLowerCase();
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package org.jeecg.modules.inquiry.staff;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 陆运拉群人员匹配:只看员工管理里岗位=产品·陆运且启用的人。
|
||||
* <p>
|
||||
* 用本单线路类别对负责线路:按逗号/顿号拆整词,禁止把「中港/中亚/中欧」按斜杠切开。
|
||||
* 没有「陆运全线」快捷项;四条都要跟就须后台四条都勾。
|
||||
* 禁止:按销售 userid 伪造名单;不查企微通讯录。
|
||||
*/
|
||||
public final class LandStaffMatcher {
|
||||
|
||||
private LandStaffMatcher() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rows 员工岗位行
|
||||
* @param routeCategory 本单线路类别(与后台四项同字)
|
||||
* @return 给智能体的 {name, wecomId, routes}
|
||||
*/
|
||||
public static List<Map<String, Object>> match(List<InquiryStaffRole> rows, String routeCategory) {
|
||||
List<Map<String, Object>> hits = new ArrayList<>();
|
||||
if (rows == null) {
|
||||
return hits;
|
||||
}
|
||||
String want = StringUtils.trimToEmpty(routeCategory);
|
||||
if (want.isEmpty()) {
|
||||
return hits;
|
||||
}
|
||||
for (InquiryStaffRole row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
if (!"land".equalsIgnoreCase(StringUtils.trimToEmpty(row.getRoleCode()))) {
|
||||
continue;
|
||||
}
|
||||
if (!"active".equalsIgnoreCase(StringUtils.defaultIfBlank(row.getStatus(), "active"))) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isBlank(row.getWecomId())) {
|
||||
continue;
|
||||
}
|
||||
if (!routeTokens(row.getRoutes()).contains(want)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("name", StringUtils.defaultString(row.getName()));
|
||||
item.put("wecomId", row.getWecomId().trim());
|
||||
item.put("roleCode", "land");
|
||||
item.put("routes", StringUtils.defaultString(row.getRoutes()));
|
||||
item.put("status", "active");
|
||||
hits.add(item);
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只按中英文逗号、顿号拆。斜杠是线路名称的一部分。
|
||||
*/
|
||||
static List<String> splitRoutes(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
if (raw == null || raw.trim().isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (String part : raw.split("[,,、]+")) {
|
||||
String t = part.trim();
|
||||
if (!t.isEmpty()) {
|
||||
out.add(t);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<String> routeTokens(String raw) {
|
||||
return splitRoutes(raw);
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package org.jeecg.modules.inquiry.template;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 陆运报价单字段词典(TMS 对接文档 9.5)。
|
||||
* <p>
|
||||
* 陆运 Excel 是「标签 + 右侧金额」,不是海运那种「费用名称 / 金额」表。
|
||||
* 本类只做名称对照,不发明价格、不打 TMS。
|
||||
*/
|
||||
public final class LandQuoteTemplateFields {
|
||||
|
||||
private static final Map<String, String> LABEL_TO_CODE = new LinkedHashMap<>();
|
||||
private static final Map<String, String> KEY_TO_CODE = new LinkedHashMap<>();
|
||||
private static final Map<String, List<String>> CODE_TO_LABELS = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
add("price_per_cbm", "元/CBM", "pricePerCbm");
|
||||
add("price_per_kg", "元/kg", "pricePerKg");
|
||||
add("delivery_fee", "送货费", "deliveryFee");
|
||||
add("pickup_fee", "提货费", "pickupFee");
|
||||
add("min_charge", "最低收费", "minCharge");
|
||||
add("transit_days", "时效(天)", "transitDays");
|
||||
add("effective_time", "生效时间", "effectiveTime");
|
||||
add("land_freight", "陆运费", "landFreight");
|
||||
add("vehicle_42_fee", "4.2米/3T费用", "vehicle42Fee");
|
||||
add("vehicle_50_fee", "5米/5T费用", "vehicle50Fee");
|
||||
add("vehicle_76_fee", "7.6米/8T费用", "vehicle76Fee");
|
||||
add("vehicle_82_fee", "8.2米/10T费用", "vehicle82Fee");
|
||||
add("vehicle_96_fee", "9.6米/10T费用", "vehicle96Fee");
|
||||
add("vehicle_12_fee", "12米/40尺费用", "vehicle12Fee");
|
||||
add("vehicle_125_fee", "12.5米/40尺费用", "vehicle125Fee");
|
||||
add("vehicle_165_fee", "16.5米/53尺费用", "vehicle165Fee");
|
||||
add("vehicle_3t_fee", "4.1*2.1*2.3M/3T费用", "vehicle3TFee");
|
||||
add("vehicle_5t_fee", "6.5*2.4*2.5M/5T费用", "vehicle5TFee");
|
||||
add("vehicle_8t_fee", "7.5*2.4*2.5M/8T费用", "vehicle8TFee");
|
||||
add("vehicle_10t_fee", "9.4*2.4*2.55M/10T费用", "vehicle10TFee");
|
||||
add("vehicle_40hq_fee", "11.8*2.32*2.7M/40'HQ费用", "vehicle40HQFee");
|
||||
add("vehicle_45hq_fee", "12.8*2.32*2.7M/45'HQ费用", "vehicle45HQFee");
|
||||
add("vehicle_48rh_fee", "14.6*2.28*2.5M/48RH冷柜费用", "vehicle48RHFee");
|
||||
add("vehicle_53r_fee", "16.3*2.7*2.8M/53尺费用", "vehicle53RFee");
|
||||
add("vehicle_1375_fee", "13.5*3M/13.75平板费用", "vehicle13.75TFee");
|
||||
add("vehicle_175_fee", "17.5*3M/17.5平板费用", "vehicle17.5TFee");
|
||||
LABEL_TO_CODE.put("时效", "transit_days");
|
||||
LABEL_TO_CODE.put("干线费", "land_freight");
|
||||
LABEL_TO_CODE.put("运费", "land_freight");
|
||||
LABEL_TO_CODE.put("4.2米/3T", "vehicle_42_fee");
|
||||
LABEL_TO_CODE.put("7.6米/8T", "vehicle_76_fee");
|
||||
LABEL_TO_CODE.put("9.6米/10T", "vehicle_96_fee");
|
||||
LABEL_TO_CODE.put("元/KG", "price_per_kg");
|
||||
}
|
||||
|
||||
private LandQuoteTemplateFields() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel 标签 → 填表码,供按标签写右侧格子。
|
||||
*/
|
||||
public static Map<String, String> labelToCode() {
|
||||
return Collections.unmodifiableMap(LABEL_TO_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* TMS 扁平字段 → 填表码。
|
||||
*/
|
||||
public static Map<String, String> tmsKeyToCode() {
|
||||
return Collections.unmodifiableMap(KEY_TO_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用名 / TMS key / 标签任一能对上则回填表码,对不上给空。
|
||||
*/
|
||||
public static String codeForFeeName(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return "";
|
||||
}
|
||||
String name = raw.trim();
|
||||
if (LABEL_TO_CODE.containsKey(name)) {
|
||||
return LABEL_TO_CODE.get(name);
|
||||
}
|
||||
if (KEY_TO_CODE.containsKey(name)) {
|
||||
return KEY_TO_CODE.get(name);
|
||||
}
|
||||
String lower = name.toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> e : LABEL_TO_CODE.entrySet()) {
|
||||
String label = e.getKey();
|
||||
if (label.length() >= 4 && (name.contains(label) || label.contains(name))) {
|
||||
return e.getValue();
|
||||
}
|
||||
if ("元/CBM".equals(label) && (name.contains("CBM") || name.contains("cbm"))) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, String> e : KEY_TO_CODE.entrySet()) {
|
||||
if (e.getKey().equalsIgnoreCase(lower)) {
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个填表码对应的 Excel 标签,找格子时按这个顺序试。
|
||||
*/
|
||||
public static List<String> labelsFor(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> labels = CODE_TO_LABELS.get(code);
|
||||
return labels == null ? Collections.emptyList() : Collections.unmodifiableList(labels);
|
||||
}
|
||||
|
||||
private static void add(String code, String label, String tmsKey) {
|
||||
LABEL_TO_CODE.put(label, code);
|
||||
if (StringUtils.isNotBlank(tmsKey)) {
|
||||
KEY_TO_CODE.put(tmsKey, code);
|
||||
}
|
||||
CODE_TO_LABELS.computeIfAbsent(code, k -> new ArrayList<String>()).add(label);
|
||||
}
|
||||
}
|
||||
+201
-3
@@ -69,11 +69,82 @@ public final class QuoteTemplateExcelSupport {
|
||||
FILL_BY_LABEL.put("重量", "weight_kg");
|
||||
FILL_BY_LABEL.put("体积", "volume_cbm");
|
||||
FILL_BY_LABEL.put("始发地", "origin");
|
||||
FILL_BY_LABEL.put("始发站", "origin");
|
||||
FILL_BY_LABEL.put("目的地", "destination");
|
||||
FILL_BY_LABEL.put("货物品名", "commodity");
|
||||
FILL_BY_LABEL.put("车型/数量", "vehicle_qty");
|
||||
FILL_BY_LABEL.put("运输类型", "运输类型");
|
||||
FILL_BY_LABEL.put("线路类别", "线路类别");
|
||||
FILL_BY_LABEL.put("运输分类", "运输分类");
|
||||
FILL_BY_LABEL.put("中港装载方式", "中港装载方式");
|
||||
FILL_BY_LABEL.put("币别", "currency");
|
||||
FILL_BY_LABEL.put("业务员", "sales_name");
|
||||
FILL_BY_LABEL.put("客户名称", "customer_name");
|
||||
FILL_BY_LABEL.put("联系人", "contact_name");
|
||||
// 陆运报价单是标签栏(TMS 9.5),金额写在标签右侧
|
||||
FILL_BY_LABEL.put("元/CBM", "price_per_cbm");
|
||||
FILL_BY_LABEL.put("元/kg", "price_per_kg");
|
||||
FILL_BY_LABEL.put("元/KG", "price_per_kg");
|
||||
FILL_BY_LABEL.put("送货费", "delivery_fee");
|
||||
FILL_BY_LABEL.put("提货费", "pickup_fee");
|
||||
FILL_BY_LABEL.put("最低收费", "min_charge");
|
||||
FILL_BY_LABEL.put("时效(天)", "transit_days");
|
||||
FILL_BY_LABEL.put("时效", "transit_days");
|
||||
FILL_BY_LABEL.put("生效时间", "effective_time");
|
||||
FILL_BY_LABEL.put("陆运费", "land_freight");
|
||||
FILL_BY_LABEL.put("干线费", "land_freight");
|
||||
FILL_BY_LABEL.put("4.2米/3T费用", "vehicle_42_fee");
|
||||
FILL_BY_LABEL.put("4.2米/3T", "vehicle_42_fee");
|
||||
FILL_BY_LABEL.put("5米/5T费用", "vehicle_50_fee");
|
||||
FILL_BY_LABEL.put("7.6米/8T费用", "vehicle_76_fee");
|
||||
FILL_BY_LABEL.put("7.6米/8T", "vehicle_76_fee");
|
||||
FILL_BY_LABEL.put("8.2米/10T费用", "vehicle_82_fee");
|
||||
FILL_BY_LABEL.put("9.6米/10T费用", "vehicle_96_fee");
|
||||
FILL_BY_LABEL.put("12米/40尺费用", "vehicle_12_fee");
|
||||
FILL_BY_LABEL.put("12.5米/40尺费用", "vehicle_125_fee");
|
||||
FILL_BY_LABEL.put("16.5米/53尺费用", "vehicle_165_fee");
|
||||
FILL_BY_LABEL.put("4.1*2.1*2.3M/3T费用", "vehicle_3t_fee");
|
||||
FILL_BY_LABEL.put("6.5*2.4*2.5M/5T费用", "vehicle_5t_fee");
|
||||
FILL_BY_LABEL.put("7.5*2.4*2.5M/8T费用", "vehicle_8t_fee");
|
||||
FILL_BY_LABEL.put("9.4*2.4*2.55M/10T费用", "vehicle_10t_fee");
|
||||
FILL_BY_LABEL.put("11.8*2.32*2.7M/40'HQ费用", "vehicle_40hq_fee");
|
||||
FILL_BY_LABEL.put("12.8*2.32*2.7M/45'HQ费用", "vehicle_45hq_fee");
|
||||
FILL_BY_LABEL.put("14.6*2.28*2.5M/48RH冷柜费用", "vehicle_48rh_fee");
|
||||
FILL_BY_LABEL.put("16.3*2.7*2.8M/53尺费用", "vehicle_53r_fee");
|
||||
FILL_BY_LABEL.put("13.5*3M/13.75平板费用", "vehicle_1375_fee");
|
||||
FILL_BY_LABEL.put("17.5*3M/17.5平板费用", "vehicle_175_fee");
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运 TMS 扁平字段 → 填表码。费用名对不上时再按这个取。
|
||||
*/
|
||||
public static final Map<String, String> LAND_TMS_KEY_TO_CODE = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
LAND_TMS_KEY_TO_CODE.put("pricePerCbm", "price_per_cbm");
|
||||
LAND_TMS_KEY_TO_CODE.put("pricePerKg", "price_per_kg");
|
||||
LAND_TMS_KEY_TO_CODE.put("deliveryFee", "delivery_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("pickupFee", "pickup_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("minCharge", "min_charge");
|
||||
LAND_TMS_KEY_TO_CODE.put("transitDays", "transit_days");
|
||||
LAND_TMS_KEY_TO_CODE.put("effectiveTime", "effective_time");
|
||||
LAND_TMS_KEY_TO_CODE.put("landFreight", "land_freight");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle42Fee", "vehicle_42_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle50Fee", "vehicle_50_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle76Fee", "vehicle_76_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle82Fee", "vehicle_82_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle96Fee", "vehicle_96_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle12Fee", "vehicle_12_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle125Fee", "vehicle_125_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle165Fee", "vehicle_165_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle3TFee", "vehicle_3t_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle5TFee", "vehicle_5t_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle8TFee", "vehicle_8t_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle10TFee", "vehicle_10t_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle40HQFee", "vehicle_40hq_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle45HQFee", "vehicle_45hq_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle48RHFee", "vehicle_48rh_fee");
|
||||
LAND_TMS_KEY_TO_CODE.put("vehicle53RFee", "vehicle_53r_fee");
|
||||
}
|
||||
|
||||
private QuoteTemplateExcelSupport() {
|
||||
@@ -409,16 +480,35 @@ public final class QuoteTemplateExcelSupport {
|
||||
continue;
|
||||
}
|
||||
Cell nameCell = findCellByFeeName(sheet, name, usedRows);
|
||||
if (nameCell == null) {
|
||||
nameCell = findCellByLandFeeAlias(sheet, name, usedRows);
|
||||
}
|
||||
if (nameCell == null) {
|
||||
unmatched.add(rowObj);
|
||||
continue;
|
||||
}
|
||||
int r = nameCell.getRowIndex();
|
||||
usedRows.add(r);
|
||||
if (shouldWriteLandFeeBesideLabel(sheet, name, nameCell)) {
|
||||
// 陆运模板没有费用表头:金额写到标签右侧,禁止落到默认 G 列
|
||||
writeAmountBesideLabel(sheet, nameCell, amount, yellow);
|
||||
continue;
|
||||
}
|
||||
Map<String, Integer> cols = findFeeTableColumnsAbove(sheet, r);
|
||||
writeFeeDetailRow(sheet, r, cols, rowObj, headerCurrency, boxType, yellow, false);
|
||||
}
|
||||
for (JSONObject rowObj : unmatched) {
|
||||
if (shouldWriteLandFeeBesideLabel(sheet, rowObj.getString("name"), null)
|
||||
&& findFeeTableColumns(sheet).isEmpty()) {
|
||||
Cell alias = findCellByLandFeeAlias(sheet, rowObj.getString("name"), usedRows);
|
||||
if (alias != null) {
|
||||
usedRows.add(alias.getRowIndex());
|
||||
writeAmountBesideLabel(sheet, alias, rowObj.getString("amount"), yellow);
|
||||
continue;
|
||||
}
|
||||
log.warn("报价单没有空行可追加费用 {}", rowObj.getString("name"));
|
||||
continue;
|
||||
}
|
||||
int empty = findEmptyFeeDataRow(sheet, usedRows);
|
||||
if (empty < 0) {
|
||||
log.warn("报价单没有空行可追加费用 {}", rowObj.getString("name"));
|
||||
@@ -431,6 +521,96 @@ public final class QuoteTemplateExcelSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运标签栏且本表没有「费用名称+金额」表头时,金额写右侧。
|
||||
* 空运「空运费」行即使没有完整表头,仍走原来的金额列兜底。
|
||||
*/
|
||||
private static boolean shouldWriteLandFeeBesideLabel(Sheet sheet, String feeName, Cell nameCell) {
|
||||
if (!findFeeTableColumns(sheet).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String code = landFactCodeForFeeName(feeName);
|
||||
if (!isLandQuoteFactCode(code) && nameCell != null) {
|
||||
code = landFactCodeForFeeName(readCellText(nameCell));
|
||||
}
|
||||
return isLandQuoteFactCode(code);
|
||||
}
|
||||
/**
|
||||
* 陆运标签栏:金额写到标签右侧空格,和询价字段同一套定位。
|
||||
*/
|
||||
private static void writeAmountBesideLabel(Sheet sheet, Cell labelCell, String amount, Set<String> yellow) {
|
||||
if (sheet == null || labelCell == null || StringUtils.isBlank(amount) || "-".equals(amount.trim())) {
|
||||
return;
|
||||
}
|
||||
String fillRef = resolveFillTarget(sheet, labelCell, sheet.getSheetName(),
|
||||
readCellText(labelCell), new DataFormatter());
|
||||
int rowIdx;
|
||||
int colIdx;
|
||||
if (StringUtils.isNotBlank(fillRef)) {
|
||||
ParsedRef ref = ParsedRef.parse(fillRef);
|
||||
rowIdx = ref.row;
|
||||
colIdx = ref.col;
|
||||
} else {
|
||||
rowIdx = labelCell.getRowIndex();
|
||||
colIdx = labelCell.getColumnIndex() + 1;
|
||||
}
|
||||
writeAmountCell(sheet, rowIdx, colIdx, amount, yellow);
|
||||
}
|
||||
|
||||
/**
|
||||
* TMS 费用名是 pricePerCbm / 4.2米/3T 时,按陆运标签再找一格。
|
||||
*/
|
||||
private static Cell findCellByLandFeeAlias(Sheet sheet, String name, Set<Integer> usedRows) {
|
||||
String code = landFactCodeForFeeName(name);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, String> e : FILL_BY_LABEL.entrySet()) {
|
||||
if (!code.equals(e.getValue())) {
|
||||
continue;
|
||||
}
|
||||
Cell hit = findCellByFeeName(sheet, e.getKey(), usedRows);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运费用名 / TMS key 对到填表码。对不上给空,不发明价格。
|
||||
*/
|
||||
public static String landFactCodeForFeeName(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return "";
|
||||
}
|
||||
String name = raw.trim();
|
||||
if (FILL_BY_LABEL.containsKey(name)) {
|
||||
String code = FILL_BY_LABEL.get(name);
|
||||
if (isLandQuoteFactCode(code)) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
if (LAND_TMS_KEY_TO_CODE.containsKey(name)) {
|
||||
return LAND_TMS_KEY_TO_CODE.get(name);
|
||||
}
|
||||
String matched = matchLabel(name, FILL_BY_LABEL);
|
||||
if (isLandQuoteFactCode(matched)) {
|
||||
return matched;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static boolean isLandQuoteFactCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return false;
|
||||
}
|
||||
return code.startsWith("price_per_") || code.startsWith("vehicle_")
|
||||
|| "delivery_fee".equals(code) || "pickup_fee".equals(code)
|
||||
|| "min_charge".equals(code) || "transit_days".equals(code)
|
||||
|| "effective_time".equals(code) || "land_freight".equals(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写一行费用。writeName=false 时不碰已有费目名,避免把「文件费/Doc.」盖成数量 1。
|
||||
*/
|
||||
@@ -1088,7 +1268,7 @@ public final class QuoteTemplateExcelSupport {
|
||||
String noColon = primary.replace(":", "").trim();
|
||||
for (Map.Entry<String, String> e : dict.entrySet()) {
|
||||
String dk = e.getKey().replace(":", ":").replace(":", "").trim();
|
||||
if (dk.length() < 2 || noColon.length() < 2) {
|
||||
if (dk.length() < 3 || noColon.length() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (dk.equalsIgnoreCase(noColon) || noColon.startsWith(dk) || dk.startsWith(noColon)) {
|
||||
@@ -1257,8 +1437,26 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (primary.equals(want)) {
|
||||
return true;
|
||||
}
|
||||
return primary.equals(want + "用") || primary.equals(want + "费用")
|
||||
|| want.equals(primary + "用") || want.equals(primary + "费用");
|
||||
if (primary.equals(want + "用") || primary.equals(want + "费用")
|
||||
|| want.equals(primary + "用") || want.equals(primary + "费用")) {
|
||||
return true;
|
||||
}
|
||||
// 陆运车型标签「4.2米/3T」与「4.2米/3T费用」视为同一格;「元」太短不能这样对
|
||||
String wantPrimary = feePrimaryName(want);
|
||||
if (StringUtils.isNotBlank(wantPrimary) && primary.equals(wantPrimary)
|
||||
&& distinctiveLandFeeToken(primary)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean distinctiveLandFeeToken(String text) {
|
||||
if (StringUtils.isBlank(text) || text.length() < 3) {
|
||||
return false;
|
||||
}
|
||||
return text.contains("米") || text.contains("CBM") || text.contains("cbm")
|
||||
|| text.toUpperCase(Locale.ROOT).contains("KG") || text.contains("HQ")
|
||||
|| text.contains("T费用") || text.contains("平板");
|
||||
}
|
||||
|
||||
static Integer pickContainerRateColumn(Map<String, Integer> cols, String boxType) {
|
||||
|
||||
+54
-6
@@ -78,7 +78,10 @@ public final class QuoteTemplateMatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* 先扫同业务线关键词(列表已按默认优先排序),再退回该业务默认模板。
|
||||
* 同业务线:先按关键词打分(运输类型上的词优先),再退回该业务默认模板。
|
||||
* <p>
|
||||
* 「陆运/海运/空运/国内」不当关键词:业务线已过滤,haystack 里必有「陆运」,
|
||||
* 否则中港模板会抢走国内拼车单。多模板同时命中时,类型命中更多者胜。
|
||||
*
|
||||
* @return 命中的模板 Map,并写入 matchType=keyword|default;都没有则 null
|
||||
*/
|
||||
@@ -90,7 +93,13 @@ public final class QuoteTemplateMatcher {
|
||||
) {
|
||||
String want = normalizeBiz(businessLine);
|
||||
String haystack = buildHaystack(businessLine, facts, quote).toLowerCase(Locale.ROOT);
|
||||
String typeVal = "";
|
||||
if (facts != null && facts.get("运输类型") != null) {
|
||||
typeVal = String.valueOf(facts.get("运输类型")).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
Map<String, Object> keywordHit = null;
|
||||
int bestTypeHits = -1;
|
||||
int bestLength = -1;
|
||||
Map<String, Object> defaultHit = null;
|
||||
if (templates == null) {
|
||||
return null;
|
||||
@@ -106,7 +115,13 @@ public final class QuoteTemplateMatcher {
|
||||
if (defaultHit == null && isDefault(t.get("isDefault"))) {
|
||||
defaultHit = t;
|
||||
}
|
||||
if (keywordHit == null && keywordsHit(t.get("keywords"), haystack)) {
|
||||
int[] score = keywordScore(t.get("keywords"), haystack, typeVal);
|
||||
if (score[0] == 0 && score[1] == 0) {
|
||||
continue;
|
||||
}
|
||||
if (score[0] > bestTypeHits || (score[0] == bestTypeHits && score[1] > bestLength)) {
|
||||
bestTypeHits = score[0];
|
||||
bestLength = score[1];
|
||||
keywordHit = t;
|
||||
}
|
||||
}
|
||||
@@ -119,13 +134,46 @@ public final class QuoteTemplateMatcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean keywordsHit(Object raw, String haystack) {
|
||||
/**
|
||||
* 业务线名、过宽的「国内」不当关键词。
|
||||
*/
|
||||
static boolean skipKeyword(String kw) {
|
||||
if (kw == null || kw.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String k = kw.toLowerCase(Locale.ROOT);
|
||||
return "海运".equals(k) || "空运".equals(k) || "陆运".equals(k)
|
||||
|| "sea".equals(k) || "air".equals(k) || "land".equals(k)
|
||||
|| "国内".equals(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [运输类型命中数, 命中词总长];都是 0 表示未命中
|
||||
*/
|
||||
static int[] keywordScore(Object raw, String haystack, String typeVal) {
|
||||
int typeHits = 0;
|
||||
int length = 0;
|
||||
boolean any = false;
|
||||
String type = typeVal == null ? "" : typeVal;
|
||||
String hay = haystack == null ? "" : haystack;
|
||||
for (String kw : asKeywords(raw)) {
|
||||
if (haystack.contains(kw.toLowerCase(Locale.ROOT))) {
|
||||
return true;
|
||||
if (skipKeyword(kw)) {
|
||||
continue;
|
||||
}
|
||||
String k = kw.toLowerCase(Locale.ROOT);
|
||||
if (!type.isEmpty() && type.contains(k)) {
|
||||
typeHits++;
|
||||
length += k.length();
|
||||
any = true;
|
||||
} else if (hay.contains(k)) {
|
||||
length += k.length();
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (!any) {
|
||||
return new int[] {0, 0};
|
||||
}
|
||||
return new int[] {typeHits, length};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-5
@@ -27,7 +27,8 @@ import java.util.TimeZone;
|
||||
/**
|
||||
* 智能体查价门面:询价必填齐了就必须打真 TMS,再按 TMS 回包分类。
|
||||
* <p>
|
||||
* 职责:询价必填齐了就出站。空运看件数/重量/体积/包装;海运看货量/整柜或拼柜/箱型箱量,不拦空运量尺。
|
||||
* 职责:询价必填齐了就出站。空运看件数/重量/体积/包装;海运看货量/整柜或拼柜/箱型箱量;
|
||||
* 陆运看类型+线路+体积重量或车型数量,不拦包装,不把运输分类/中港装载送 TMS。
|
||||
* 机场三字码能投影就带上;对不上也必须出站,禁止本地判「暂无报价」。
|
||||
* 业务线以工单为准,禁止海运请求被默认成 AIR。
|
||||
* 分类:1 且有价 → 报价;TMS 已回 1002 / 无 quoteFound / 1003 → 暂无报价;
|
||||
@@ -91,6 +92,9 @@ public class InquiryTmsQueryService {
|
||||
if (items != null) {
|
||||
segment.put("containerItems", items);
|
||||
}
|
||||
} else if ("LAND".equals(line)) {
|
||||
// 陆运不套空运包装/件数;分类和中港装载只在工单,mapper 不会放进出站。
|
||||
segment.putAll(TmsLandSegmentMapper.build(facts));
|
||||
} else {
|
||||
segment.put("quantity", TmsV2RequestMapper.asInteger(facts.get("件数")));
|
||||
segment.put("weightKg", TmsV2RequestMapper.asPositiveDecimal(facts.get("毛重")));
|
||||
@@ -107,9 +111,14 @@ public class InquiryTmsQueryService {
|
||||
quoteDate = fmt.format(new Date());
|
||||
}
|
||||
|
||||
List<String> missing = "SEA".equals(line)
|
||||
? missingSeaQuery(workOrderNo, quoteDate, segment, facts)
|
||||
: missingAirQuery(workOrderNo, quoteDate, segment);
|
||||
List<String> missing;
|
||||
if ("SEA".equals(line)) {
|
||||
missing = missingSeaQuery(workOrderNo, quoteDate, segment, facts);
|
||||
} else if ("LAND".equals(line)) {
|
||||
missing = TmsLandSegmentMapper.missingQuery(workOrderNo, quoteDate, segment, facts);
|
||||
} else {
|
||||
missing = missingAirQuery(workOrderNo, quoteDate, segment);
|
||||
}
|
||||
if (!missing.isEmpty()) {
|
||||
log.warn("tms.query skip incomplete wo={} missing={}", workOrderNo, missing);
|
||||
return incomplete(missing);
|
||||
@@ -134,6 +143,10 @@ public class InquiryTmsQueryService {
|
||||
|
||||
String cacheKey = line + ":" + quoteDate + ":" + origin + ":" + destination
|
||||
+ ":" + segment.get("weightKg") + ":" + segment.get("volumeCbm");
|
||||
if ("LAND".equals(line)) {
|
||||
cacheKey += ":" + segment.get("landTransportType") + ":" + segment.get("routeCategory")
|
||||
+ ":" + segment.get("vehicleType") + ":" + segment.get("quantity");
|
||||
}
|
||||
try {
|
||||
Map<String, Object> tms = gateway.query(internal, cacheKey);
|
||||
return classifyAndPersist(workOrderNo, tms);
|
||||
@@ -554,7 +567,7 @@ public class InquiryTmsQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单已落库的海运事实补进查价 facts,避免智能体漏传货量/箱型。
|
||||
* 工单已落库的事实补进查价 facts。海运补港口货量箱型;陆运 factsJson 里的类型/线路/分类一并带回。
|
||||
*/
|
||||
static void mergeTicketFacts(Map<String, Object> facts, InquiryTicket ticket) {
|
||||
if (facts == null || ticket == null) {
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package org.jeecg.modules.inquiry.tms;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 陆运 TMS 6.1.1 枚举:销售中文 → 出站码。
|
||||
* <p>
|
||||
* 职责:只在打 TMS 前翻译运输类型、线路类别。工单/卡片/拉人仍用中文。
|
||||
* 禁止:把运输分类、中港装载方式译进出站;发明表外码。
|
||||
*/
|
||||
public final class TmsLandEnumMapper {
|
||||
|
||||
private static final Map<String, String> TYPE = new LinkedHashMap<String, String>();
|
||||
private static final Map<String, String> ROUTE = new LinkedHashMap<String, String>();
|
||||
|
||||
static {
|
||||
TYPE.put("国内运输拼车", "DOMESTIC_LTL");
|
||||
TYPE.put("国内运输整车", "DOMESTIC_FTL");
|
||||
TYPE.put("跨境集拼", "CROSS_BORDER_LTL");
|
||||
TYPE.put("跨境整车", "CROSS_BORDER_FTL");
|
||||
TYPE.put("中港", "CHINA_HK");
|
||||
TYPE.put("DOMESTIC_LTL", "DOMESTIC_LTL");
|
||||
TYPE.put("DOMESTIC_FTL", "DOMESTIC_FTL");
|
||||
TYPE.put("CROSS_BORDER_LTL", "CROSS_BORDER_LTL");
|
||||
TYPE.put("CROSS_BORDER_FTL", "CROSS_BORDER_FTL");
|
||||
TYPE.put("CHINA_HK", "CHINA_HK");
|
||||
ROUTE.put("国内长途/零担", "DOMESTIC_LONG_LTL");
|
||||
ROUTE.put("国内城配/拖车/打包", "DOMESTIC_CITY_DIST");
|
||||
ROUTE.put("中港/中亚/中欧", "CN_HK_CA_EU");
|
||||
ROUTE.put("东南亚", "SOUTHEAST_ASIA");
|
||||
ROUTE.put("DOMESTIC_LONG_LTL", "DOMESTIC_LONG_LTL");
|
||||
ROUTE.put("DOMESTIC_CITY_DIST", "DOMESTIC_CITY_DIST");
|
||||
ROUTE.put("CN_HK_CA_EU", "CN_HK_CA_EU");
|
||||
ROUTE.put("SOUTHEAST_ASIA", "SOUTHEAST_ASIA");
|
||||
}
|
||||
|
||||
private TmsLandEnumMapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param raw 销售中文或已是 TMS 码
|
||||
* @return 6.1.1 landTransportType;对不上返回空串,禁止用中文顶上
|
||||
*/
|
||||
public static String transportType(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String key = raw.trim();
|
||||
String code = TYPE.get(key);
|
||||
return code == null ? "" : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param raw 销售中文或已是 TMS 码
|
||||
* @return 6.1.1 routeCategory;对不上返回空串
|
||||
*/
|
||||
public static String routeCategory(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String key = raw.trim();
|
||||
String code = ROUTE.get(key);
|
||||
return code == null ? "" : code;
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package org.jeecg.modules.inquiry.tms;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 陆运 TMS 查价段:按运输类型拼出站字段。
|
||||
* <p>
|
||||
* 职责:把工单 facts 里的中文键写成 TMS 段;运输分类、中港装载方式只留在工单,不进出站。
|
||||
* 拼车/跨境集拼带体积重量;国内整车/跨境整车带「车型/数量」一项再拆成出站车型与台数;
|
||||
* 中港整车不带车型/数量,中港零担/集拼才带。通关口岸有值才放。
|
||||
* 类型/线路出站必须是 TMS 6.1.1 英文枚举,线路键名 routeCategory。
|
||||
* 禁止:按空运要包装件数;把分类当查价条件;直连 TMS。
|
||||
*/
|
||||
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("国内运输整车", "跨境整车"));
|
||||
|
||||
private static final Pattern VEHICLE_SPLIT = Pattern.compile("\\s*[/xX×**]\\s*");
|
||||
private static final Pattern TAIL_QTY = Pattern.compile("^(.*?)[\\s,,]+(\\d+)\\s*(台|辆)?$");
|
||||
|
||||
private TmsLandSegmentMapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param facts 工单/智能体中文 facts(含运输类型、线路类别、货物、中港装载方式)
|
||||
* @return 可并入查价 segment 的键;不含 landClass / hkLoadType
|
||||
*/
|
||||
public static Map<String, Object> build(Map<String, Object> facts) {
|
||||
Map<String, Object> source = facts == null ? new LinkedHashMap<String, Object>() : facts;
|
||||
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("起运港")));
|
||||
put(seg, "destination", text(source.get("目的港")));
|
||||
put(seg, "cargoName", text(source.get("品名")));
|
||||
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 (FTL.contains(type) || "中港".equals(type)) {
|
||||
putVehicleQty(source, seg);
|
||||
}
|
||||
String customs = text(source.get("通关口岸"));
|
||||
if (StringUtils.isNotBlank(customs)) {
|
||||
seg.put("customsPort", customs);
|
||||
}
|
||||
return seg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运出站拦网:工单号、报价日期、起运目的品名、类型、线路,再加当前类型货物。
|
||||
* 不要包装、不要运输分类、不要中港装载。中港整车不要车型/数量。
|
||||
*
|
||||
* @param facts 用于读中港装载方式;禁止把装载方式写进 segment
|
||||
*/
|
||||
public static List<String> missingQuery(String workOrderNo, String quoteDate,
|
||||
Map<String, Object> segment,
|
||||
Map<String, Object> facts) {
|
||||
List<String> missing = new ArrayList<String>();
|
||||
if (workOrderNo == null || !workOrderNo.matches("WO\\d{12}")) {
|
||||
missing.add("workOrderNo");
|
||||
}
|
||||
if (quoteDate == null || quoteDate.isEmpty()) {
|
||||
missing.add("quoteDate");
|
||||
}
|
||||
Map<String, Object> seg = segment == null ? new LinkedHashMap<String, Object>() : segment;
|
||||
String[] required = {
|
||||
"origin", "destination", "cargoName", "landTransportType", "routeCategory"
|
||||
};
|
||||
for (String key : required) {
|
||||
if (blank(seg.get(key))) {
|
||||
missing.add(key);
|
||||
}
|
||||
}
|
||||
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 ftl = FTL.contains(cnType)
|
||||
|| "DOMESTIC_FTL".equals(typeEnum)
|
||||
|| "CROSS_BORDER_FTL".equals(typeEnum);
|
||||
if (lcl) {
|
||||
if (seg.get("volumeCbm") == null) {
|
||||
missing.add("volumeCbm");
|
||||
}
|
||||
if (seg.get("weightKg") == null) {
|
||||
missing.add("weightKg");
|
||||
}
|
||||
} else if (hk && !"零担/集拼".equals(hkLoad)) {
|
||||
// 中港整车不拦车型/数量
|
||||
} else if (ftl || hk) {
|
||||
if (blank(seg.get("vehicleType"))) {
|
||||
missing.add("vehicleType");
|
||||
}
|
||||
if (seg.get("quantity") == null) {
|
||||
missing.add("quantity");
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧调用:没有 facts 时按段上的类型拦;中港整车必须走四参才能放过车型。
|
||||
*/
|
||||
public static List<String> missingQuery(String workOrderNo, String quoteDate,
|
||||
Map<String, Object> segment) {
|
||||
return missingQuery(workOrderNo, quoteDate, segment, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销售侧「车型/数量」是一个字段,出站再拆成 vehicleType + quantity。
|
||||
*/
|
||||
private static void putVehicleQty(Map<String, Object> source, Map<String, Object> seg) {
|
||||
String combo = firstNonBlank(
|
||||
text(source.get("车型/数量")),
|
||||
text(source.get("车型数量")));
|
||||
String vehicle = text(source.get("车型"));
|
||||
String qtyRaw = text(source.get("数量"));
|
||||
if (combo.isEmpty()) {
|
||||
if (!vehicle.isEmpty() && !qtyRaw.isEmpty()) {
|
||||
combo = vehicle + "/" + qtyRaw;
|
||||
} else if (!vehicle.isEmpty()) {
|
||||
combo = vehicle;
|
||||
}
|
||||
}
|
||||
if (combo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String[] parts = splitVehicleQty(combo);
|
||||
String qty = parts[1];
|
||||
if (StringUtils.isNotBlank(qtyRaw) && !combo.contains(qtyRaw)) {
|
||||
qty = qtyRaw;
|
||||
}
|
||||
put(seg, "vehicleType", parts[0]);
|
||||
Integer number = TmsV2RequestMapper.asInteger(qty);
|
||||
put(seg, "quantity", number == null ? Integer.valueOf(1) : number);
|
||||
}
|
||||
|
||||
/**
|
||||
* 例:9米6/1、9米6*2、9米6 1台。拆不出台数时默认 1。
|
||||
*/
|
||||
static String[] splitVehicleQty(String raw) {
|
||||
String text = text(raw);
|
||||
if (text.isEmpty()) {
|
||||
return new String[] {"", "1"};
|
||||
}
|
||||
String[] parts = VEHICLE_SPLIT.split(text, 2);
|
||||
if (parts.length == 2 && StringUtils.isNotBlank(parts[0]) && StringUtils.isNotBlank(parts[1])) {
|
||||
Integer n = TmsV2RequestMapper.asInteger(parts[1]);
|
||||
return new String[] {parts[0].trim(), n == null ? parts[1].trim() : String.valueOf(n)};
|
||||
}
|
||||
Matcher tail = TAIL_QTY.matcher(text);
|
||||
if (tail.matches() && StringUtils.isNotBlank(tail.group(1))) {
|
||||
return new String[] {tail.group(1).trim(), tail.group(2)};
|
||||
}
|
||||
return new String[] {text, "1"};
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return "";
|
||||
}
|
||||
for (String value : values) {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static void put(Map<String, Object> target, String key, Object value) {
|
||||
if (value != null && !"".equals(value)) {
|
||||
target.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean blank(Object raw) {
|
||||
return raw == null || String.valueOf(raw).trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String text(Object raw) {
|
||||
return raw == null ? "" : String.valueOf(raw).trim();
|
||||
}
|
||||
}
|
||||
+18
@@ -80,6 +80,7 @@ public final class TmsV2RequestMapper {
|
||||
// 海运出站:口语必须先整理。FCL 要 containerType+containerQty,loadType 只能 FCL/LCL,
|
||||
// cargoQty 只能是数字。销售侧「整柜 / 1x40HQ / 15件」原样留在工单,不在这里改。
|
||||
applySeaFields(source, target);
|
||||
applyLandFields(source, target);
|
||||
return compact(target);
|
||||
}
|
||||
|
||||
@@ -114,6 +115,23 @@ public final class TmsV2RequestMapper {
|
||||
put(target, "containerQty", asInteger(qty));
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运出站:类型+线路用 6.1.1 枚举;线路字段名是 routeCategory。
|
||||
* 分类和中港装载即使误放在 source 也不抄走。中文不得原样出站。
|
||||
*/
|
||||
private static void applyLandFields(Map<String, Object> source, Map<String, Object> target) {
|
||||
String type = textOf(first(source, "landTransportType"));
|
||||
String route = textOf(first(source, "routeCategory", "landRouteCategory"));
|
||||
put(target, "landTransportType", TmsLandEnumMapper.transportType(type));
|
||||
put(target, "routeCategory", TmsLandEnumMapper.routeCategory(route));
|
||||
put(target, "vehicleType", first(source, "vehicleType"));
|
||||
put(target, "customsPort", first(source, "customsPort"));
|
||||
}
|
||||
|
||||
private static String textOf(Object raw) {
|
||||
return raw == null ? "" : String.valueOf(raw).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 报价日期规范成 yyyy-MM-dd。解析不了则不传,避免 1001。
|
||||
*/
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.jeecg.modules.inquiry.staff;
|
||||
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 陆运拉群人员匹配:不连库。
|
||||
* <p>
|
||||
* 只认产品·陆运启用岗;负责线路按逗号整词对线路类别,禁止按斜杠切开「中港/中亚/中欧」。
|
||||
*/
|
||||
public class LandStaffMatcherTest {
|
||||
|
||||
@Test
|
||||
public void southeastOnly() {
|
||||
InquiryStaffRole a = staff("land", "active", "a", "甲", "东南亚");
|
||||
InquiryStaffRole b = staff("land", "active", "b", "乙", "国内长途/零担");
|
||||
InquiryStaffRole c = staff("land", "active", "c", "丙", "中港/中亚/中欧,东南亚");
|
||||
InquiryStaffRole d = staff("land", "active", "d", "丁", "中港/中亚/中欧");
|
||||
List<Map<String, Object>> hits = LandStaffMatcher.match(
|
||||
Arrays.asList(a, b, c, d), "东南亚");
|
||||
assertEquals(2, hits.size());
|
||||
assertEquals("甲", hits.get(0).get("name"));
|
||||
assertEquals("丙", hits.get(1).get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slashNotSplit() {
|
||||
InquiryStaffRole row = staff("land", "active", "x", "张", "中港/中亚/中欧");
|
||||
assertEquals(1, LandStaffMatcher.match(Arrays.asList(row), "中港/中亚/中欧").size());
|
||||
assertEquals(0, LandStaffMatcher.match(Arrays.asList(row), "东南亚").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoreSeaRole() {
|
||||
InquiryStaffRole sea = staff("sea", "active", "s", "海", "东南亚");
|
||||
assertTrue(LandStaffMatcher.match(Arrays.asList(sea), "东南亚").isEmpty());
|
||||
}
|
||||
|
||||
private static InquiryStaffRole staff(String role, String status, String wecomId,
|
||||
String name, String routes) {
|
||||
InquiryStaffRole row = new InquiryStaffRole();
|
||||
row.setRoleCode(role);
|
||||
row.setStatus(status);
|
||||
row.setWecomId(wecomId);
|
||||
row.setName(name);
|
||||
row.setRoutes(routes);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
+104
@@ -14,6 +14,7 @@ import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -487,6 +488,109 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运国内拼车模板是标签栏,不是「费用名称+金额」表。
|
||||
* TMS 的 元/CBM、送货费、时效必须写到标签右侧,不能落到 G 列或整张空着。
|
||||
*/
|
||||
@Test
|
||||
public void landLtlTemplateWritesTmsRatesBesideLabels() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-ltl-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-ltl-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("国内拼车");
|
||||
row(sheet, 1).createCell(0).setCellValue("始发站");
|
||||
row(sheet, 2).createCell(0).setCellValue("目的地");
|
||||
row(sheet, 3).createCell(0).setCellValue("元/CBM");
|
||||
row(sheet, 4).createCell(0).setCellValue("元/kg");
|
||||
row(sheet, 5).createCell(0).setCellValue("送货费");
|
||||
row(sheet, 6).createCell(0).setCellValue("时效(天)");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("origin", "泉州");
|
||||
facts.put("destination", "南京");
|
||||
facts.put("price_per_cbm", "280");
|
||||
facts.put("price_per_kg", "1.2");
|
||||
facts.put("delivery_fee", "50");
|
||||
facts.put("transit_days", "3");
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"元/CBM\",\"amount\":\"280\"},"
|
||||
+ "{\"name\":\"元/kg\",\"amount\":\"1.2\"},"
|
||||
+ "{\"name\":\"送货费\",\"amount\":\"50\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, new JSONObject(), facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals("泉州", read(sheet, 1, 1));
|
||||
assertEquals("南京", read(sheet, 2, 1));
|
||||
assertEquals(280.0, sheet.getRow(3).getCell(1).getNumericCellValue(), 0.01);
|
||||
assertEquals(1.2, sheet.getRow(4).getCell(1).getNumericCellValue(), 0.01);
|
||||
assertEquals(50.0, sheet.getRow(5).getCell(1).getNumericCellValue(), 0.01);
|
||||
assertEquals("3", read(sheet, 6, 1).replace(".0", ""));
|
||||
assertTrue(sheet.getRow(3).getCell(6) == null
|
||||
|| StringUtils.isBlank(read(sheet, 3, 6)));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 国内整车:4.2米/3T 这类车型费用也是标签右侧,不是海运费目表。
|
||||
*/
|
||||
@Test
|
||||
public void landFtlTemplateWritesVehicleFeeBesideLabel() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-ftl-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-ftl-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("国内整车");
|
||||
row(sheet, 2).createCell(0).setCellValue("4.2米/3T费用");
|
||||
row(sheet, 3).createCell(0).setCellValue("7.6米/8T费用");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"4.2米/3T费用\",\"amount\":\"1800\"},"
|
||||
+ "{\"name\":\"7.6米/8T费用\",\"amount\":\"2600\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, new JSONObject(), facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals(1800.0, sheet.getRow(2).getCell(1).getNumericCellValue(), 0.01);
|
||||
assertEquals(2600.0, sheet.getRow(3).getCell(1).getNumericCellValue(), 0.01);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TMS 陆运 fee_lines 要投影成模板标签码,写出单时才能对上 元/CBM。
|
||||
*/
|
||||
@Test
|
||||
public void buildFillFactsProjectsLandQuoteLabels() {
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
facts.put("始发站", "泉州");
|
||||
facts.put("运输类型", "国内运输拼车");
|
||||
Map<String, Object> quote = new HashMap<>();
|
||||
quote.put("total", "1280");
|
||||
quote.put("currency", "CNY");
|
||||
quote.put("eta", "3");
|
||||
quote.put("fee_lines",
|
||||
"[{\"name\":\"元/CBM\",\"amount\":\"280\"},"
|
||||
+ "{\"name\":\"送货费\",\"amount\":\"50\"}]");
|
||||
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||
.buildFillFacts("WO202609170012", facts, quote, "WuJiLin");
|
||||
assertEquals("泉州", out.getString("origin"));
|
||||
assertEquals("3", out.getString("transit_days"));
|
||||
assertEquals("280", out.getString("price_per_cbm"));
|
||||
assertEquals("50", out.getString("delivery_fee"));
|
||||
assertEquals("1280", out.getString("total_amount"));
|
||||
assertEquals(2, out.getJSONArray("fee_rows").size());
|
||||
}
|
||||
|
||||
private static org.apache.poi.ss.usermodel.Row row(Sheet sheet, int r) {
|
||||
org.apache.poi.ss.usermodel.Row exist = sheet.getRow(r);
|
||||
return exist == null ? sheet.createRow(r) : exist;
|
||||
|
||||
+62
-2
@@ -82,6 +82,19 @@ public class QuoteTemplateMatcherTest {
|
||||
assertEquals("海运默认", hit.get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landClassKeywordHits() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
list.add(tpl("陆运急件", "陆运", false, "急件"));
|
||||
list.add(tpl("陆运默认", "陆运", true, "普件"));
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
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 seaTemplateIgnoredForAir() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
@@ -91,13 +104,60 @@ public class QuoteTemplateMatcherTest {
|
||||
assertNull(QuoteTemplateMatcher.pick(list, "AIR", facts, null));
|
||||
}
|
||||
|
||||
private static Map<String, Object> tpl(String name, String biz, boolean isDefault, String keyword) {
|
||||
@Test
|
||||
public void landPincheNotStolenByGenericLandKeyword() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
list.add(tpl("中港", "陆运", false, "中港", "陆运"));
|
||||
list.add(tpl("国内拼车", "陆运", false, "拼车", "零担"));
|
||||
list.add(tpl("陆运默认", "陆运", true, "普件"));
|
||||
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 landCrossFtlBeatsDomesticFtl() {
|
||||
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("线路类别", "东南亚");
|
||||
Map<String, Object> hit = QuoteTemplateMatcher.pick(list, "LAND", facts, null);
|
||||
assertNotNull(hit);
|
||||
assertEquals("跨境整车", hit.get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landFallsBackToDefault() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
list.add(tpl("中港", "陆运", false, "中港"));
|
||||
list.add(tpl("陆运默认", "陆运", true, "急件"));
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
facts.put("运输类型", "国内运输拼车");
|
||||
facts.put("运输分类", "普件,常用线路/常规车型");
|
||||
Map<String, Object> hit = QuoteTemplateMatcher.pick(list, "LAND", facts, null);
|
||||
assertNotNull(hit);
|
||||
assertEquals("default", hit.get("matchType"));
|
||||
assertEquals("陆运默认", hit.get("name"));
|
||||
}
|
||||
|
||||
private static Map<String, Object> tpl(String name, String biz, boolean isDefault, String... keywords) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("name", name);
|
||||
m.put("bizType", biz);
|
||||
m.put("isDefault", isDefault);
|
||||
List<String> kws = new ArrayList<>();
|
||||
kws.add(keyword);
|
||||
if (keywords != null) {
|
||||
for (String keyword : keywords) {
|
||||
kws.add(keyword);
|
||||
}
|
||||
}
|
||||
m.put("keywords", kws);
|
||||
return m;
|
||||
}
|
||||
|
||||
+19
@@ -47,4 +47,23 @@ public class InquiryTmsQueryServiceTest {
|
||||
assertEquals("整柜", facts.get("整柜或拼柜"));
|
||||
assertEquals("1x40HQ", facts.get("箱型箱量"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeTicketFactsJsonKeepsLandOptions() {
|
||||
InquiryTicket ticket = new InquiryTicket();
|
||||
ticket.setFactsJson("{\"运输类型\":\"跨境集拼\",\"线路类别\":\"东南亚\",\"运输分类\":\"急件\"}");
|
||||
java.util.Map<String, Object> facts = new java.util.HashMap<String, Object>();
|
||||
InquiryTmsQueryService.mergeTicketFacts(facts, ticket);
|
||||
assertEquals("跨境集拼", facts.get("运输类型"));
|
||||
assertEquals("东南亚", facts.get("线路类别"));
|
||||
assertEquals("急件", facts.get("运输分类"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ticketLandWinsWhenBodyOmitsLine() {
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<String, Object>();
|
||||
body.put("work_order_no", "WO202609170001");
|
||||
String line = InquiryTmsQueryService.resolveTransportLine(body, new java.util.HashMap<String, Object>(), "LAND");
|
||||
assertEquals("LAND", line);
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package org.jeecg.modules.inquiry.tms;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 陆运 TMS 枚举:销售中文不得原样出站。不打真实 TMS。
|
||||
*/
|
||||
public class TmsLandEnumMapperTest {
|
||||
|
||||
@Test
|
||||
public void pincheCityDistMatchesTms61() {
|
||||
assertEquals("DOMESTIC_LTL", TmsLandEnumMapper.transportType("国内运输拼车"));
|
||||
assertEquals("DOMESTIC_CITY_DIST", TmsLandEnumMapper.routeCategory("国内城配/拖车/打包"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alreadyEnumStays() {
|
||||
assertEquals("DOMESTIC_LTL", TmsLandEnumMapper.transportType("DOMESTIC_LTL"));
|
||||
assertEquals("SOUTHEAST_ASIA", TmsLandEnumMapper.routeCategory("SOUTHEAST_ASIA"));
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package org.jeecg.modules.inquiry.tms;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* 陆运段拼参:不打真实 TMS。分类和中港装载不得出现在出站 map。
|
||||
*/
|
||||
public class TmsLandSegmentMapperTest {
|
||||
|
||||
@Test
|
||||
public void landSegmentOmitsClass() {
|
||||
Map<String, Object> facts = new HashMap<String, Object>();
|
||||
facts.put("运输类型", "跨境集拼");
|
||||
facts.put("线路类别", "东南亚");
|
||||
facts.put("运输分类", "急件");
|
||||
facts.put("起运港", "广州");
|
||||
facts.put("目的港", "河内");
|
||||
facts.put("品名", "衣服");
|
||||
facts.put("体积", "100");
|
||||
facts.put("毛重", "50");
|
||||
facts.put("报价日期", "2026-09-17");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
assertEquals("CROSS_BORDER_LTL", seg.get("landTransportType"));
|
||||
assertEquals("SOUTHEAST_ASIA", seg.get("routeCategory"));
|
||||
assertFalse(seg.containsKey("landRouteCategory"));
|
||||
assertNull(seg.get("landClass"));
|
||||
assertFalse(seg.containsKey("hkLoadType"));
|
||||
assertFalse(seg.containsKey("运输分类"));
|
||||
}
|
||||
|
||||
@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("车型/数量", "9米6/1");
|
||||
facts.put("体积", "99");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
assertEquals("9米6", seg.get("vehicleType"));
|
||||
assertNull(seg.get("volumeCbm"));
|
||||
assertFalse(seg.containsKey("hkLoadType"));
|
||||
assertEquals(Integer.valueOf(1), seg.get("quantity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hkFullTruckOmitsVehicle() {
|
||||
Map<String, Object> facts = new HashMap<String, Object>();
|
||||
facts.put("运输类型", "中港");
|
||||
facts.put("线路类别", "中港/中亚/中欧");
|
||||
facts.put("中港装载方式", "整车");
|
||||
facts.put("起运港", "广州");
|
||||
facts.put("目的港", "香港");
|
||||
facts.put("品名", "衣服");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
assertNull(seg.get("vehicleType"));
|
||||
assertNull(seg.get("quantity"));
|
||||
java.util.List<String> missing = TmsLandSegmentMapper.missingQuery(
|
||||
"WO202609170005", "2026-09-17", seg, facts);
|
||||
assertEquals(java.util.Collections.emptyList(), missing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void domesticFtlSplitsCombinedField() {
|
||||
Map<String, Object> facts = new HashMap<String, Object>();
|
||||
facts.put("运输类型", "国内运输整车");
|
||||
facts.put("线路类别", "国内长途/零担");
|
||||
facts.put("起运港", "广州");
|
||||
facts.put("目的港", "深圳");
|
||||
facts.put("品名", "衣服");
|
||||
facts.put("车型/数量", "9米6*2");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
assertEquals("9米6", seg.get("vehicleType"));
|
||||
assertEquals(Integer.valueOf(2), seg.get("quantity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wo011PincheCityDistUsesTmsEnums() {
|
||||
Map<String, Object> facts = new HashMap<String, Object>();
|
||||
facts.put("运输类型", "国内运输拼车");
|
||||
facts.put("线路类别", "国内城配/拖车/打包");
|
||||
facts.put("起运港", "泉州市");
|
||||
facts.put("目的港", "南京市");
|
||||
facts.put("品名", "通信机柜及线缆配件");
|
||||
facts.put("体积", "10CBM");
|
||||
facts.put("毛重", "10KG");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
assertEquals("DOMESTIC_LTL", seg.get("landTransportType"));
|
||||
assertEquals("DOMESTIC_CITY_DIST", seg.get("routeCategory"));
|
||||
assertFalse(seg.containsKey("landRouteCategory"));
|
||||
java.util.List<String> missing = TmsLandSegmentMapper.missingQuery(
|
||||
"WO202609170011", "2026-09-17", seg, facts);
|
||||
assertEquals(java.util.Collections.emptyList(), missing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lclPincheDoesNotRequireQuantity() {
|
||||
Map<String, Object> facts = new HashMap<String, Object>();
|
||||
facts.put("运输类型", "国内运输拼车");
|
||||
facts.put("线路类别", "国内长途/零担");
|
||||
facts.put("起运港", "广州");
|
||||
facts.put("目的港", "深圳");
|
||||
facts.put("品名", "衣服");
|
||||
facts.put("体积", "100CBM");
|
||||
facts.put("毛重", "50kg");
|
||||
Map<String, Object> seg = TmsLandSegmentMapper.build(facts);
|
||||
java.util.List<String> missing = TmsLandSegmentMapper.missingQuery(
|
||||
"WO202609170005", "2026-09-17", seg);
|
||||
assertEquals("DOMESTIC_LTL", seg.get("landTransportType"));
|
||||
assertEquals("DOMESTIC_LONG_LTL", seg.get("routeCategory"));
|
||||
assertNull(seg.get("quantity"));
|
||||
assertEquals(java.util.Collections.emptyList(), missing);
|
||||
}
|
||||
}
|
||||
+25
@@ -105,4 +105,29 @@ public class TmsV2RequestMapperTest {
|
||||
assertEquals("40HQ", segs.get(0).get("containerType"));
|
||||
assertEquals(Integer.valueOf(1), segs.get(0).get("containerQty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landFieldsPassThroughWithoutClass() {
|
||||
Map<String, Object> seg = new HashMap<>();
|
||||
seg.put("origin", "广州");
|
||||
seg.put("destination", "河内");
|
||||
seg.put("cargoName", "衣服");
|
||||
seg.put("landTransportType", "跨境集拼");
|
||||
seg.put("landRouteCategory", "东南亚");
|
||||
seg.put("volumeCbm", "100");
|
||||
seg.put("weightKg", "50");
|
||||
seg.put("landClass", "急件");
|
||||
Map<String, Object> internal = new HashMap<>();
|
||||
internal.put("transportMode", "LAND");
|
||||
internal.put("quoteDate", "2026-09-17");
|
||||
internal.put("workOrderNo", "WO202609170001");
|
||||
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("CROSS_BORDER_LTL", segs.get(0).get("landTransportType"));
|
||||
assertEquals("SOUTHEAST_ASIA", segs.get(0).get("routeCategory"));
|
||||
assertFalse(segs.get(0).containsKey("landRouteCategory"));
|
||||
assertFalse(segs.get(0).containsKey("landClass"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user