空运人工价锁舱现拉补全舱位并拦清缺参;成交/报价填表与工单陆运费率展示同步收紧。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
@@ -1171,6 +1171,27 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
one.put("delivery_fee", out.getString("delivery_fee"));
|
||||
one.put("price_per_cbm", out.getString("price_per_cbm"));
|
||||
one.put("price_per_kg", out.getString("price_per_kg"));
|
||||
// 产品文字报「2-500KG 单价…」时 weightPrices / 费率在 quote 顶层,无 TMS landSegments。
|
||||
Object wp = q.get("weightPrices");
|
||||
if (wp != null) {
|
||||
one.put("weightPrices", wp);
|
||||
}
|
||||
String fuelRate = firstNonBlankLocal(
|
||||
out.getString("fuel_surcharge_rate"),
|
||||
first(q, "fuel_surcharge_rate"),
|
||||
first(q, "fuelSurchargeRate"));
|
||||
if (StringUtils.isNotBlank(fuelRate)) {
|
||||
one.put("fuel_surcharge_rate", fuelRate);
|
||||
one.put("fuelSurchargeRate", fuelRate);
|
||||
}
|
||||
String taxRate = firstNonBlankLocal(
|
||||
out.getString("tax_rate"),
|
||||
first(q, "tax_rate"),
|
||||
first(q, "taxRate"));
|
||||
if (StringUtils.isNotBlank(taxRate)) {
|
||||
one.put("tax_rate", taxRate);
|
||||
one.put("taxRate", taxRate);
|
||||
}
|
||||
String landAmt = firstNonBlankLocal(out.getString("land_freight"), out.getString("price_per_cbm"));
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject fee = rows.getJSONObject(i);
|
||||
|
||||
+91
-10
@@ -243,6 +243,9 @@ public final class QuoteTemplateExcelSupport {
|
||||
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("fuelSurchargeRate", "fuel_surcharge_rate");
|
||||
LAND_TMS_KEY_TO_CODE.put("rate", "tax_rate");
|
||||
LAND_TMS_KEY_TO_CODE.put("rateFlag", "tax_included");
|
||||
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");
|
||||
@@ -2853,15 +2856,17 @@ public final class QuoteTemplateExcelSupport {
|
||||
writeFeeCol(sheet, ri, columns.getString("remark"), rowObj.getString("remark"), yellow, sheet.getSheetName());
|
||||
if (columns.containsKey("tax_rate")) {
|
||||
writeFeeCol(sheet, ri, columns.getString("tax_rate"),
|
||||
firstNonBlank(rowObj.getString("tax_rate"), rowObj.getString("taxRate"),
|
||||
facts == null ? null : facts.getString("tax_rate")),
|
||||
formatLandTaxRateDisplay(firstNonBlank(rowObj.getString("tax_rate"),
|
||||
rowObj.getString("taxRate"),
|
||||
rowObj.getString("rate"),
|
||||
facts == null ? null : facts.getString("tax_rate"))),
|
||||
yellow, sheet.getSheetName());
|
||||
}
|
||||
if (columns.containsKey("fuel_surcharge_rate")) {
|
||||
writeFeeCol(sheet, ri, columns.getString("fuel_surcharge_rate"),
|
||||
firstNonBlank(rowObj.getString("fuel_surcharge_rate"),
|
||||
formatLandPercentDisplay(firstNonBlank(rowObj.getString("fuel_surcharge_rate"),
|
||||
rowObj.getString("fuelSurchargeRate"),
|
||||
facts == null ? null : facts.getString("fuel_surcharge_rate")),
|
||||
facts == null ? null : facts.getString("fuel_surcharge_rate"))),
|
||||
yellow, sheet.getSheetName());
|
||||
}
|
||||
writeLandMatrixSideCols(sheet, ri, region, rowObj, facts, yellow);
|
||||
@@ -2937,12 +2942,19 @@ public final class QuoteTemplateExcelSupport {
|
||||
facts == null ? null : facts.getString("currency"));
|
||||
}
|
||||
if (header.contains("燃油附加")) {
|
||||
return firstNonBlank(rowObj.getString("fuel_surcharge_rate"), rowObj.getString("fuelSurchargeRate"),
|
||||
facts == null ? null : facts.getString("fuel_surcharge_rate"));
|
||||
return formatLandPercentDisplay(firstNonBlank(rowObj.getString("fuel_surcharge_rate"),
|
||||
rowObj.getString("fuelSurchargeRate"),
|
||||
facts == null ? null : facts.getString("fuel_surcharge_rate")));
|
||||
}
|
||||
if ("税率".equals(header)) {
|
||||
return firstNonBlank(rowObj.getString("tax_rate"), rowObj.getString("taxRate"),
|
||||
facts == null ? null : facts.getString("tax_rate"));
|
||||
return formatLandTaxRateDisplay(firstNonBlank(rowObj.getString("tax_rate"), rowObj.getString("taxRate"),
|
||||
rowObj.getString("rate"),
|
||||
facts == null ? null : facts.getString("tax_rate")));
|
||||
}
|
||||
if (header.contains("是否含税") || "含税".equals(header)) {
|
||||
return firstNonBlank(rowObj.getString("tax_included"),
|
||||
formatLandTaxIncludedDisplay(firstNonBlank(rowObj.getString("rateFlag"),
|
||||
rowObj.getString("rate_flag"))));
|
||||
}
|
||||
if (header.contains("含税合计") || header.contains("杂费合计")) {
|
||||
return firstNonBlank(rowObj.getString("amount"), rowObj.getString("salePrice"),
|
||||
@@ -3179,14 +3191,21 @@ public final class QuoteTemplateExcelSupport {
|
||||
out.put("transit_time", transit);
|
||||
out.put("transitDays", transit);
|
||||
}
|
||||
String fuel = firstNonBlank(row.getString("fuel_surcharge_rate"), row.getString("fuelSurchargeRate"));
|
||||
String fuel = formatLandPercentDisplay(firstNonBlank(
|
||||
row.getString("fuel_surcharge_rate"), row.getString("fuelSurchargeRate")));
|
||||
if (StringUtils.isNotBlank(fuel)) {
|
||||
out.put("fuel_surcharge_rate", fuel);
|
||||
}
|
||||
String tax = firstNonBlank(row.getString("tax_rate"), row.getString("taxRate"));
|
||||
String tax = formatLandTaxRateDisplay(firstNonBlank(
|
||||
row.getString("tax_rate"), row.getString("taxRate"), row.getString("rate")));
|
||||
if (StringUtils.isNotBlank(tax)) {
|
||||
out.put("tax_rate", tax);
|
||||
}
|
||||
String included = firstNonBlank(row.getString("tax_included"),
|
||||
formatLandTaxIncludedDisplay(firstNonBlank(row.getString("rateFlag"), row.getString("rate_flag"))));
|
||||
if (StringUtils.isNotBlank(included)) {
|
||||
out.put("tax_included", included);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -3829,6 +3848,68 @@ public final class QuoteTemplateExcelSupport {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运燃油附加费率写入格子:数字带 %;已带 % 保留。空值不写。
|
||||
*/
|
||||
static String formatLandPercentDisplay(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if (text.contains("%")) {
|
||||
return text;
|
||||
}
|
||||
if (text.matches("-?\\d+(\\.\\d+)?")) {
|
||||
if (text.contains(".")) {
|
||||
text = text.replaceAll("0+$", "").replaceAll("\\.$", "");
|
||||
}
|
||||
return text + "%";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 税率 rate:0.09→9%;大于 1 按百分点(10→10%)。
|
||||
*/
|
||||
static String formatLandTaxRateDisplay(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if (text.contains("%")) {
|
||||
return text;
|
||||
}
|
||||
if (!text.matches("-?\\d+(\\.\\d+)?")) {
|
||||
return text;
|
||||
}
|
||||
try {
|
||||
java.math.BigDecimal n = new java.math.BigDecimal(text);
|
||||
java.math.BigDecimal abs = n.abs();
|
||||
if (abs.compareTo(java.math.BigDecimal.ZERO) > 0
|
||||
&& abs.compareTo(java.math.BigDecimal.ONE) <= 0) {
|
||||
n = n.multiply(new java.math.BigDecimal("100")).stripTrailingZeros();
|
||||
}
|
||||
return n.stripTrailingZeros().toPlainString() + "%";
|
||||
} catch (RuntimeException ex) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/** rateFlag:1=含税,0=不含税。 */
|
||||
static String formatLandTaxIncludedDisplay(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if ("1".equals(text) || "true".equalsIgnoreCase(text) || "是".equals(text) || "含税".equals(text)) {
|
||||
return "含税";
|
||||
}
|
||||
if ("0".equals(text) || "false".equalsIgnoreCase(text) || "否".equals(text) || "不含税".equals(text)) {
|
||||
return "不含税";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void writeFeeRows(Workbook wb, JSONObject mapping, JSONObject facts, Set<String> yellow) {
|
||||
JSONObject feeRows = mapping.getJSONObject("fee_rows");
|
||||
if (feeRows == null) {
|
||||
|
||||
+175
-18
@@ -75,11 +75,16 @@ public class InquiryTmsCabinService {
|
||||
Map<String, Object> quote = payloadOf(rows.get(0));
|
||||
// 「AIR-OPT01 TH」被误落成航线报价不算真正人工价 → 仍走仅 TMS(SYSTEM)。
|
||||
boolean latestManual = isManualQuote(quote) && !isFakeManualLockQuote(quote);
|
||||
if (!latestManual && StringUtils.isBlank(resolveQuoteId(quote, no))) {
|
||||
// 库里只有人工价、没有舱位时也要现拉 TMS:补 quoteId/airOptions,
|
||||
// 避免 MANUAL 空壳 1001,也避免逼航线手打航班号。
|
||||
boolean cabinSparse = StringUtils.isBlank(resolveQuoteId(quote, no))
|
||||
|| airOptions(quote).isEmpty();
|
||||
if (cabinSparse) {
|
||||
quote = refreshLockQuote(ticket, quote);
|
||||
}
|
||||
String tmsQuoteId = realTmsQuoteId(rows, no);
|
||||
if (!latestManual && StringUtils.isBlank(tmsQuoteId)) {
|
||||
// 现拉到的 TMS 编号也算:库里无历史行时仍可 MANUAL+quoteId。
|
||||
if (StringUtils.isBlank(tmsQuoteId)) {
|
||||
tmsQuoteId = resolveQuoteId(quote, no);
|
||||
}
|
||||
boolean hasTms = StringUtils.isNotBlank(tmsQuoteId);
|
||||
@@ -140,26 +145,68 @@ public class InquiryTmsCabinService {
|
||||
text(option, "destination"), ticket.getDestination(),
|
||||
fact(ticket, "目的地", "目的港")));
|
||||
// 航司:句中两位码(如「AIR-OPT01 TH」)> 舱位/报价字段 > 航班号前两位。
|
||||
req.put("airline", firstNonBlank(
|
||||
// airlineName 中文名不能当航司码出站,只取两位码或航班前缀。
|
||||
String airlineCode = firstNonBlank(
|
||||
text(body, "airline", "airlineCode"),
|
||||
airlineFromInstruction(text(body, "instructionText", "instruction_text")),
|
||||
text(option, "airlineCode", "airline"),
|
||||
text(option, "airlineName"),
|
||||
text(quote, "airlineCode", "airline", "airlineName"),
|
||||
fact(ticket, "航司", "航司代码"),
|
||||
airlineFromFlight(firstNonBlank(text(option, "flightNo"), text(quote, "flightNo")))));
|
||||
text(quote, "airlineCode", "airline"),
|
||||
fact(ticket, "航司代码"),
|
||||
airlineFromFlight(firstNonBlank(text(option, "flightNo"), text(quote, "flightNo"))),
|
||||
airlineCodeOrBlank(text(option, "airlineName")),
|
||||
airlineCodeOrBlank(text(quote, "airlineName")),
|
||||
airlineCodeOrBlank(fact(ticket, "航司")));
|
||||
req.put("airline", airlineCode);
|
||||
req.put("weightKg", ticket.getWeightKg());
|
||||
req.put("volumeCbm", ticket.getVolumeCbm());
|
||||
req.put("pieceQty", ticket.getPieces());
|
||||
// 计费重:舱位 > 报价 > 工单计费重事实 > 毛重(避免 MANUAL 空壳 1001)。
|
||||
req.put("chargeableWeightKg", firstNonBlank(
|
||||
text(option, "chargeableWeightKg", "chargeable_weight_kg"),
|
||||
text(quote, "chargeableWeightKg", "chargeable_weight_kg")));
|
||||
text(quote, "chargeableWeightKg", "chargeable_weight_kg"),
|
||||
fact(ticket, "计费重", "计费重量"),
|
||||
ticket.getWeightKg() == null ? "" : String.valueOf(ticket.getWeightKg()),
|
||||
fact(ticket, "毛重")));
|
||||
req.put("operatorUserId", text(body, "operatorUserId", "sender_id"));
|
||||
req.put("operatorName", text(body, "operatorName", "operator_name"));
|
||||
// 重试漏传姓名时用 userid 兜底,仍要有 operatorName 键。
|
||||
req.put("operatorName", firstNonBlank(
|
||||
text(body, "operatorName", "operator_name"),
|
||||
text(body, "operatorUserId", "sender_id")));
|
||||
req.put("instructionText", text(body, "instructionText", "instruction_text"));
|
||||
if ("MANUAL".equals(lockSource)) {
|
||||
// 有 TMS 时保留 quoteId;纯人工才去掉。
|
||||
fillManualLock(req, quote, option, body, StringUtils.isNotBlank(quoteId));
|
||||
// 人工价常不写有效期:用航班日当天 23:59:59+08:00 兜底,避免空壳 1001。
|
||||
if (StringUtils.isBlank(text(req, "validUntil"))) {
|
||||
String isoUntil = normalizeValidUntilIso(text(req, "flightDate"));
|
||||
if (StringUtils.isNotBlank(isoUntil)) {
|
||||
req.put("validUntil", isoUntil);
|
||||
if (StringUtils.isBlank(text(req, "requestedUntil"))) {
|
||||
req.put("requestedUntil", isoUntil);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 缺必填仍出站会被 TMS 1001;在此拦住并回清原因。
|
||||
String incomplete = validateManualLockOutbound(req);
|
||||
// 现拉仍无舱位时,改成业务补问口径(禁止堆「缺少 optionNo」技术字段名)。
|
||||
if (StringUtils.isNotBlank(incomplete)) {
|
||||
String refreshClass = text(quote, "_lockRefreshClass");
|
||||
String refreshMsg = text(quote, "_lockRefreshMsg");
|
||||
boolean noMatch = "TMS_NO_QUOTE_1002".equals(refreshClass)
|
||||
|| (StringUtils.isNotBlank(refreshMsg) && refreshMsg.contains("无匹配"));
|
||||
if (noMatch) {
|
||||
incomplete = "本单是手工报价,且运价库无匹配舱位,无法自动带出航班。"
|
||||
+ "请按「AIR-OPT01 CZ 航班CZ327」补舱位、航司和航班号后再锁。"
|
||||
+ "(现拉TMS:" + firstNonBlank(refreshMsg, "标准运价库无匹配报价") + ")";
|
||||
} else if (StringUtils.isNotBlank(refreshMsg)) {
|
||||
incomplete = incomplete + "(现拉TMS:" + refreshMsg + ")";
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(incomplete)) {
|
||||
log.info("tms.lock incomplete wo={} optionNo={} missing={}",
|
||||
no, resolvedOption, incomplete);
|
||||
return fail("lock_incomplete", incomplete);
|
||||
}
|
||||
} else {
|
||||
// 仅 TMS:source=SYSTEM + quoteId。禁止再塞 validUntil/feeItems(会 1001)。
|
||||
req.put("source", "SYSTEM");
|
||||
@@ -315,14 +362,26 @@ public class InquiryTmsCabinService {
|
||||
}
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("workOrderNo", ticket.getWorkOrderNo());
|
||||
// 锁舱现拉必须用「今天」报价日:历史询价日常已无运价(1002),
|
||||
// 不能把销售初询那天的日期带出去导致永远补不到舱位。
|
||||
JSONObject facts = new JSONObject();
|
||||
if (StringUtils.isNotBlank(ticket.getFactsJson())) {
|
||||
JSONObject facts = JSON.parseObject(ticket.getFactsJson());
|
||||
if (facts != null) {
|
||||
body.put("facts", facts);
|
||||
JSONObject stored = JSON.parseObject(ticket.getFactsJson());
|
||||
if (stored != null) {
|
||||
facts.putAll(stored);
|
||||
}
|
||||
}
|
||||
SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd");
|
||||
dayFmt.setTimeZone(java.util.TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
String today = dayFmt.format(new java.util.Date());
|
||||
facts.put("报价日期", today);
|
||||
body.put("facts", facts);
|
||||
try {
|
||||
Map<String, Object> live = queryService.queryFromAgent(body);
|
||||
if (live != null && live.get("classification") != null) {
|
||||
current.put("_lockRefreshClass", String.valueOf(live.get("classification")));
|
||||
current.put("_lockRefreshMsg", String.valueOf(live.get("message") == null ? "" : live.get("message")));
|
||||
}
|
||||
Object raw = live == null ? null : live.get("quote");
|
||||
if (!(raw instanceof Map)) {
|
||||
return current;
|
||||
@@ -335,14 +394,19 @@ public class InquiryTmsCabinService {
|
||||
if (airOptions(current).isEmpty() && fetched.get("airOptions") != null) {
|
||||
current.put("airOptions", fetched.get("airOptions"));
|
||||
}
|
||||
if (airOptions(current).isEmpty() && fetched.get("air_options") != null) {
|
||||
current.put("airOptions", fetched.get("air_options"));
|
||||
}
|
||||
if (StringUtils.isBlank(text(current, "chargeableWeightKg", "chargeable_weight_kg"))
|
||||
&& StringUtils.isNotBlank(text(fetched, "chargeableWeightKg", "chargeable_weight_kg"))) {
|
||||
current.put("chargeableWeightKg", text(fetched, "chargeableWeightKg", "chargeable_weight_kg"));
|
||||
}
|
||||
log.info("tms.lock refresh wo={} hasQuoteId={} airOpt={}",
|
||||
log.info("tms.lock refresh wo={} hasQuoteId={} airOpt={} class={} quoteDate={}",
|
||||
ticket.getWorkOrderNo(),
|
||||
StringUtils.isNotBlank(text(current, "quoteId", "quote_id")),
|
||||
airOptions(current).size());
|
||||
airOptions(current).size(),
|
||||
live == null ? "" : live.get("classification"),
|
||||
today);
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("tms.lock refresh fail wo={} err={}", ticket.getWorkOrderNo(), ex.getMessage());
|
||||
}
|
||||
@@ -416,7 +480,11 @@ public class InquiryTmsCabinService {
|
||||
if (StringUtils.isNotBlank(transfer)) {
|
||||
req.put("transferAirport", transfer);
|
||||
}
|
||||
String flightNo = firstNonBlank(text(option, "flightNo"), text(quote, "flightNo"));
|
||||
// 句中/请求体点名的航班号优先,其次舱位与报价。
|
||||
String flightNo = firstNonBlank(
|
||||
text(body, "flightNo", "flight_no"),
|
||||
text(option, "flightNo", "flight_no"),
|
||||
text(quote, "flightNo", "flight_no"));
|
||||
if (StringUtils.isNotBlank(flightNo)) {
|
||||
req.put("flightNo", flightNo);
|
||||
}
|
||||
@@ -463,11 +531,41 @@ public class InquiryTmsCabinService {
|
||||
List<Map<String, Object>> departure = feeRows(quote, option, "departureCharges", "departure_charges");
|
||||
List<Map<String, Object>> fees = feeRows(quote, option, "feeItems", "fee_items");
|
||||
splitFeeRows(quote, departure, fees);
|
||||
if (!departure.isEmpty()) {
|
||||
req.put("departureCharges", toLockFees(departure, true));
|
||||
}
|
||||
// 运行证据(2026-10-07):只要出站带 departureCharges,TMS 一律 1001「参数格式错误」;
|
||||
// 去掉后变为业务码 2008。feeItems 可保留。故 MANUAL 锁舱不再出站 departureCharges。
|
||||
if (!fees.isEmpty()) {
|
||||
req.put("feeItems", toLockFees(fees, false));
|
||||
} else if (!departure.isEmpty()) {
|
||||
// 仅有起运港费、无 feeItems 时,折进 feeItems,避免再走 departureCharges。
|
||||
List<Map<String, Object>> asFees = new ArrayList<>();
|
||||
for (Map<String, Object> row : toLockFees(departure, true)) {
|
||||
Map<String, Object> fee = new LinkedHashMap<>();
|
||||
String name = text(row, "chargeName", "feeName");
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
fee.put("feeName", name);
|
||||
}
|
||||
if (row.get("unitPrice") != null) {
|
||||
fee.put("unitPrice", row.get("unitPrice"));
|
||||
}
|
||||
if (row.get("quantity") != null) {
|
||||
fee.put("quantity", row.get("quantity"));
|
||||
}
|
||||
if (row.get("amount") != null) {
|
||||
fee.put("amount", row.get("amount"));
|
||||
}
|
||||
if (row.get("currency") != null) {
|
||||
fee.put("currency", row.get("currency"));
|
||||
}
|
||||
if (row.get("unit") != null) {
|
||||
fee.put("unit", row.get("unit"));
|
||||
}
|
||||
if (!fee.isEmpty()) {
|
||||
asFees.add(fee);
|
||||
}
|
||||
}
|
||||
if (!asFees.isEmpty()) {
|
||||
req.put("feeItems", asFees);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,10 +789,24 @@ public class InquiryTmsCabinService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本单真实 TMS quoteId:优先非人工报价行;人工覆盖后再锁仍要带原编号。
|
||||
* 跳过 QT-WO 假编号。
|
||||
*/
|
||||
static String realTmsQuoteId(List<InquiryQuote> rows, String workOrderNo) {
|
||||
if (rows == null) {
|
||||
return "";
|
||||
}
|
||||
for (InquiryQuote row : rows) {
|
||||
Map<String, Object> payload = payloadOf(row);
|
||||
if (isManualQuote(payload) && !isFakeManualLockQuote(payload)) {
|
||||
continue;
|
||||
}
|
||||
String id = resolveQuoteId(payload, workOrderNo);
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
for (InquiryQuote row : rows) {
|
||||
String id = resolveQuoteId(payloadOf(row), workOrderNo);
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
@@ -704,6 +816,51 @@ public class InquiryTmsCabinService {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* MANUAL 出站必填校验。缺字段直接失败,禁止 HMAC 空壳打 TMS(会 1001)。
|
||||
* 返回中文原因;齐了返回空串。
|
||||
*/
|
||||
static String validateManualLockOutbound(Map<String, Object> req) {
|
||||
if (req == null) {
|
||||
return "锁舱参数为空";
|
||||
}
|
||||
java.util.List<String> missing = new ArrayList<>();
|
||||
if (StringUtils.isBlank(text(req, "optionNo", "option_no"))) {
|
||||
missing.add("舱位编号optionNo");
|
||||
}
|
||||
if (StringUtils.isBlank(text(req, "airline", "airlineCode"))) {
|
||||
missing.add("航司airline");
|
||||
}
|
||||
if (StringUtils.isBlank(text(req, "chargeableWeightKg", "chargeable_weight_kg"))) {
|
||||
missing.add("计费重chargeableWeightKg");
|
||||
}
|
||||
if (StringUtils.isBlank(text(req, "flightNo", "flight_no"))) {
|
||||
missing.add("航班号flightNo");
|
||||
}
|
||||
if (StringUtils.isBlank(text(req, "operatorName", "operator_name"))) {
|
||||
missing.add("操作人operatorName");
|
||||
}
|
||||
if (StringUtils.isBlank(text(req, "validUntil", "valid_until"))) {
|
||||
missing.add("有效期validUntil");
|
||||
}
|
||||
if (missing.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "锁舱参数不完整,缺少:" + String.join("、", missing)
|
||||
+ "。请按「AIR-OPT01 CZ 航班CZ327」补舱位、航司和航班号后再锁。";
|
||||
}
|
||||
|
||||
/**
|
||||
* 航司名只有恰好两位字母时才当代码;「南航」等中文名不当 airline 出站。
|
||||
*/
|
||||
static String airlineCodeOrBlank(String raw) {
|
||||
String text = StringUtils.trimToEmpty(raw).toUpperCase();
|
||||
if (text.length() == 2 && Character.isLetter(text.charAt(0)) && Character.isLetter(text.charAt(1))) {
|
||||
return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private List<InquiryQuote> quoteRows(String workOrderNo) {
|
||||
if (StringUtils.isBlank(workOrderNo)) {
|
||||
return new ArrayList<>();
|
||||
|
||||
+81
@@ -484,6 +484,21 @@ public class InquiryTmsQueryService {
|
||||
putExtra(row, pair[1], firstText(seg.get(pair[0]), seg.get(pair[1])));
|
||||
putExtra(row, pair[0], firstText(seg.get(pair[0])));
|
||||
}
|
||||
// 陆运 TMS:fuelSurchargeRate=燃油附加费率;rate=税率;rateFlag=是否含税(1是0否)。
|
||||
// 只投影 landSegments,禁止拿 fxRates.rate(汇率)冒充税率。
|
||||
String fuelRate = formatLandPercentRate(firstText(
|
||||
seg.get("fuelSurchargeRate"), seg.get("fuel_surcharge_rate")));
|
||||
putExtra(row, "fuel_surcharge_rate", fuelRate);
|
||||
putExtra(row, "fuelSurchargeRate", fuelRate);
|
||||
String taxRate = formatLandTaxRate(firstText(
|
||||
seg.get("rate"), seg.get("tax_rate"), seg.get("taxRate")));
|
||||
putExtra(row, "tax_rate", taxRate);
|
||||
putExtra(row, "taxRate", taxRate);
|
||||
putExtra(row, "rate", firstText(seg.get("rate")));
|
||||
String rateFlagRaw = firstText(seg.get("rateFlag"), seg.get("rate_flag"));
|
||||
putExtra(row, "rateFlag", rateFlagRaw);
|
||||
putExtra(row, "rate_flag", rateFlagRaw);
|
||||
putExtra(row, "tax_included", formatLandTaxIncluded(rateFlagRaw));
|
||||
if (StringUtils.isNotBlank(row.get("origin")) || StringUtils.isNotBlank(row.get("destination"))
|
||||
|| StringUtils.isNotBlank(row.get("amount"))) {
|
||||
out.add(row);
|
||||
@@ -1004,6 +1019,72 @@ public class InquiryTmsQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 燃油附加费率展示:数字带 %;已带 % 保留。空值不编造。
|
||||
*/
|
||||
static String formatLandPercentRate(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if (text.contains("%")) {
|
||||
return text;
|
||||
}
|
||||
if (text.matches("-?\\d+(\\.\\d+)?")) {
|
||||
if (text.contains(".")) {
|
||||
text = text.replaceAll("0+$", "").replaceAll("\\.$", "");
|
||||
}
|
||||
return text + "%";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 税率 rate 展示:TMS 常回小数(0.09=9%);大于 1 的按百分点(10=10%)。
|
||||
*/
|
||||
static String formatLandTaxRate(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if (text.contains("%")) {
|
||||
return text;
|
||||
}
|
||||
if (!text.matches("-?\\d+(\\.\\d+)?")) {
|
||||
return text;
|
||||
}
|
||||
try {
|
||||
java.math.BigDecimal n = new java.math.BigDecimal(text);
|
||||
java.math.BigDecimal abs = n.abs();
|
||||
if (abs.compareTo(java.math.BigDecimal.ZERO) > 0
|
||||
&& abs.compareTo(java.math.BigDecimal.ONE) <= 0) {
|
||||
n = n.multiply(java.math.BigDecimal.TEN).multiply(java.math.BigDecimal.TEN)
|
||||
.stripTrailingZeros();
|
||||
}
|
||||
String out = n.stripTrailingZeros().toPlainString();
|
||||
return out + "%";
|
||||
} catch (RuntimeException ex) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rateFlag:是否含税。字典 1=是、0=否。
|
||||
*/
|
||||
static String formatLandTaxIncluded(String raw) {
|
||||
if (StringUtils.isBlank(raw) || "-".equals(raw) || "null".equalsIgnoreCase(raw.trim())) {
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim();
|
||||
if ("1".equals(text) || "true".equalsIgnoreCase(text) || "是".equals(text) || "含税".equals(text)) {
|
||||
return "含税";
|
||||
}
|
||||
if ("0".equals(text) || "false".equalsIgnoreCase(text) || "否".equals(text) || "不含税".equals(text)) {
|
||||
return "不含税";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void addFeeLine(List<Map<String, String>> lines, String name, String amount,
|
||||
String currency, Map<String, String> extras) {
|
||||
TmsQuoteCurrency.Money money = TmsQuoteCurrency.split(amount, currency);
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ VALUES
|
||||
('brv10000000000000000000000000038','deal','已成交','deal.recorded_won','销售选择「成交」','已记录为「已成交」。工单 {{ticket_no}} 已关闭智能体跟进。',1,1,'admin',NOW()),
|
||||
('brv10000000000000000000000000039','negotiating','协商中','negotiating.recorded','成交跟进中选择「协商中」','已记录为「协商中」。已顺延三个工作日,{{followup_when}}的9:00再次发起成交状态询问。',1,1,'admin',NOW()),
|
||||
('brv10000000000000000000000000040','lost','未成交','lost.ask_reason','成交跟进中选择「未成交」且未写原因','已记录为「未成交」。请补充未成交原因。',1,1,'admin',NOW()),
|
||||
('brv10000000000000000000000000041','lost','未成交','lost.reason_ack','销售回复未成交原因','已记下未成交原因:{{lost_reason}}。',1,2,'admin',NOW()),
|
||||
('brv10000000000000000000000000041','lost','未成交','lost.reason_ack','销售回复未成交原因','已将未成交原因“{{lost_reason}}”记录在后台,工单 {{ticket_no}}已关闭,智能体不再跟进该工单,请悉知。',1,2,'admin',NOW()),
|
||||
('brv10000000000000000000000000042','closed','已关闭','closed.air_cannot_activate','空运工单已是终态仍要激活','工单{{ticket_no}}已是「{{status}}」,不能再激活。',1,1,'admin',NOW()),
|
||||
('brv10000000000000000000000000043','sys-ex','系统异常','sys.llm_fail','大模型调用失败(销售侧)','大模型服务异常,IT排查中...',1,1,'admin',NOW()),
|
||||
('brv10000000000000000000000000044','sys-ex','系统异常','sys.tms_fail','TMS 接口失败(销售侧)','TMS接口异常,IT运维排查中...',1,2,'admin',NOW()),
|
||||
|
||||
+54
@@ -2231,4 +2231,58 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运运价表:燃油附加费率、税率必须写到表头同列的数据行。
|
||||
* rate=0.09→9%;rateFlag 是是否含税,不写进税率格。
|
||||
*/
|
||||
@Test
|
||||
public void landGroupageFillsFuelSurchargeAndTaxRateOnMatrix() throws Exception {
|
||||
Path src = Files.createTempFile("quote-land-fuel-tax-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-land-fuel-tax-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("中越集拼报价");
|
||||
org.apache.poi.ss.usermodel.Row matrix = row(sheet, 7);
|
||||
matrix.createCell(1).setCellValue("始发地");
|
||||
matrix.createCell(2).setCellValue("目的地");
|
||||
matrix.createCell(9).setCellValue("币种");
|
||||
matrix.createCell(10).setCellValue("燃油附加费率");
|
||||
matrix.createCell(11).setCellValue("税率");
|
||||
row(sheet, 9);
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("sheet", "中越集拼报价");
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject region = new JSONObject(true);
|
||||
region.put("headerRow", 8);
|
||||
region.put("dataStartRow", 10);
|
||||
JSONObject columns = new JSONObject(true);
|
||||
columns.put("origin", "B");
|
||||
columns.put("destination", "C");
|
||||
region.put("columns", columns);
|
||||
regions.add(region);
|
||||
mapping.put("routeRegions", regions);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("route_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"origin\":\"广州\",\"destination\":\"河内\",\"fuelSurchargeRate\":\"8\",\"rate\":\"0.09\",\"rateFlag\":\"1\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals("广州", read(sheet, 9, 1));
|
||||
assertEquals("河内", read(sheet, 9, 2));
|
||||
assertEquals("8%", read(sheet, 9, 10));
|
||||
assertEquals("9%", read(sheet, 9, 11));
|
||||
assertEquals("燃油附加费率", read(sheet, 7, 10));
|
||||
assertEquals("税率", read(sheet, 7, 11));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
assertEquals("9%", QuoteTemplateExcelSupport.formatLandTaxRateDisplay("0.09"));
|
||||
assertEquals("8%", QuoteTemplateExcelSupport.formatLandPercentDisplay("8.0"));
|
||||
assertEquals("含税", QuoteTemplateExcelSupport.formatLandTaxIncludedDisplay("1"));
|
||||
}
|
||||
}
|
||||
|
||||
+38
-4
@@ -178,10 +178,8 @@ public class InquiryTmsCabinServiceTest {
|
||||
InquiryTmsCabinService.fillLockQuoteExtras(req, quote, option);
|
||||
|
||||
assertEquals("2026-09-30T23:59:59+08:00", req.get("validUntil"));
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> dep =
|
||||
(java.util.List<Map<String, Object>>) req.get("departureCharges");
|
||||
assertEquals("运输费", dep.get(0).get("chargeName"));
|
||||
// departureCharges 出站会 TMS 1001,只保留 feeItems。
|
||||
org.junit.jupiter.api.Assertions.assertNull(req.get("departureCharges"));
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> fees =
|
||||
(java.util.List<Map<String, Object>>) req.get("feeItems");
|
||||
@@ -261,4 +259,40 @@ public class InquiryTmsCabinServiceTest {
|
||||
InquiryTmsCabinService.fillManualLock(req, quote, new HashMap<>(), new HashMap<>(), false);
|
||||
org.junit.jupiter.api.Assertions.assertNull(req.get("quoteId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateManualLockOutboundReportsMissingFields() {
|
||||
Map<String, Object> empty = new HashMap<>();
|
||||
String why = InquiryTmsCabinService.validateManualLockOutbound(empty);
|
||||
org.junit.jupiter.api.Assertions.assertTrue(why.contains("optionNo"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(why.contains("airline"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(why.contains("chargeableWeightKg"));
|
||||
|
||||
Map<String, Object> ok = new HashMap<>();
|
||||
ok.put("optionNo", "AIR-OPT01");
|
||||
ok.put("airline", "CZ");
|
||||
ok.put("chargeableWeightKg", "100");
|
||||
ok.put("flightNo", "CZ327");
|
||||
ok.put("operatorName", "航线");
|
||||
ok.put("validUntil", "2026-10-01T23:59:59+08:00");
|
||||
assertEquals("", InquiryTmsCabinService.validateManualLockOutbound(ok));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airlineCodeOrBlankRejectsChineseName() {
|
||||
assertEquals("CZ", InquiryTmsCabinService.airlineCodeOrBlank("CZ"));
|
||||
assertEquals("", InquiryTmsCabinService.airlineCodeOrBlank("南航"));
|
||||
assertEquals("", InquiryTmsCabinService.airlineCodeOrBlank(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realTmsQuoteIdSkipsManualAndKeepsOlderTms() {
|
||||
org.jeecg.modules.inquiry.entity.InquiryQuote manual = new org.jeecg.modules.inquiry.entity.InquiryQuote();
|
||||
manual.setPayloadJson("{\"source\":\"PRODUCT\",\"source_label\":\"航线报价\",\"total\":\"200\"}");
|
||||
org.jeecg.modules.inquiry.entity.InquiryQuote tms = new org.jeecg.modules.inquiry.entity.InquiryQuote();
|
||||
tms.setPayloadJson("{\"source\":\"TMS\",\"quoteId\":\"AIR-REAL-1\",\"airOptions\":[{\"optionNo\":\"AIR-OPT01\"}]}");
|
||||
String id = InquiryTmsCabinService.realTmsQuoteId(
|
||||
java.util.Arrays.asList(manual, tms), "WO202610030009");
|
||||
assertEquals("AIR-REAL-1", id);
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -184,6 +184,42 @@ public class TmsQuoteCurrencyTest {
|
||||
assertTrue(quote.get("fee_lines").contains("2800"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运 TMS:fuelSurchargeRate=燃油附加费率;rate=税率;rateFlag=是否含税。
|
||||
* 禁止用 fxRates.rate(汇率)冒充税率。
|
||||
*/
|
||||
@Test
|
||||
public void toAgentQuoteProjectsLandFuelAndTaxRate() {
|
||||
Map<String, Object> seg = new HashMap<>();
|
||||
seg.put("segmentSequence", 1);
|
||||
seg.put("origin", "广州市");
|
||||
seg.put("destination", "深圳市");
|
||||
seg.put("costPrice", "110");
|
||||
seg.put("currency", "CNY");
|
||||
seg.put("fuelSurchargeRate", 8);
|
||||
seg.put("rate", 0.09);
|
||||
seg.put("rateFlag", 1);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("quoteFound", true);
|
||||
data.put("currency", "CNY");
|
||||
data.put("landSegments", Collections.singletonList(seg));
|
||||
Map<String, Object> tms = new HashMap<>();
|
||||
tms.put("code", 1);
|
||||
tms.put("tmsData", data);
|
||||
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
|
||||
assertTrue(quote.get("route_rows").contains("\"fuel_surcharge_rate\":\"8%\""));
|
||||
assertTrue(quote.get("route_rows").contains("\"tax_rate\":\"9%\""));
|
||||
assertTrue(quote.get("route_rows").contains("\"tax_included\":\"含税\""));
|
||||
assertTrue(quote.get("route_rows").contains("\"rateFlag\":\"1\""));
|
||||
assertFalse(quote.get("route_rows").contains("fxRates"));
|
||||
assertEquals("8%", InquiryTmsQueryService.formatLandPercentRate("8"));
|
||||
assertEquals("9%", InquiryTmsQueryService.formatLandTaxRate("0.09"));
|
||||
assertEquals("10%", InquiryTmsQueryService.formatLandTaxRate("10"));
|
||||
assertEquals("含税", InquiryTmsQueryService.formatLandTaxIncluded("1"));
|
||||
assertEquals("不含税", InquiryTmsQueryService.formatLandTaxIncluded("0"));
|
||||
assertEquals("", InquiryTmsQueryService.formatLandPercentRate(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toAgentQuoteKeepsSeaBucketTotals() {
|
||||
Map<String, Object> pol = new HashMap<>();
|
||||
|
||||
+11
-2
@@ -568,13 +568,22 @@ public class LoginController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换菜单表为vue3的表
|
||||
* 切换菜单表为vue3的表。
|
||||
* <p>
|
||||
* 询价新库若已无 {@code sys_permission_v3},服务层会拒绝切换并返回错误,避免把现用菜单表 rename 掉。
|
||||
* </p>
|
||||
*/
|
||||
@RequiresRoles({"admin"})
|
||||
@GetMapping(value = "/switchVue3Menu")
|
||||
public Result<String> switchVue3Menu(HttpServletResponse response) {
|
||||
Result<String> res = new Result<String>();
|
||||
sysPermissionService.switchVue3Menu();
|
||||
try {
|
||||
sysPermissionService.switchVue3Menu();
|
||||
res.success("切换成功");
|
||||
} catch (Exception e) {
|
||||
log.error("switchVue3Menu rejected or failed: {}", e.getMessage(), e);
|
||||
res.error500(e.getMessage());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -49,6 +49,18 @@ public interface SysPermissionMapper extends BaseMapper<SysPermission> {
|
||||
public void backupVue2Menu();
|
||||
@Update("alter table sys_permission_v3 rename to sys_permission")
|
||||
public void changeVue3Menu();
|
||||
|
||||
/**
|
||||
* 是否存在 Jeecg 官方 Vue3 菜单备份表 {@code sys_permission_v3}。
|
||||
* <p>
|
||||
* 询价新库 {@code inquiry_robot} 的菜单种子本身已是 Vue3 形态,通常没有 v3 表;
|
||||
* 若此时仍执行 rename 切换,会把正在使用的 {@code sys_permission} 改名后找不到替代表,侧栏全挂。
|
||||
* </p>
|
||||
*
|
||||
* @return 存在则 > 0
|
||||
*/
|
||||
@Select("SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'sys_permission_v3'")
|
||||
Long countVue3MenuTable();
|
||||
|
||||
/**
|
||||
* 获取模糊匹配规则的数据权限URL
|
||||
|
||||
+14
@@ -55,8 +55,22 @@ public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, S
|
||||
@Resource
|
||||
private SysDepartRolePermissionMapper sysDepartRolePermissionMapper;
|
||||
|
||||
/**
|
||||
* 将菜单表从 Vue2 备份切换为 Vue3 表(Jeecg 官方升级路径)。
|
||||
* <p>
|
||||
* 副作用:{@code sys_permission} rename 为 {@code sys_permission_v2},再把 {@code sys_permission_v3}
|
||||
* rename 为 {@code sys_permission}。线程:请求线程,需 admin 角色。
|
||||
* </p>
|
||||
* <p>
|
||||
* 为何加护栏:询价测服库菜单已是 Vue3,且没有 {@code sys_permission_v3};盲目切换会弄挂后台菜单。
|
||||
* </p>
|
||||
*/
|
||||
@Override
|
||||
public void switchVue3Menu() {
|
||||
Long vue3TableCnt = sysPermissionMapper.countVue3MenuTable();
|
||||
if (vue3TableCnt == null || vue3TableCnt.longValue() <= 0L) {
|
||||
throw new JeecgBootException("当前库没有 sys_permission_v3,菜单已是 Vue3 或不可自动切换,已拒绝执行");
|
||||
}
|
||||
sysPermissionMapper.backupVue2Menu();
|
||||
sysPermissionMapper.changeVue3Menu();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user