陆运多条线路按段展示 TMS 费用名和时效,选定后再写回该条成本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 18:28:29 +08:00
co-authored by Cursor
parent e769eb9c1d
commit 49faa35f47
23 changed files with 2323 additions and 55 deletions
@@ -290,6 +290,16 @@ public class InquiryAgentController {
return Result.OK(workOrderService.queryTms(body));
}
@ApiOperation("覆盖本单最新 TMS 报价(选定陆运线路后写回成本)")
@PostMapping("/ticket/replaceTmsQuote")
public Result<Map<String, Object>> replaceTmsQuote(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.replaceTmsQuote(body));
}
@ApiOperation("海运拉群:按起运/目的匹配启用的产品·海运")
@PostMapping("/staff/matchSea")
public Result<Map<String, Object>> matchSea(@RequestBody Map<String, Object> body,
@@ -78,6 +78,12 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
*/
Map<String, Object> queryTms(Map<String, Object> body);
/**
* 覆盖本单最新 TMS 报价快照(选定陆运线路后写回成本/时效)。
* 不新插 TMS 行,不改手工/调价行。
*/
Map<String, Object> replaceTmsQuote(Map<String, Object> body);
/**
* 智能体续办:按工单号取字段/报价/推断停点。只读。
* quote 是当前生效价(可能已是销售调价);tms_quote 永远是 TMS 原价,拉群摘要必须用这份。
@@ -751,7 +751,7 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
log.warn("报价出单 mappingJson 非法 wo={} err={}", workOrderNo, e.getMessage());
return fail;
}
JSONObject fillFacts = buildFillFacts(workOrderNo, facts, quote, salesWecomId);
JSONObject fillFacts = buildFillFacts(workOrderNo, facts, quote, salesWecomId, businessLine);
String safeNo = StringUtils.defaultIfBlank(workOrderNo, "WO").replaceAll("[^A-Za-z0-9_-]", "");
Path targetDir = Paths.get(StringUtils.defaultIfBlank(uploadPath, "."), BIZ_PATH, "render");
Path target = targetDir.resolve(safeNo + "-" + System.currentTimeMillis() + ".xlsx");
@@ -782,6 +782,17 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
*/
public static JSONObject buildFillFacts(String workOrderNo, Map<String, Object> facts,
Map<String, Object> quote, String salesWecomId) {
return buildFillFacts(workOrderNo, facts, quote, salesWecomId, null);
}
/**
* 出单填表投影。空运不写时效:业务确认空运没有时效字段。
*
* @param businessLine 智能体传入的业务线;空时再看询价事实 / 报价快照
*/
public static JSONObject buildFillFacts(String workOrderNo, Map<String, Object> facts,
Map<String, Object> quote, String salesWecomId,
String businessLine) {
JSONObject out = new JSONObject(true);
Map<String, Object> src = facts == null ? new LinkedHashMap<>() : facts;
Map<String, Object> q = quote == null ? new LinkedHashMap<>() : quote;
@@ -844,8 +855,9 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
out.put("currency", currency);
}
out.put("sales_name", StringUtils.defaultString(salesWecomId));
boolean hideAirEta = looksAirInquiry(src, q, businessLine);
String eta = first(q, "eta", "transit_days", "transitDays");
if (StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
if (!hideAirEta && StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
out.put("transit_days", eta);
out.put("transit_time", eta);
}
@@ -1014,6 +1026,14 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
out.put("route_rows", routeRows);
}
}
if (hideAirEta) {
// 空运没有时效:询价事实拷进来的、TMS eta、陆运键投影一律拿掉
out.remove("transit_days");
out.remove("transit_time");
out.remove("transitDays");
out.remove("时效");
out.remove("时效(天)");
}
return out;
}
@@ -1029,6 +1049,26 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
return "";
}
/**
* 空运销售面没有时效:出单不把 eta / transit_* 填进模板。
* 业务线 AIR / 运输方式含空运 / 报价带 airOptions 都算空运。
*/
private static boolean looksAirInquiry(Map<String, Object> src, Map<String, Object> quote,
String businessLine) {
String line = StringUtils.defaultString(businessLine);
if (StringUtils.isBlank(line) && src != null) {
line = first(src, "业务线", "business_line", "businessLine", "运输方式");
}
if ("AIR".equalsIgnoreCase(line) || "空".equals(line) || (line != null && line.contains("空运"))) {
return true;
}
if (quote != null && (quote.get("airOptions") != null || quote.get("air_options") != null
|| quote.get("selected_air_option") != null)) {
return true;
}
return false;
}
/**
* 询价事实里已有陆运选项/始发站,运费兜底名就不能再写成空运费。
*/
@@ -586,6 +586,18 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
return tmsQueryService.queryFromAgent(body);
}
/**
* 选定陆运线路后覆盖 TMS 快照。请求线程只转发,不查 TMS。
*/
@Override
public Map<String, Object> replaceTmsQuote(Map<String, Object> body) {
String no = text(body, "workOrderNo", "work_order_no");
Object raw = body == null ? null : body.get("quote");
@SuppressWarnings("unchecked")
Map<String, Object> quote = raw instanceof Map ? (Map<String, Object>) raw : new LinkedHashMap<String, Object>();
return tmsQueryService.replaceTmsQuote(no, quote);
}
private String nextWorkOrderNo() {
String day = new SimpleDateFormat("yyyyMMdd").format(new Date());
long n = count(new LambdaQueryWrapper<InquiryTicket>().likeRight(InquiryTicket::getWorkOrderNo, "WO" + day));
@@ -238,12 +238,7 @@ public class InquiryTmsQueryService {
row.setTicketId(ticket.getId());
row.setWorkOrderNo(workOrderNo);
row.setQuoteVersion(1);
row.setSource("TMS");
row.setSourceLabel("TMS 标准报价");
// 后台售价栏必须带 TMS 币种,不能只落纯数字。
row.setPriceText(TmsQuoteCurrency.formatDisplay(quote.get("currency"), quote.get("total")));
row.setHighlighted(1);
row.setPayloadJson(JSON.toJSONString(quote));
applySnapshotToQuoteRow(row, quote);
row.setCreateTime(new Date());
quoteMapper.insert(row);
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
@@ -323,6 +318,7 @@ public class InquiryTmsQueryService {
quote.put("source", "TMS");
quote.put("source_label", "TMS 标准报价");
quote.put("fee_lines", buildFeeLinesJson(data, freightMoney.amount, otherMoney.amount, headerCurrency));
putFeeItemsRaw(quote, data);
// 海运 feeItems 是分段桶:polCostItems / oceanCostItems / podCostItems,单独透出给模板分区回填
putSeaFeeBuckets(quote, data);
// 锁舱必须带着 TMS 报价编号和舱位选项,不能只落金额。
@@ -349,6 +345,8 @@ public class InquiryTmsQueryService {
}
// 陆运线路段:与空运 airOptions 同级透出,供模板 routeRegions 填项目一
putLandSegments(quote, data, tms);
// 陆运常无整单总价:一条线路才把各段成本加总;多条备选禁止加在一起
applyLandSegmentTotals(quote);
String chargeable = firstText(data.get("chargeableWeightKg"), data.get("chargeable_weight_kg"));
if (StringUtils.isNotBlank(chargeable)) {
quote.put("chargeableWeightKg", chargeable);
@@ -385,9 +383,9 @@ public class InquiryTmsQueryService {
if (!rows.isEmpty()) {
quote.put("route_rows", JSON.toJSONString(rows));
}
// feeItems 为空时,用各腿售价补一条运输费,避免报价明细全空
// feeItems 为空时,用各腿成本补运输费。顶层总价会被误收成「空运费」,陆运要换掉。
String feeLines = quote.get("fee_lines");
if ((StringUtils.isBlank(feeLines) || "[]".equals(feeLines.trim())) && !rows.isEmpty()) {
if (shouldFillLandFeeLines(feeLines) && !rows.isEmpty()) {
List<Map<String, String>> lines = new ArrayList<>();
for (Map<String, String> seg : rows) {
String amount = firstText(seg.get("salePrice"), seg.get("costPrice"), seg.get("amount"));
@@ -395,11 +393,17 @@ public class InquiryTmsQueryService {
continue;
}
Map<String, String> line = new LinkedHashMap<>();
line.put("name", "运输费");
line.put("name", firstText(seg.get("feeName"), seg.get("name"), "运输费"));
line.put("amount", amount);
if (StringUtils.isNotBlank(seg.get("currency"))) {
line.put("currency", seg.get("currency"));
}
if (StringUtils.isNotBlank(seg.get("segmentSequence"))) {
line.put("segmentSequence", seg.get("segmentSequence"));
}
if (StringUtils.isNotBlank(seg.get("routeLegSequence"))) {
line.put("routeLegSequence", seg.get("routeLegSequence"));
}
if (StringUtils.isNotBlank(seg.get("remark"))) {
line.put("remark", seg.get("remark"));
}
@@ -477,6 +481,201 @@ public class InquiryTmsQueryService {
return out;
}
/**
* 陆运分段成本收成工单总价。
* <p>
* 同一 segmentSequence 是一条完整线路。只有 1 条时才把各段 costPrice 相加,
* 时效按段原文用「 / 」拼上(如 N+2 / N+3)。多条备选禁止加总,避免 37100 这种假合计。
* 顶层已有真实总价时不覆盖。
*/
static void applyLandSegmentTotals(Map<String, String> quote) {
if (quote == null) {
return;
}
List<Map<String, String>> rows = normalizeLandSegmentRows(quote.get("landSegments"));
if (rows.isEmpty()) {
return;
}
Map<Integer, List<Map<String, String>>> groups = new LinkedHashMap<>();
for (Map<String, String> row : rows) {
int seq = 1;
try {
seq = Integer.parseInt(firstText(row.get("segmentSequence"), "1"));
} catch (RuntimeException ex) {
seq = 1;
}
groups.computeIfAbsent(seq, k -> new ArrayList<>()).add(row);
}
if (groups.size() != 1) {
return;
}
List<Map<String, String>> legs = groups.values().iterator().next();
java.math.BigDecimal sum = java.math.BigDecimal.ZERO;
boolean any = false;
List<String> etas = new ArrayList<>();
String ccy = firstText(quote.get("currency"));
for (Map<String, String> leg : legs) {
String amt = firstText(leg.get("costPrice"), leg.get("salePrice"), leg.get("amount"));
if (StringUtils.isNotBlank(amt) && !"-".equals(amt)) {
try {
sum = sum.add(new java.math.BigDecimal(amt.trim().replace(",", "")));
any = true;
} catch (RuntimeException ignored) {
// 非数字成本不编
}
}
if (StringUtils.isBlank(ccy)) {
ccy = firstText(leg.get("currency"));
}
String days = firstText(leg.get("transitDays"), leg.get("transit_time"));
if (StringUtils.isNotBlank(days) && !"-".equals(days)) {
etas.add(days);
}
}
if (any && blankOrDash(quote.get("total"))) {
quote.put("total", stripPlainNumber(sum));
}
if (any && blankOrDash(quote.get("cost"))) {
quote.put("cost", stripPlainNumber(sum));
}
if (StringUtils.isNotBlank(ccy) && StringUtils.isBlank(quote.get("currency"))) {
quote.put("currency", ccy);
}
if (!etas.isEmpty() && blankOrDash(quote.get("eta"))) {
quote.put("eta", String.join(" / ", etas));
}
}
/**
* 把规范化 quote 写进 TMS 报价行:金额带币种,载荷原样 JSON。
* 查价落库与选定线路后覆盖共用,禁止再插一条假 TMS。
*/
static void applySnapshotToQuoteRow(InquiryQuote row, Map<String, String> quote) {
if (row == null || quote == null) {
return;
}
row.setSource("TMS");
row.setSourceLabel("TMS 标准报价");
row.setPriceText(TmsQuoteCurrency.formatDisplay(quote.get("currency"), quote.get("total")));
row.setHighlighted(1);
row.setPayloadJson(JSON.toJSONString(quote));
}
/**
* 选定陆运线路后覆盖本单最新 TMS 快照,并重算工单成本价。
* <p>
* 为何:查价当时可能是多条备选,工单总价只能先空着;销售选定后必须把该条成本写回。
* 不新插 TMS 行,避免后台出现两条「TMS标准报价」。已冻结工单拒绝。
*/
public Map<String, Object> replaceTmsQuote(String workOrderNo, Map<String, Object> incoming) {
Map<String, Object> out = new LinkedHashMap<>();
if (StringUtils.isBlank(workOrderNo) || incoming == null || incoming.isEmpty()) {
out.put("ok", false);
out.put("error", "quote_blank");
return out;
}
InquiryTicket ticket = workOrderService.getOne(
new LambdaQueryWrapper<InquiryTicket>()
.eq(InquiryTicket::getWorkOrderNo, workOrderNo)
.last("LIMIT 1"));
if (ticket == null) {
out.put("ok", false);
out.put("error", "ticket_not_found");
return out;
}
if (TicketStatuses.isFrozen(ticket.getStatus())) {
out.put("ok", false);
out.put("error", "frozen");
out.put("status", ticket.getStatus());
return out;
}
Map<String, String> quote = stringifyQuote(incoming);
applyLandSegmentTotals(quote);
InquiryQuote row = quoteMapper.selectOne(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
.eq(InquiryQuote::getSource, "TMS")
.orderByDesc(InquiryQuote::getCreateTime)
.last("LIMIT 1"));
boolean insert = row == null;
if (insert) {
row = new InquiryQuote();
row.setTicketId(ticket.getId());
row.setWorkOrderNo(workOrderNo);
row.setQuoteVersion(1);
row.setCreateTime(new Date());
}
applySnapshotToQuoteRow(row, quote);
row.setUpdateTime(new Date());
if (insert) {
quoteMapper.insert(row);
} else {
quoteMapper.updateById(row);
}
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
.orderByAsc(InquiryQuote::getCreateTime));
InquiryTicketPriceSupport.apply(ticket, quotes);
ticket.setTmsHit(1);
workOrderService.updateById(ticket);
if (TicketStatuses.canTransit(ticket.getStatus(), TicketStatuses.QUOTED)) {
TicketTransitionRequest tr = new TicketTransitionRequest();
tr.setWorkOrderNo(workOrderNo);
tr.setToStatus(TicketStatuses.QUOTED);
workOrderService.transition(tr, "agent");
}
out.put("ok", true);
out.put("quote_version", row.getQuoteVersion() == null ? 1 : row.getQuoteVersion());
out.put("status", ticket.getStatus());
return out;
}
/**
* 智能体回写的 quote 可能带 list/map,落库前收成字符串。
*/
static Map<String, String> stringifyQuote(Map<String, Object> incoming) {
Map<String, String> quote = new LinkedHashMap<>();
if (incoming == null) {
return quote;
}
for (Map.Entry<String, Object> e : incoming.entrySet()) {
if (e.getKey() == null || e.getValue() == null) {
continue;
}
Object v = e.getValue();
if (v instanceof String) {
quote.put(e.getKey(), (String) v);
} else if (v instanceof Map || v instanceof List) {
quote.put(e.getKey(), JSON.toJSONString(v));
} else {
quote.put(e.getKey(), String.valueOf(v));
}
}
return quote;
}
private static boolean blankOrDash(String raw) {
return StringUtils.isBlank(raw) || "-".equals(raw.trim());
}
/**
* 陆运分段行能不能盖掉当前 fee_lines。
* 空列表、或只有「空运费」兜底(把陆运总价误写成空运)时才盖。
*/
static boolean shouldFillLandFeeLines(String feeLines) {
if (StringUtils.isBlank(feeLines) || "[]".equals(feeLines.trim())) {
return true;
}
String text = feeLines.replace(" ", "");
return text.contains("空运费") && !text.contains("运输费");
}
private static String stripPlainNumber(java.math.BigDecimal value) {
if (value == null) {
return "";
}
return value.stripTrailingZeros().toPlainString();
}
/**
* 把本次 TMS 报价写回工单主表,供后台列表/详情展示。
* 海运/陆运进成本价,空运进销售价;利润点必须等销售调价后才算。
@@ -762,9 +961,29 @@ public class InquiryTmsQueryService {
putExtra(extras, "quantity", firstText(row.get("quantity"), row.get("chargeQuantity"), row.get("charge_quantity")));
putExtra(extras, "unit_price", firstText(row.get("unitPrice"), row.get("unit_price")));
putExtra(extras, "remark", firstText(row.get("remark"), row.get("description"), row.get("note")));
putExtra(extras, "segmentSequence", firstText(row.get("segmentSequence"), row.get("segment_sequence")));
putExtra(extras, "routeLegSequence", firstText(row.get("routeLegSequence"), row.get("route_leg_sequence")));
return extras;
}
/**
* 陆运选线要按段过滤费用行:feeItems 原文单独落下,名称保持 TMS 回的 feeName。
*/
private static void putFeeItemsRaw(Map<String, String> quote, Map<String, Object> data) {
if (quote == null || data == null) {
return;
}
Object raw = first(data.get("feeItems"), data.get("fee_items"), data.get("feeList"));
if (raw == null) {
return;
}
String json = raw instanceof String ? String.valueOf(raw) : JSON.toJSONString(raw);
if (StringUtils.isBlank(json) || "null".equalsIgnoreCase(json) || "[]".equals(json.trim())) {
return;
}
quote.put("feeItems", json);
}
private static void putExtra(Map<String, String> extras, String key, String value) {
if (StringUtils.isNotBlank(value) && !"-".equals(value) && !"null".equalsIgnoreCase(value)) {
extras.put(key, value);
@@ -87,6 +87,31 @@ public class QuoteTemplateFillTest {
assertEquals("CNY", ((JSONArray) fee).getJSONObject(0).getString("currency"));
}
/**
* 空运没有时效:即便 TMS 回了 eta,出单也不填 transit_* / 时效。
* 报价有效期仍要留下。
*/
@Test
public void buildFillFactsHidesAirTransitKeepsValidUntil() {
Map<String, Object> facts = new HashMap<>();
facts.put("起运港", "南京");
facts.put("目的港", "吉隆坡");
facts.put("运输方式", "空运");
facts.put("时效", "3-5 天");
Map<String, Object> quote = new HashMap<>();
quote.put("total", "5001.0");
quote.put("currency", "CNY");
quote.put("eta", "3天");
quote.put("valid_until", "2026-09-30");
quote.put("airOptions", "[{\"originAirportCode\":\"NKG\"}]");
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
.buildFillFacts("WO202609220018", facts, quote, "WuJiLin", "AIR");
assertEquals("2026-09-30", out.getString("valid_until"));
assertTrue(StringUtils.isBlank(out.getString("transit_days")));
assertTrue(StringUtils.isBlank(out.getString("transit_time")));
assertTrue(StringUtils.isBlank(out.getString("时效")));
}
@Test
public void writeFeeDetailColumnsOntoNamedFeeRow() throws Exception {
Path src = Files.createTempFile("quote-fee-src", ".xlsx");
@@ -331,4 +331,121 @@ public class TmsQuoteCurrencyTest {
assertEquals("11.00", lines.get(0).get("unit_price"));
assertEquals("123.00", lines.get(1).get("amount"));
}
@Test
public void toAgentQuoteSumsSingleLandRouteCostWhenNoHeaderTotal() {
// 陆运只有分段成本、没有整单总价:一条两段要加出 17100,时效带上各段原文。
Map<String, Object> data = new HashMap<>();
data.put("quoteFound", true);
data.put("currency", "CNY");
data.put("isMulti", 1);
data.put("landSegments", java.util.Arrays.asList(
landLeg(1, 1, "上海", "谅山", 7500, "N+2"),
landLeg(1, 2, "谅山", "胡志明", 9600, "N+3")));
Map<String, Object> tms = new HashMap<>();
tms.put("code", 1);
tms.put("tmsData", data);
tms.put("quoteFound", true);
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
assertEquals("17100", quote.get("total"));
assertEquals("17100", quote.get("cost"));
assertTrue(quote.get("eta").contains("N+2"));
assertTrue(quote.get("eta").contains("N+3"));
assertFalse(quote.get("eta").contains("120"));
}
@Test
public void toAgentQuoteKeepsLandFeeItemNameAndSegment() {
// 陆运 feeItems 的费用名原样落下,并带上段号,选线后才能按条过滤。
Map<String, Object> data = new HashMap<>();
data.put("quoteFound", true);
data.put("currency", "CNY");
data.put("isMulti", 2);
data.put("landSegments", java.util.Arrays.asList(
landLeg(1, 1, "上海", "谅山", 7500, "N+2"),
landLeg(1, 2, "谅山", "胡志明", 9600, "N+3"),
landLeg(2, 1, "上海", "广西", 7500, "N+3"),
landLeg(2, 2, "广西", "胡志明", 12500, "N+3")));
data.put("feeItems", java.util.Arrays.asList(
landFee(1, 1, "运输费", 7500),
landFee(1, 2, "运输费", 9600),
landFee(2, 1, "运输费", 7500),
landFee(2, 2, "运输费", 12500)));
Map<String, Object> tms = new HashMap<>();
tms.put("code", 1);
tms.put("tmsData", data);
tms.put("quoteFound", true);
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
String fees = quote.get("fee_lines");
assertTrue(fees.contains("\"name\":\"运输费\""));
assertTrue(fees.contains("\"segmentSequence\":\"1\""));
assertTrue(fees.contains("\"routeLegSequence\":\"2\""));
assertFalse(fees.contains("上海 →"));
assertTrue(quote.get("feeItems").contains("运输费"));
}
@Test
public void toAgentQuoteDoesNotSumTwoLandRoutes() {
// 两条备选不能加在一起写成 37100;未选线时整单总价保持空。
Map<String, Object> data = new HashMap<>();
data.put("quoteFound", true);
data.put("currency", "CNY");
data.put("isMulti", 2);
data.put("landSegments", java.util.Arrays.asList(
landLeg(1, 1, "上海", "谅山", 7500, "N+2"),
landLeg(1, 2, "谅山", "胡志明", 9600, "N+3"),
landLeg(2, 1, "上海", "广西", 7500, "N+3"),
landLeg(2, 2, "广西", "胡志明", 12500, "N+3")));
Map<String, Object> tms = new HashMap<>();
tms.put("code", 1);
tms.put("tmsData", data);
tms.put("quoteFound", true);
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
assertTrue(StringUtilsBlankOrDash(quote.get("total")));
assertFalse("37100".equals(quote.get("total")));
assertTrue(quote.get("landSegments").contains("谅山"));
assertTrue(quote.get("landSegments").contains("广西"));
}
@Test
public void applySnapshotWritesLandCostAsTmsPriceText() {
Map<String, String> quote = new LinkedHashMap<>();
quote.put("currency", "CNY");
quote.put("total", "17100");
org.jeecg.modules.inquiry.entity.InquiryQuote row =
new org.jeecg.modules.inquiry.entity.InquiryQuote();
InquiryTmsQueryService.applySnapshotToQuoteRow(row, quote);
assertEquals("CNY 17100", row.getPriceText());
assertTrue(row.getPayloadJson().contains("17100"));
}
private static boolean StringUtilsBlankOrDash(String raw) {
return raw == null || raw.trim().isEmpty() || "-".equals(raw.trim());
}
private static Map<String, Object> landLeg(int seq, int leg, String origin, String dest,
int cost, String transit) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("segmentSequence", seq);
row.put("routeLegSequence", leg);
row.put("origin", origin);
row.put("destination", dest);
row.put("costPrice", cost);
row.put("currency", "CNY");
row.put("transitDays", transit);
row.put("vehicleType", "3T");
return row;
}
private static Map<String, Object> landFee(int seq, int leg, String name, int amount) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("segmentSequence", seq);
row.put("routeLegSequence", leg);
row.put("feeName", name);
row.put("amount", amount);
row.put("currency", "CNY");
row.put("unit", "台");
row.put("quantity", 1);
return row;
}
}