落地空运单聊询价字段与线路选择。
必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+20
@@ -399,6 +399,26 @@ public class InquiryAgentController {
|
||||
return Result.OK(workOrderService.patchCollabFacts(body));
|
||||
}
|
||||
|
||||
@ApiOperation("选定线路后写回询价事实(不改六态)")
|
||||
@PostMapping("/ticket/updateFacts")
|
||||
public Result<Map<String, Object>> updateFacts(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.updateInquiryFacts(body));
|
||||
}
|
||||
|
||||
@ApiOperation("采用后改选:作废旧报价单(不改六态)")
|
||||
@PostMapping("/ticket/voidQuoteFiles")
|
||||
public Result<Map<String, Object>> voidQuoteFiles(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.voidQuoteFiles(firstStr(body, "workOrderNo", "work_order_no")));
|
||||
}
|
||||
|
||||
@ApiOperation("按协同群 chat_id 找回工单(只读)")
|
||||
@PostMapping("/ticket/byCollabChat")
|
||||
public Result<Map<String, Object>> byCollabChat(@RequestBody Map<String, Object> body,
|
||||
|
||||
+10
@@ -91,6 +91,16 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
|
||||
*/
|
||||
void markQuoteFileIssued(String workOrderNo);
|
||||
|
||||
/**
|
||||
* 选定线路后写回询价事实(起运港等)。不改六态、不写报价版本。
|
||||
*/
|
||||
Map<String, Object> updateInquiryFacts(Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 采用后改选:作废旧报价单。不改六态。续办回到待采用。
|
||||
*/
|
||||
Map<String, Object> voidQuoteFiles(String workOrderNo);
|
||||
|
||||
/**
|
||||
* 记下海运协同群。不改六态。
|
||||
*/
|
||||
|
||||
+78
-7
@@ -27,7 +27,9 @@ import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
|
||||
import org.jeecg.modules.inquiry.support.AgentSystemExceptionRetryClient;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
|
||||
import org.jeecg.modules.inquiry.support.InquiryQuoteDisplaySupport;
|
||||
import org.jeecg.modules.inquiry.support.InquiryQuoteFileSupport;
|
||||
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
|
||||
import org.jeecg.modules.inquiry.support.InquiryTicketFactsSupport;
|
||||
import org.jeecg.modules.inquiry.support.InquiryTicketPriceSupport;
|
||||
import org.jeecg.modules.inquiry.tms.InquiryTmsQueryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -858,23 +860,92 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
if (payload == null) {
|
||||
payload = new JSONObject(true);
|
||||
}
|
||||
payload.put("quoteFileIssued", true);
|
||||
row.setPayloadJson(payload.toJSONString());
|
||||
row.setPayloadJson(InquiryQuoteFileSupport.markIssued(payload.toJSONString()));
|
||||
row.setUpdateTime(new Date());
|
||||
quoteMapper.updateById(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定线路后写回询价事实。不改六态。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> updateInquiryFacts(Map<String, Object> body) {
|
||||
String no = text(body, "workOrderNo", "work_order_no");
|
||||
InquiryTicket ticket = findByWorkOrderNo(no);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
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", "handoff_frozen");
|
||||
return out;
|
||||
}
|
||||
Map<String, String> facts = new LinkedHashMap<>();
|
||||
Object raw = body == null ? null : body.get("facts");
|
||||
if (raw instanceof Map) {
|
||||
for (Map.Entry<?, ?> e : ((Map<?, ?>) raw).entrySet()) {
|
||||
if (e.getKey() == null || e.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
facts.put(String.valueOf(e.getKey()), String.valueOf(e.getValue()));
|
||||
}
|
||||
}
|
||||
InquiryTicketFactsSupport.apply(ticket, facts);
|
||||
ticket.setUpdateTime(new Date());
|
||||
updateById(ticket);
|
||||
out.put("ok", true);
|
||||
out.put("work_order_no", no);
|
||||
out.put("facts", InquiryTicketFactsSupport.parseFacts(ticket.getFactsJson()));
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 采用后改选:作废旧报价单。不改六态。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> voidQuoteFiles(String workOrderNo) {
|
||||
String no = StringUtils.trimToEmpty(workOrderNo);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
if (StringUtils.isBlank(no)) {
|
||||
out.put("ok", false);
|
||||
out.put("error", "work_order_required");
|
||||
return out;
|
||||
}
|
||||
InquiryTicket ticket = findByWorkOrderNo(no);
|
||||
if (ticket == null) {
|
||||
out.put("ok", false);
|
||||
out.put("error", "ticket_not_found");
|
||||
return out;
|
||||
}
|
||||
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, no)
|
||||
.orderByDesc(InquiryQuote::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
if (quotes != null && !quotes.isEmpty()) {
|
||||
InquiryQuote row = quotes.get(0);
|
||||
row.setPayloadJson(InquiryQuoteFileSupport.applyVoid(row.getPayloadJson()));
|
||||
row.setUpdateTime(new Date());
|
||||
quoteMapper.updateById(row);
|
||||
}
|
||||
out.put("ok", true);
|
||||
out.put("work_order_no", no);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 续办停点:文件已发出或已有附件,就不再回到出方案/协同。
|
||||
* 作废优先:改选后即使还有旧附件也不进成交跟进。
|
||||
*/
|
||||
private boolean quoteFileAlreadyIssued(String workOrderNo, Map<String, Object> quote) {
|
||||
Object flag = quote == null ? null : quote.get("quoteFileIssued");
|
||||
if (Boolean.TRUE.equals(flag) || "true".equalsIgnoreCase(String.valueOf(flag))) {
|
||||
return true;
|
||||
}
|
||||
String payload = quote == null ? "" : JSON.toJSONString(quote);
|
||||
Long n = attachmentMapper.selectCount(new LambdaQueryWrapper<InquiryAttachment>()
|
||||
.eq(InquiryAttachment::getWorkOrderNo, workOrderNo));
|
||||
return n != null && n > 0;
|
||||
return InquiryQuoteFileSupport.isIssued(payload, n != null && n > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 报价单发出 / 作废标记。
|
||||
* <p>
|
||||
* 职责:只改报价载荷里的 quoteFileIssued / quoteFileVoided,不改六态。
|
||||
* 副作用:无。调用方写回 inq_quote.payload_json。
|
||||
* 为何:采用后改选必须作废旧单,续办不能再走进成交跟进。
|
||||
*/
|
||||
public final class InquiryQuoteFileSupport {
|
||||
|
||||
public static final String ISSUED = "quoteFileIssued";
|
||||
public static final String VOIDED = "quoteFileVoided";
|
||||
|
||||
private InquiryQuoteFileSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废:清发出标记,写下作废。有附件也不能再当有效报价单。
|
||||
*/
|
||||
public static String applyVoid(String payloadJson) {
|
||||
JSONObject payload = parse(payloadJson);
|
||||
payload.put(ISSUED, false);
|
||||
payload.put(VOIDED, true);
|
||||
return payload.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新采用:打发出标记,去掉作废。
|
||||
*/
|
||||
public static String markIssued(String payloadJson) {
|
||||
JSONObject payload = parse(payloadJson);
|
||||
payload.put(ISSUED, true);
|
||||
payload.remove(VOIDED);
|
||||
return payload.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 续办是否按「文件已发出」进成交跟进。
|
||||
* 作废优先:即使库里还有附件也不算有效报价单。
|
||||
*/
|
||||
public static boolean isIssued(String payloadJson, boolean hasAttachments) {
|
||||
JSONObject payload = parse(payloadJson);
|
||||
if (Boolean.TRUE.equals(payload.getBoolean(VOIDED))
|
||||
|| "true".equalsIgnoreCase(String.valueOf(payload.get(VOIDED)))) {
|
||||
return false;
|
||||
}
|
||||
Object flag = payload.get(ISSUED);
|
||||
if (Boolean.TRUE.equals(flag) || "true".equalsIgnoreCase(String.valueOf(flag))) {
|
||||
return true;
|
||||
}
|
||||
return hasAttachments;
|
||||
}
|
||||
|
||||
private static JSONObject parse(String payloadJson) {
|
||||
if (StringUtils.isBlank(payloadJson)) {
|
||||
return new JSONObject(true);
|
||||
}
|
||||
JSONObject parsed = JSON.parseObject(payloadJson);
|
||||
return parsed == null ? new JSONObject(true) : parsed;
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工单询价事实写回:选定线路后覆盖起运港等列。
|
||||
* <p>
|
||||
* 职责:合并 factsJson 与列字段。不改六态、不写报价版本。
|
||||
* 副作用:无。调用方负责 updateById。
|
||||
* 为何:H5 选线路后工单起运港必须变成所选,改选覆盖。
|
||||
*/
|
||||
public final class InquiryTicketFactsSupport {
|
||||
|
||||
private InquiryTicketFactsSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用销售侧内部中文键覆盖工单。空值不删已有键(改选只覆盖有值项)。
|
||||
*
|
||||
* @param ticket 当前工单
|
||||
* @param facts 完整或增量 facts
|
||||
*/
|
||||
public static void apply(InquiryTicket ticket, Map<String, String> facts) {
|
||||
if (ticket == null || facts == null || facts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> merged = parseFacts(ticket.getFactsJson());
|
||||
for (Map.Entry<String, String> e : facts.entrySet()) {
|
||||
if (e.getKey() == null || e.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
String val = e.getValue().trim();
|
||||
if (val.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
merged.put(e.getKey(), val);
|
||||
}
|
||||
ticket.setFactsJson(JSON.toJSONString(merged));
|
||||
if (StringUtils.isNotBlank(merged.get("起运港"))) {
|
||||
ticket.setOrigin(merged.get("起运港"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(merged.get("目的港"))) {
|
||||
ticket.setDestination(merged.get("目的港"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(merged.get("品名"))) {
|
||||
ticket.setGoodsName(merged.get("品名"));
|
||||
}
|
||||
Integer pieces = parseInt(merged.get("件数"));
|
||||
if (pieces != null) {
|
||||
ticket.setPieces(pieces);
|
||||
}
|
||||
BigDecimal weight = parseDecimal(first(merged, "重量", "毛重"));
|
||||
if (weight != null) {
|
||||
ticket.setWeightKg(weight);
|
||||
}
|
||||
BigDecimal volume = parseDecimal(merged.get("体积"));
|
||||
if (volume != null) {
|
||||
ticket.setVolumeCbm(volume);
|
||||
}
|
||||
Date quoteDate = parseDate(merged.get("报价日期"));
|
||||
if (quoteDate != null) {
|
||||
ticket.setQuoteDate(quoteDate);
|
||||
}
|
||||
String origin = StringUtils.defaultString(ticket.getOrigin());
|
||||
String dest = StringUtils.defaultString(ticket.getDestination());
|
||||
if (StringUtils.isNotBlank(origin) || StringUtils.isNotBlank(dest)) {
|
||||
ticket.setDemandTitle(origin + " → " + dest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解开 factsJson。坏 JSON 当空。
|
||||
*/
|
||||
public static Map<String, String> parseFacts(String factsJson) {
|
||||
Map<String, String> out = new LinkedHashMap<>();
|
||||
if (StringUtils.isBlank(factsJson)) {
|
||||
return out;
|
||||
}
|
||||
try {
|
||||
JSONObject parsed = JSON.parseObject(factsJson);
|
||||
if (parsed == null) {
|
||||
return out;
|
||||
}
|
||||
for (String key : parsed.keySet()) {
|
||||
if (parsed.get(key) != null) {
|
||||
out.put(key, String.valueOf(parsed.get(key)));
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String first(Map<String, String> facts, String... keys) {
|
||||
if (facts == null || keys == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
String val = facts.get(key);
|
||||
if (StringUtils.isNotBlank(val)) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Integer parseInt(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String digits = raw.replaceAll("[^0-9-]", "");
|
||||
if (digits.isEmpty() || "-".equals(digits)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(digits);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal parseDecimal(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String norm = raw.replaceAll("[^0-9.\\-]", "");
|
||||
if (norm.isEmpty() || ".".equals(norm) || "-".equals(norm)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(norm);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Date parseDate(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new SimpleDateFormat("yyyy-MM-dd").parse(raw.trim());
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
-25
@@ -26,8 +26,10 @@ import java.util.Map;
|
||||
* 空运锁舱/释放:智能体只打主账,本类 HMAC 出站 TMS。
|
||||
* <p>
|
||||
* 锁舱按 /air/cabin/lock:用工单起运地/目的地/报价日期/重量/体积拼正文。
|
||||
* 锁舱必须带 TMS 查价返回的 quoteId / optionNo。库里只有金额、没有编号时当场再查一次。
|
||||
* 仍没有编号才补 QT-{工单号} + AIR-OPT-01 出站;不因「没 TMS 报价」本地拦住。
|
||||
* 锁舱优先带 TMS 查价返回的 quoteId / optionNo。库里只有金额、没有编号时当场再查一次。
|
||||
* 没有 5.2 报价也可以锁舱:TMS 锁舱这两项必填,缺了会 1001「参数格式错误」。
|
||||
* 此时补 QT-{工单号} + AIR-OPT-01,禁止拿空字段出站。
|
||||
* 有真实 TMS 编号时不准用自编号盖掉,否则会回 2006。
|
||||
* 已成交再释放:本地拦住。测服打开 TMS_V2_LOCK_ENABLED;现网验收期保持关闭。
|
||||
* 禁止:按 userid 伪造报价;单测打真实 TMS。
|
||||
*/
|
||||
@@ -67,18 +69,20 @@ public class InquiryTmsCabinService {
|
||||
}
|
||||
Map<String, Object> quote = latestQuote(no);
|
||||
quote = refreshLockQuote(ticket, quote);
|
||||
String quoteId = resolveQuoteId(quote, no);
|
||||
String quoteId = lockQuoteId(resolveQuoteId(quote, no), no);
|
||||
Map<String, Object> option = resolveOption(quote, optionNo);
|
||||
if (StringUtils.isBlank(quoteId) || option == null) {
|
||||
return fail("no_lock_target", "锁舱缺少报价编号或舱位选项");
|
||||
if (option == null) {
|
||||
option = new LinkedHashMap<>();
|
||||
}
|
||||
String resolvedOption = lockOptionNo(text(option, "optionNo", "option_no"));
|
||||
option.put("optionNo", resolvedOption);
|
||||
if (!properties.lockConfigured()) {
|
||||
return fail("lock_disabled", "主账未开放锁舱");
|
||||
}
|
||||
Map<String, Object> req = new LinkedHashMap<>();
|
||||
req.put("workOrderNo", no);
|
||||
req.put("quoteId", quoteId);
|
||||
req.put("optionNo", text(option, "optionNo", "option_no"));
|
||||
req.put("optionNo", resolvedOption);
|
||||
req.put("flightDate", quoteDate(ticket));
|
||||
req.put("origin", firstNonBlank(text(option, "originAirportCode"), ticket.getOrigin(),
|
||||
fact(ticket, "起运地", "起运港")));
|
||||
@@ -97,7 +101,7 @@ public class InquiryTmsCabinService {
|
||||
req.put("operatorUserId", text(body, "operatorUserId", "sender_id"));
|
||||
req.put("operatorName", text(body, "operatorName", "operator_name"));
|
||||
req.put("instructionText", text(body, "instructionText", "instruction_text"));
|
||||
return invokeLock(ticket, req, quoteId, text(option, "optionNo", "option_no"));
|
||||
return invokeLock(ticket, req, quoteId, resolvedOption);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +144,10 @@ public class InquiryTmsCabinService {
|
||||
Map<String, Object> tms = gateway.lockCabin(req);
|
||||
int code = TmsV2ResponseNormalizer.number(tms.get("code"));
|
||||
if (code != 1) {
|
||||
return fail("lock_fail", cabinFailReason(code, tms));
|
||||
String why = cabinFailReason(code, tms, quoteDate(ticket));
|
||||
log.info("tms.lock fail wo={} quoteId={} optionNo={} code={} reason={}",
|
||||
ticket.getWorkOrderNo(), quoteId, optionNo, code, why);
|
||||
return fail("lock_fail", why);
|
||||
}
|
||||
Map<String, Object> data = tmsData(tms);
|
||||
String lockId = firstNonBlank(text(data, "lockId"), text(tms, "lockId"));
|
||||
@@ -169,7 +176,9 @@ public class InquiryTmsCabinService {
|
||||
Map<String, Object> tms = gateway.releaseCabin(req);
|
||||
int code = TmsV2ResponseNormalizer.number(tms.get("code"));
|
||||
if (code != 1) {
|
||||
return fail("release_fail", cabinFailReason(code, tms));
|
||||
String why = cabinFailReason(code, tms, quoteDate(ticket));
|
||||
log.info("tms.release fail wo={} code={} reason={}", ticket.getWorkOrderNo(), code, why);
|
||||
return fail("release_fail", why);
|
||||
}
|
||||
Map<String, Object> data = tmsData(tms);
|
||||
Map<String, Object> out = okBase();
|
||||
@@ -301,18 +310,49 @@ public class InquiryTmsCabinService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 没有 TMS 5.2 报价编号时,用工单号生成关联 ID,仍调锁舱。
|
||||
* 库里的真实 TMS 报价编号。自编 QT-工单号不算:先再查一次 5.2,
|
||||
* 仍没有才在 {@link #lockQuoteId} 补占位,给无报价锁舱用。
|
||||
*/
|
||||
static String resolveQuoteId(Map<String, Object> quote, String workOrderNo) {
|
||||
String id = text(quote, "quoteId", "quote_id");
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
return id;
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return "";
|
||||
}
|
||||
return StringUtils.isBlank(workOrderNo) ? "" : "QT-" + workOrderNo.trim();
|
||||
String wo = StringUtils.trimToEmpty(workOrderNo);
|
||||
if (id.equalsIgnoreCase("QT-" + wo) || id.toUpperCase().startsWith("QT-WO")) {
|
||||
return "";
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有舱位选项按名称挑;一张都没有时补 AIR-OPT-01,让人工价也能出站。
|
||||
* 锁舱出站用的报价编号。TMS /air/cabin/lock 的 quoteId 必填,缺了回 1001。
|
||||
* 有 5.2 真编号就用真编号;没有才补 QT-{工单号},允许无报价锁舱。
|
||||
*/
|
||||
static String lockQuoteId(String resolved, String workOrderNo) {
|
||||
if (StringUtils.isNotBlank(resolved)) {
|
||||
return resolved.trim();
|
||||
}
|
||||
String wo = StringUtils.trimToEmpty(workOrderNo);
|
||||
if (wo.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "QT-" + wo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁舱出站用的舱位选项。TMS optionNo 必填,缺了回 1001。
|
||||
* 有选项用选项;没有补 AIR-OPT-01。
|
||||
*/
|
||||
static String lockOptionNo(String resolved) {
|
||||
if (StringUtils.isNotBlank(resolved)) {
|
||||
return resolved.trim();
|
||||
}
|
||||
return "AIR-OPT-01";
|
||||
}
|
||||
|
||||
/**
|
||||
* 有舱位选项按名称挑;没有就补 AIR-OPT-01,无报价锁舱也要带上必填项。
|
||||
*/
|
||||
static Map<String, Object> resolveOption(Map<String, Object> quote, String optionNo) {
|
||||
List<Map<String, Object>> options = airOptions(quote);
|
||||
@@ -324,10 +364,10 @@ public class InquiryTmsCabinService {
|
||||
if (options.size() == 1) {
|
||||
return options.get(0);
|
||||
}
|
||||
return null;
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("optionNo", StringUtils.isNotBlank(optionNo) ? optionNo.trim() : "AIR-OPT-01");
|
||||
row.put("optionNo", lockOptionNo(optionNo));
|
||||
String airline = text(quote, "airlineCode", "airline", "airlineName");
|
||||
if (StringUtils.isNotBlank(airline)) {
|
||||
row.put("airlineCode", airline);
|
||||
@@ -417,25 +457,51 @@ public class InquiryTmsCabinService {
|
||||
return tms == null ? new LinkedHashMap<>() : tms;
|
||||
}
|
||||
|
||||
private static String cabinFailReason(int code, Map<String, Object> tms) {
|
||||
/**
|
||||
* 锁舱/释放失败话术:原样用 TMS 的 failReason。
|
||||
* 禁止按航班日改写成「起飞」,禁止把 2006 映射成「报价选项不存在」——没有报价也可以锁舱。
|
||||
*/
|
||||
static String cabinFailReason(int code, Map<String, Object> tms, String flightDate) {
|
||||
return cabinFailReason(code, tms, flightDate, "");
|
||||
}
|
||||
|
||||
static String cabinFailReason(int code, Map<String, Object> tms, String flightDate, String today) {
|
||||
// flightDate / today 保留调用签名,不再按日期改写 TMS 原文。
|
||||
Map<String, Object> data = tmsData(tms);
|
||||
String detail = firstNonBlank(text(data, "failReason"), text(tms, "message"));
|
||||
if (code == 2006) {
|
||||
return firstNonBlank(detail, "舱位选项不存在或与报价不匹配");
|
||||
String detail = firstNonBlank(
|
||||
text(data, "failReason", "fail_reason"),
|
||||
text(tms, "failReason", "fail_reason"),
|
||||
nestedFailReason(data),
|
||||
text(data, "message"),
|
||||
text(tms, "message"));
|
||||
if (StringUtils.isNotBlank(detail)) {
|
||||
return detail;
|
||||
}
|
||||
if (code == 2007) {
|
||||
return firstNonBlank(detail, "报价已过期,请重新询价");
|
||||
return "报价已过期,请重新询价";
|
||||
}
|
||||
if (code == 2008) {
|
||||
return firstNonBlank(detail, "舱位不存在");
|
||||
return "舱位不存在";
|
||||
}
|
||||
if (code == 2009) {
|
||||
return firstNonBlank(detail, "锁舱被业务规则拒绝");
|
||||
return "锁舱被业务规则拒绝";
|
||||
}
|
||||
if (code == 1005) {
|
||||
return firstNonBlank(detail, "TMS 签名失败");
|
||||
return "TMS 签名失败";
|
||||
}
|
||||
return firstNonBlank(detail, "TMS 业务码=" + code);
|
||||
return "TMS 业务码=" + code;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String nestedFailReason(Map<String, Object> data) {
|
||||
if (data == null) {
|
||||
return "";
|
||||
}
|
||||
Object inner = data.get("data");
|
||||
if (inner instanceof Map) {
|
||||
return text((Map<String, Object>) inner, "failReason", "fail_reason");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static Map<String, Object> okBase() {
|
||||
|
||||
+37
-27
@@ -81,11 +81,11 @@ public class InquiryTmsQueryService {
|
||||
segment.put("transportMode", line);
|
||||
String origin = text(facts.get("起运港"));
|
||||
String destination = text(facts.get("目的港"));
|
||||
segment.put("origin", origin);
|
||||
segment.put("destination", destination);
|
||||
segment.put("cargoName", text(facts.get("品名")));
|
||||
if ("SEA".equals(line)) {
|
||||
// 海运不拦空运件数/重量/体积/包装;港口只带名称,没有港口字典也不跳过。
|
||||
segment.put("origin", origin);
|
||||
segment.put("destination", destination);
|
||||
segment.put("cargoQty", first(facts.get("货量"), facts.get("货物数量")));
|
||||
segment.put("loadType", facts.get("整柜或拼柜"));
|
||||
segment.put("containerSpec", facts.get("箱型箱量"));
|
||||
@@ -100,14 +100,24 @@ public class InquiryTmsQueryService {
|
||||
facts.get("tradeTerms")));
|
||||
} else if ("LAND".equals(line)) {
|
||||
// 陆运不套空运包装/件数;分类和中港装载只在工单,mapper 不会放进出站。
|
||||
segment.put("origin", origin);
|
||||
segment.put("destination", destination);
|
||||
segment.putAll(TmsLandSegmentMapper.build(facts));
|
||||
} else {
|
||||
// 空运:地名栏留空,三字码只放 originCode / destinationCode。
|
||||
segment.put("origin", "");
|
||||
segment.put("destination", "");
|
||||
segment.put("quantity", TmsV2RequestMapper.asInteger(facts.get("件数")));
|
||||
segment.put("weightKg", TmsV2RequestMapper.asPositiveDecimal(facts.get("毛重")));
|
||||
segment.put("volumeCbm", TmsV2RequestMapper.asPositiveDecimal(facts.get("体积")));
|
||||
fillAirportCode(segment, "originCode", text(first(facts.get("originCode"), origin)));
|
||||
fillAirportCode(segment, "destinationCode", text(first(facts.get("destinationCode"), destination)));
|
||||
fillPackageType(segment, text(first(facts.get("packageType"), facts.get("包装方式"), facts.get("包装类型"))));
|
||||
// 包装优先销售原文,避免旧工单 packageType=2 盖住「托盘」。
|
||||
fillPackageType(segment, text(first(facts.get("包装方式"), facts.get("包装类型"), facts.get("packageType"))));
|
||||
segment.put("cargoSource", first(facts.get("cargoSource"), facts.get("货源地")));
|
||||
segment.put("cargoReadyDate", first(facts.get("cargoReadyDate"), facts.get("货好时间")));
|
||||
segment.put("placeOfReceipt", first(facts.get("placeOfReceipt"), facts.get("提货地址")));
|
||||
segment.put("deliveryAddress", first(facts.get("deliveryAddress"), facts.get("收货地址")));
|
||||
}
|
||||
|
||||
String quoteDate = TmsV2RequestMapper.normalizeQuoteDate(facts.get("报价日期"));
|
||||
@@ -333,12 +343,12 @@ public class InquiryTmsQueryService {
|
||||
? String.valueOf(airOptions)
|
||||
: JSON.toJSONString(airOptions));
|
||||
}
|
||||
Object isMulti = first(data.get("isMulti"), data.get("is_multi"), tms.get("isMulti"));
|
||||
if (isMulti != null && !"".equals(String.valueOf(isMulti))) {
|
||||
quote.put("isMulti", String.valueOf(isMulti));
|
||||
}
|
||||
// 陆运线路段:与空运 airOptions 同级透出,供模板 routeRegions 填项目一
|
||||
putLandSegments(quote, data, tms);
|
||||
String isMulti = firstText(data.get("isMulti"), data.get("is_multi"));
|
||||
if (StringUtils.isNotBlank(isMulti)) {
|
||||
quote.put("isMulti", isMulti);
|
||||
}
|
||||
String chargeable = firstText(data.get("chargeableWeightKg"), data.get("chargeable_weight_kg"));
|
||||
if (StringUtils.isNotBlank(chargeable)) {
|
||||
quote.put("chargeableWeightKg", chargeable);
|
||||
@@ -669,7 +679,10 @@ public class InquiryTmsQueryService {
|
||||
return raw == null || String.valueOf(raw).trim().isEmpty();
|
||||
}
|
||||
|
||||
private static List<String> missingAirQuery(String workOrderNo, String quoteDate, Map<String, Object> segment) {
|
||||
/**
|
||||
* 空运出站齐套检查。地名栏允许空串,禁止再拿 destination 拦查价。
|
||||
*/
|
||||
static List<String> missingAirQuery(String workOrderNo, String quoteDate, Map<String, Object> segment) {
|
||||
List<String> missing = new ArrayList<>();
|
||||
if (workOrderNo == null || !workOrderNo.matches("WO\\d{12}")) {
|
||||
missing.add("workOrderNo");
|
||||
@@ -678,7 +691,6 @@ public class InquiryTmsQueryService {
|
||||
missing.add("quoteDate");
|
||||
}
|
||||
for (String key : new String[] {
|
||||
"origin", "destination",
|
||||
"cargoName", "quantity", "weightKg", "volumeCbm", "packageType"
|
||||
}) {
|
||||
if (segment.get(key) == null || "".equals(segment.get(key))) {
|
||||
@@ -711,28 +723,26 @@ public class InquiryTmsQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* TMS 出站要 packageType。销售原文非空即收下,不对销售做包装白名单。
|
||||
* 认得出代码用代码;未对上默认 2。不要默认 1(全部),现网航线 1 常无价、2/3 有价。
|
||||
* 空运包装出站:销售输入什么就传什么,禁止编成 1/2/3。
|
||||
*/
|
||||
private void fillPackageType(Map<String, Object> segment, String raw) {
|
||||
Object pkg = airPackageType(raw);
|
||||
if (pkg != null) {
|
||||
segment.put("packageType", pkg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装原文原样。空串不写,避免主账再编一个默认码。
|
||||
*
|
||||
* @param raw 销售写的包装,例如「托盘」
|
||||
* @return 去空白后的原文;空则 null
|
||||
*/
|
||||
static Object airPackageType(String raw) {
|
||||
if (raw == null || raw.trim().isEmpty()) {
|
||||
segment.put("packageType", 2);
|
||||
return;
|
||||
}
|
||||
if (raw.matches("\\d+")) {
|
||||
segment.put("packageType", Integer.valueOf(raw));
|
||||
return;
|
||||
}
|
||||
TmsExactProjector.Projection hit = projector.resolvePackage(raw);
|
||||
if (!hit.ok()) {
|
||||
segment.put("packageType", 2);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
segment.put("packageType", Integer.valueOf(hit.code));
|
||||
} catch (NumberFormatException ex) {
|
||||
segment.put("packageType", hit.code);
|
||||
return null;
|
||||
}
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
private static Map<String, Object> tech(String message) {
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ import java.util.UUID;
|
||||
* <p>
|
||||
* 锁舱唯一键:workOrderNo + quoteId + optionNo。flightDate 用工单报价日期,接口没有单独 quoteDate。
|
||||
* 操作人角色固定 AIRLINE。数字字段保持 Number,禁止带 KGS/CBM。
|
||||
* quoteId / optionNo 由主账补齐:有 TMS 报价用原值,没有则 QT-工单号 + AIR-OPT-01。
|
||||
* quoteId / optionNo 必填:有 TMS 报价用真编号;没有时由 CabinService 补 QT-{工单号} / AIR-OPT-01。
|
||||
* mapper 仍跳过空字符串,避免把 null 写成 JSON。
|
||||
*/
|
||||
public final class TmsCabinRequestMapper {
|
||||
|
||||
|
||||
+45
-8
@@ -55,28 +55,43 @@ public final class TmsV2RequestMapper {
|
||||
}
|
||||
List<Map<String, Object>> segments = maps(internal.get("segments"));
|
||||
List<Map<String, Object>> external = new ArrayList<>();
|
||||
Object parentMode = first(internal, "transportMode");
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
external.add(segment(segments.get(i), i + 1));
|
||||
external.add(segment(segments.get(i), i + 1, parentMode));
|
||||
}
|
||||
result.put("segments", external);
|
||||
return compact(result);
|
||||
}
|
||||
|
||||
private static Map<String, Object> segment(Map<String, Object> source, int fallbackSequence) {
|
||||
private static Map<String, Object> segment(Map<String, Object> source, int fallbackSequence, Object parentMode) {
|
||||
Map<String, Object> target = new LinkedHashMap<>();
|
||||
Object sequence = first(source, "sequence", "segmentSequence");
|
||||
target.put("sequence", sequence == null ? fallbackSequence : asInteger(sequence));
|
||||
put(target, "transportMode", first(source, "transportMode"));
|
||||
put(target, "origin", first(source, "origin"));
|
||||
put(target, "originCode", first(source, "originCode"));
|
||||
put(target, "destination", first(source, "destination"));
|
||||
put(target, "destinationCode", first(source, "destinationCode"));
|
||||
Object mode = first(source, "transportMode");
|
||||
if (mode == null) {
|
||||
mode = parentMode;
|
||||
}
|
||||
put(target, "transportMode", mode);
|
||||
if ("AIR".equalsIgnoreCase(String.valueOf(mode == null ? "" : mode))) {
|
||||
// 空运地名栏必须空串,三字码只放 originCode / destinationCode。
|
||||
target.put("origin", "");
|
||||
target.put("destination", "");
|
||||
put(target, "originCode", first(source, "originCode"));
|
||||
put(target, "destinationCode", first(source, "destinationCode"));
|
||||
} else {
|
||||
put(target, "origin", first(source, "origin"));
|
||||
put(target, "originCode", first(source, "originCode"));
|
||||
put(target, "destination", first(source, "destination"));
|
||||
put(target, "destinationCode", first(source, "destinationCode"));
|
||||
}
|
||||
put(target, "cargoName", first(source, "cargoName", "goodsName"));
|
||||
// 5.2:quantity=int,weightKg/volumeCbm=decimal>0,禁止带单位字符串
|
||||
put(target, "quantity", asInteger(first(source, "quantity", "pieces")));
|
||||
put(target, "weightKg", asPositiveDecimal(first(source, "weightKg")));
|
||||
put(target, "volumeCbm", asPositiveDecimal(first(source, "volumeCbm")));
|
||||
put(target, "packageType", first(source, "packageType", "packagingType"));
|
||||
// 空运选填:有值才带。贸易条款不进出站,只给后台模板关键词匹配。
|
||||
applyAirOptionalFields(source, target);
|
||||
// 海运出站:口语必须先整理。FCL 要 containerType+containerQty,loadType 只能 FCL/LCL,
|
||||
// cargoQty 只能是数字。销售侧「整柜 / 1x40HQ / 15件」原样留在工单,不在这里改。
|
||||
applySeaFields(source, target);
|
||||
@@ -84,6 +99,17 @@ public final class TmsV2RequestMapper {
|
||||
return compact(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空运选填出站。货源地/货好时间/提货/收货有值才写。
|
||||
* 禁止把贸易条款写成 transportTerms。
|
||||
*/
|
||||
private static void applyAirOptionalFields(Map<String, Object> source, Map<String, Object> target) {
|
||||
put(target, "cargoSource", first(source, "cargoSource", "货源地"));
|
||||
put(target, "cargoReadyDate", first(source, "cargoReadyDate", "货好时间"));
|
||||
put(target, "placeOfReceipt", first(source, "placeOfReceipt", "提货地址"));
|
||||
put(target, "deliveryAddress", first(source, "deliveryAddress", "收货地址"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 把海运口语字段写成 TMS 查询字段。空运段没有这些键则不写。
|
||||
*/
|
||||
@@ -223,7 +249,18 @@ public final class TmsV2RequestMapper {
|
||||
}
|
||||
|
||||
private static Map<String, Object> compact(Map<String, Object> value) {
|
||||
value.entrySet().removeIf(e -> e.getValue() == null || "".equals(e.getValue()));
|
||||
// 空运地名栏要留空串给 TMS,禁止把 origin / destination 当成「没值」删掉。
|
||||
value.entrySet().removeIf(e -> {
|
||||
Object current = e.getValue();
|
||||
if (current == null) {
|
||||
return true;
|
||||
}
|
||||
if ("".equals(current)) {
|
||||
String key = String.valueOf(e.getKey());
|
||||
return !"origin".equals(key) && !"destination".equals(key);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
-- 角色菜单授权:补齐 Jeecg sys_role_permission 标准列。
|
||||
-- 根因:inquiry_robot 建表时只留了 id/role_id/permission_id,
|
||||
-- MyBatis-Plus 按实体写入 operate_date 导致「授权失败」。
|
||||
-- 库:inquiry_robot;幂等。
|
||||
|
||||
SET @has_data_rule := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_role_permission'
|
||||
AND column_name = 'data_rule_ids'
|
||||
);
|
||||
SET @sql := IF(@has_data_rule = 0,
|
||||
'ALTER TABLE sys_role_permission ADD COLUMN data_rule_ids varchar(1000) NULL COMMENT ''数据规则id'' AFTER permission_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_operate_date := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_role_permission'
|
||||
AND column_name = 'operate_date'
|
||||
);
|
||||
SET @sql := IF(@has_operate_date = 0,
|
||||
'ALTER TABLE sys_role_permission ADD COLUMN operate_date datetime NULL COMMENT ''操作时间'' AFTER data_rule_ids',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_operate_ip := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_role_permission'
|
||||
AND column_name = 'operate_ip'
|
||||
);
|
||||
SET @sql := IF(@has_operate_ip = 0,
|
||||
'ALTER TABLE sys_role_permission ADD COLUMN operate_ip varchar(45) NULL COMMENT ''操作ip'' AFTER operate_date',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- 账号绑定角色后列表仍显示「未绑定」:补齐 sys_user_role.tenant_id。
|
||||
-- 根因:绑定写入成功,但 queryUserRole 按 Jeecg 实体 SELECT tenant_id,测库缺列导致查询失败。
|
||||
-- 库:inquiry_robot;幂等。
|
||||
|
||||
SET @has_col := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_user_role'
|
||||
AND column_name = 'tenant_id'
|
||||
);
|
||||
SET @sql := IF(@has_col = 0,
|
||||
'ALTER TABLE sys_user_role ADD COLUMN tenant_id int NULL COMMENT ''租户ID'' AFTER role_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
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.assertTrue;
|
||||
|
||||
/**
|
||||
* 选定线路后写回询价事实:起运港覆盖工单列,不改六态。
|
||||
*/
|
||||
public class InquiryTicketFactsSupportTest {
|
||||
|
||||
@Test
|
||||
public void applyWritesOriginAndKeepsOtherFacts() {
|
||||
InquiryTicket ticket = new InquiryTicket();
|
||||
ticket.setStatus("已报价");
|
||||
ticket.setDestination("克拉克");
|
||||
ticket.setFactsJson("{\"目的港\":\"克拉克\",\"品名\":\"茶叶\"}");
|
||||
Map<String, String> facts = new LinkedHashMap<>();
|
||||
facts.put("目的港", "克拉克");
|
||||
facts.put("品名", "茶叶");
|
||||
facts.put("起运港", "深圳");
|
||||
facts.put("贸易条款", "FOB");
|
||||
InquiryTicketFactsSupport.apply(ticket, facts);
|
||||
assertEquals("深圳", ticket.getOrigin());
|
||||
assertEquals("克拉克", ticket.getDestination());
|
||||
assertEquals("茶叶", ticket.getGoodsName());
|
||||
assertTrue(ticket.getFactsJson().contains("\"起运港\":\"深圳\""));
|
||||
assertTrue(ticket.getFactsJson().contains("\"贸易条款\":\"FOB\""));
|
||||
assertEquals("已报价", ticket.getStatus());
|
||||
assertEquals("深圳 → 克拉克", ticket.getDemandTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidedQuoteIsNotIssuedEvenWithAttachments() {
|
||||
String raw = InquiryQuoteFileSupport.applyVoid("{\"quoteFileIssued\":true,\"total\":\"1\"}");
|
||||
assertTrue(raw.contains("\"quoteFileVoided\":true"));
|
||||
assertFalse(InquiryQuoteFileSupport.isIssued(raw, true));
|
||||
String issued = InquiryQuoteFileSupport.markIssued(raw);
|
||||
assertTrue(InquiryQuoteFileSupport.isIssued(issued, false));
|
||||
assertFalse(issued.contains("quoteFileVoided"));
|
||||
}
|
||||
}
|
||||
+56
-7
@@ -43,17 +43,35 @@ public class InquiryTmsCabinServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveQuoteIdWithoutTmsUsesWorkOrder() {
|
||||
assertEquals("QT-WO202609160012",
|
||||
InquiryTmsCabinService.resolveQuoteId(new HashMap<>(), "WO202609160012"));
|
||||
public void resolveQuoteIdWithoutTmsStaysBlank() {
|
||||
assertEquals("", InquiryTmsCabinService.resolveQuoteId(new HashMap<>(), "WO202609160012"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveOptionWithoutTmsSynthesizesDefault() {
|
||||
public void resolveOptionWithoutTmsUsesAirOpt01() {
|
||||
Map<String, Object> option = InquiryTmsCabinService.resolveOption(new HashMap<>(), "");
|
||||
assertEquals("AIR-OPT-01", option.get("optionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockQuoteIdWithoutTmsUsesWorkOrderPlaceholder() {
|
||||
assertEquals("QT-WO202609210003",
|
||||
InquiryTmsCabinService.lockQuoteId("", "WO202609210003"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockQuoteIdKeepsRealAirId() {
|
||||
assertEquals("AIR-2094721692181016578-2094721692654972929",
|
||||
InquiryTmsCabinService.lockQuoteId(
|
||||
"AIR-2094721692181016578-2094721692654972929",
|
||||
"WO202609160015"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockOptionNoWithoutTmsUsesAirOpt01() {
|
||||
assertEquals("AIR-OPT-01", InquiryTmsCabinService.lockOptionNo(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveQuoteIdKeepsRealAirId() {
|
||||
Map<String, Object> quote = new HashMap<>();
|
||||
@@ -63,11 +81,10 @@ public class InquiryTmsCabinServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void priceOnlyStillFallsBackToWorkOrder() {
|
||||
public void priceOnlyDoesNotInventQuoteId() {
|
||||
Map<String, Object> onlyPrice = new HashMap<>();
|
||||
onlyPrice.put("total", "11792.00");
|
||||
assertEquals("QT-WO202609160015",
|
||||
InquiryTmsCabinService.resolveQuoteId(onlyPrice, "WO202609160015"));
|
||||
assertEquals("", InquiryTmsCabinService.resolveQuoteId(onlyPrice, "WO202609160015"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,4 +95,36 @@ public class InquiryTmsCabinServiceTest {
|
||||
quote.put("airOptions", Arrays.asList(only));
|
||||
assertEquals("AIR-OPT-03", InquiryTmsCabinService.resolveOption(quote, "").get("optionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockFailShowsTmsFailReasonNotMapped2006() {
|
||||
Map<String, Object> tms = new HashMap<>();
|
||||
tms.put("message", "报价选项不存在或与工单不匹配");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("failReason", "该航班日期不存在可锁定的配载记录");
|
||||
tms.put("tmsData", data);
|
||||
assertEquals("该航班日期不存在可锁定的配载记录",
|
||||
InquiryTmsCabinService.cabinFailReason(2006, tms, "2026-09-20", "2026-09-21"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockFailKeepsTmsMessageWhenNoFailReason() {
|
||||
Map<String, Object> tms = new HashMap<>();
|
||||
tms.put("message", "航班已经起飞或起飞时间已到");
|
||||
assertEquals("航班已经起飞或起飞时间已到",
|
||||
InquiryTmsCabinService.cabinFailReason(2006, tms, "2026-09-28", "2026-09-21"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fakeQtWorkOrderQuoteIdIsDropped() {
|
||||
Map<String, Object> quote = new HashMap<>();
|
||||
quote.put("quoteId", "QT-WO202609210003");
|
||||
assertEquals("", InquiryTmsCabinService.resolveQuoteId(quote, "WO202609210003"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockFail2006WithoutTextDoesNotInventQuoteMismatch() {
|
||||
String why = InquiryTmsCabinService.cabinFailReason(2006, new HashMap<>(), "2026-09-20", "2026-09-21");
|
||||
assertEquals("TMS 业务码=2006", why);
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -66,4 +66,30 @@ public class InquiryTmsQueryServiceTest {
|
||||
String line = InquiryTmsQueryService.resolveTransportLine(body, new java.util.HashMap<String, Object>(), "LAND");
|
||||
assertEquals("LAND", line);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airPackageTypeKeepsUserText() {
|
||||
// 销售写托盘就传托盘,禁止编成 2。
|
||||
assertEquals("托盘", InquiryTmsQueryService.airPackageType("托盘"));
|
||||
assertEquals("纸箱", InquiryTmsQueryService.airPackageType("纸箱"));
|
||||
assertEquals("2", InquiryTmsQueryService.airPackageType("2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airQueryAllowsEmptyPlaceNames() {
|
||||
// 地名栏空串不能拦出站;三字码和包装原文齐了就要打 TMS。
|
||||
java.util.Map<String, Object> segment = new java.util.HashMap<String, Object>();
|
||||
segment.put("origin", "");
|
||||
segment.put("destination", "");
|
||||
segment.put("originCode", "ZUH");
|
||||
segment.put("destinationCode", "CRK");
|
||||
segment.put("cargoName", "茶叶");
|
||||
segment.put("quantity", 1);
|
||||
segment.put("weightKg", "1.5");
|
||||
segment.put("volumeCbm", "100");
|
||||
segment.put("packageType", "托盘");
|
||||
java.util.List<String> missing = InquiryTmsQueryService.missingAirQuery(
|
||||
"WO202609220004", "2026-09-25", segment);
|
||||
org.junit.jupiter.api.Assertions.assertTrue(missing.isEmpty(), String.valueOf(missing));
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -39,6 +39,31 @@ public class TmsCabinRequestMapperTest {
|
||||
assertTrue(body.containsKey("requestId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockOmitsBlankQuoteAndOption() {
|
||||
Map<String, Object> internal = new LinkedHashMap<>();
|
||||
internal.put("workOrderNo", "WO202609210003");
|
||||
internal.put("quoteId", "");
|
||||
internal.put("optionNo", "");
|
||||
internal.put("flightDate", "2026-09-20");
|
||||
Map<String, Object> body = TmsCabinRequestMapper.lock(internal);
|
||||
assertEquals("WO202609210003", body.get("workOrderNo"));
|
||||
assertEquals(null, body.get("quoteId"));
|
||||
assertEquals(null, body.get("optionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockKeepsNoQuotePlaceholders() {
|
||||
Map<String, Object> filled = new LinkedHashMap<>();
|
||||
filled.put("workOrderNo", "WO202609210003");
|
||||
filled.put("quoteId", "QT-WO202609210003");
|
||||
filled.put("optionNo", "AIR-OPT-01");
|
||||
filled.put("flightDate", "2026-09-20");
|
||||
Map<String, Object> sent = TmsCabinRequestMapper.lock(filled);
|
||||
assertEquals("QT-WO202609210003", sent.get("quoteId"));
|
||||
assertEquals("AIR-OPT-01", sent.get("optionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releaseKeepsLockId() {
|
||||
Map<String, Object> internal = new LinkedHashMap<>();
|
||||
|
||||
+66
@@ -9,6 +9,7 @@ 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。
|
||||
@@ -156,4 +157,69 @@ public class TmsV2RequestMapperTest {
|
||||
assertFalse(segs.get(0).containsKey("运输分类"));
|
||||
assertFalse(body.containsKey("运输分类"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airOptionalFieldsPassThroughWithoutTradeTerms() {
|
||||
Map<String, Object> seg = new HashMap<>();
|
||||
seg.put("destination", "克拉克");
|
||||
seg.put("destinationCode", "CRK");
|
||||
seg.put("cargoName", "茶叶");
|
||||
seg.put("quantity", "10");
|
||||
seg.put("weightKg", "100");
|
||||
seg.put("volumeCbm", "2");
|
||||
seg.put("packageType", "2");
|
||||
seg.put("cargoSource", "东莞");
|
||||
seg.put("cargoReadyDate", "2026-09-22");
|
||||
seg.put("placeOfReceipt", "东莞仓库");
|
||||
seg.put("deliveryAddress", "马尼拉");
|
||||
seg.put("贸易条款", "FOB");
|
||||
seg.put("tradeTerms", "FOB");
|
||||
Map<String, Object> internal = new HashMap<>();
|
||||
internal.put("transportMode", "AIR");
|
||||
internal.put("quoteDate", "2026-09-22");
|
||||
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");
|
||||
Map<String, Object> out = segs.get(0);
|
||||
assertEquals("东莞", out.get("cargoSource"));
|
||||
assertEquals("2026-09-22", out.get("cargoReadyDate"));
|
||||
assertEquals("东莞仓库", out.get("placeOfReceipt"));
|
||||
assertEquals("马尼拉", out.get("deliveryAddress"));
|
||||
assertEquals("", out.get("origin"));
|
||||
assertEquals("", out.get("destination"));
|
||||
assertEquals("CRK", out.get("destinationCode"));
|
||||
assertFalse(out.containsKey("transportTerms"));
|
||||
assertFalse(out.containsKey("tradeTerms"));
|
||||
assertFalse(out.containsKey("贸易条款"));
|
||||
assertNull(out.get("originCode"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airPlaceNamesStayEmptyCodesOnlyAndRawPackage() {
|
||||
// 地名栏必须空串;三字码只放 *Code。包装原文原样,禁止编成 2。
|
||||
Map<String, Object> seg = new HashMap<>();
|
||||
seg.put("origin", "ZUH");
|
||||
seg.put("originCode", "ZUH");
|
||||
seg.put("destination", "CRK");
|
||||
seg.put("destinationCode", "CRK");
|
||||
seg.put("cargoName", "茶叶");
|
||||
seg.put("quantity", "1");
|
||||
seg.put("weightKg", "1.5");
|
||||
seg.put("volumeCbm", "100");
|
||||
seg.put("packageType", "托盘");
|
||||
Map<String, Object> internal = new HashMap<>();
|
||||
internal.put("transportMode", "AIR");
|
||||
internal.put("quoteDate", "2026-09-25");
|
||||
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");
|
||||
Map<String, Object> out = segs.get(0);
|
||||
assertEquals("", out.get("origin"));
|
||||
assertEquals("ZUH", out.get("originCode"));
|
||||
assertEquals("", out.get("destination"));
|
||||
assertEquals("CRK", out.get("destinationCode"));
|
||||
assertEquals("托盘", out.get("packageType"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user