完善报价单填表与币种回填,并补齐账号末次登录、角色按钮权限与陆运全线匹配。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+219
-15
@@ -846,6 +846,13 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
String eta = first(q, "eta", "transit_days", "transitDays");
|
||||
if (StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
|
||||
out.put("transit_days", eta);
|
||||
out.put("transit_time", eta);
|
||||
}
|
||||
// 空运计费重:TMS chargeableWeightKg → 模板 chargeable_weight_kg
|
||||
String chargeable = first(q, "chargeableWeightKg", "chargeable_weight_kg", "chargeable_weight");
|
||||
if (StringUtils.isNotBlank(chargeable) && !"-".equals(chargeable)) {
|
||||
out.put("chargeable_weight_kg", chargeable);
|
||||
out.put("chargeableWeightKg", chargeable);
|
||||
}
|
||||
for (Map.Entry<String, String> e : QuoteTemplateExcelSupport.LAND_TMS_KEY_TO_CODE.entrySet()) {
|
||||
String val = first(q, e.getKey(), e.getValue());
|
||||
@@ -892,9 +899,102 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
if (!rows.isEmpty()) {
|
||||
out.put("fee_rows", rows);
|
||||
}
|
||||
// 海运分区:透传 pol/ocean/pod 桶,供 feeRegions 按段回填
|
||||
for (String bucket : new String[]{"polCostItems", "oceanCostItems", "podCostItems"}) {
|
||||
JSONArray part = QuoteTemplateExcelSupport.parseFeeRows(q.get("fee_rows_" + bucket));
|
||||
if (part.isEmpty()) {
|
||||
part = QuoteTemplateExcelSupport.parseFeeRows(q.get(bucket));
|
||||
}
|
||||
if (!part.isEmpty()) {
|
||||
out.put("fee_rows_" + bucket, part);
|
||||
out.put(bucket, part);
|
||||
}
|
||||
}
|
||||
// 陆运线路:透传 landSegments / route_rows,供 routeRegions 填项目一
|
||||
JSONArray landSegs = QuoteTemplateExcelSupport.parseFeeRows(q.get("route_rows"));
|
||||
if (landSegs.isEmpty()) {
|
||||
landSegs = QuoteTemplateExcelSupport.parseFeeRows(q.get("landSegments"));
|
||||
}
|
||||
if (!landSegs.isEmpty()) {
|
||||
out.put("landSegments", landSegs);
|
||||
out.put("route_rows", landSegs);
|
||||
// 首段时效若 facts 尚无,用腿上 transit
|
||||
if (StringUtils.isBlank(out.getString("transit_time")) || "-".equals(out.getString("transit_time"))) {
|
||||
JSONObject firstSeg = landSegs.getJSONObject(0);
|
||||
if (firstSeg != null) {
|
||||
String t = firstNonBlankLocal(firstSeg.getString("transit_time"), firstSeg.getString("transitDays"));
|
||||
if (StringUtils.isNotBlank(t)) {
|
||||
out.put("transit_time", t);
|
||||
out.put("transit_days", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (looksLandInquiry(src)) {
|
||||
// 无 landSegments 时:用询价 OD + fee_lines 分项拼一行,尽量填「提货费/运输费/送货费」列
|
||||
JSONObject one = new JSONObject(true);
|
||||
one.put("origin", out.getString("origin"));
|
||||
one.put("destination", out.getString("destination"));
|
||||
one.put("transit_time", out.getString("transit_time"));
|
||||
one.put("transitDays", out.getString("transit_days"));
|
||||
one.put("currency", out.getString("currency"));
|
||||
one.put("pickup_fee", out.getString("pickup_fee"));
|
||||
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"));
|
||||
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);
|
||||
if (fee == null) {
|
||||
continue;
|
||||
}
|
||||
String fname = StringUtils.trimToEmpty(fee.getString("name"));
|
||||
String famt = StringUtils.trimToEmpty(fee.getString("amount"));
|
||||
if (StringUtils.isBlank(famt) || "-".equals(famt)) {
|
||||
continue;
|
||||
}
|
||||
if (fname.contains("提货") && StringUtils.isBlank(one.getString("pickup_fee"))) {
|
||||
one.put("pickup_fee", famt);
|
||||
} else if ((fname.contains("送货") || fname.contains("派送")) && StringUtils.isBlank(one.getString("delivery_fee"))) {
|
||||
one.put("delivery_fee", famt);
|
||||
} else if (fname.contains("CBM") && StringUtils.isBlank(one.getString("price_per_cbm"))) {
|
||||
one.put("price_per_cbm", famt);
|
||||
} else if ((fname.contains("KG") || fname.contains("kg") || fname.contains("公斤"))
|
||||
&& StringUtils.isBlank(one.getString("price_per_kg"))) {
|
||||
one.put("price_per_kg", famt);
|
||||
} else if (fname.contains("运输") || fname.contains("陆运") || fname.contains("运费")) {
|
||||
landAmt = famt;
|
||||
one.put("salePrice", famt);
|
||||
one.put("amount", famt);
|
||||
if (StringUtils.isNotBlank(fee.getString("remark"))) {
|
||||
one.put("remark", fee.getString("remark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(landAmt) && StringUtils.isBlank(one.getString("amount"))) {
|
||||
one.put("amount", landAmt);
|
||||
one.put("salePrice", landAmt);
|
||||
}
|
||||
if (StringUtils.isNotBlank(one.getString("origin")) || StringUtils.isNotBlank(one.getString("destination"))) {
|
||||
JSONArray routeRows = new JSONArray();
|
||||
routeRows.add(one);
|
||||
out.put("route_rows", routeRows);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String firstNonBlankLocal(String... vals) {
|
||||
if (vals == null) {
|
||||
return "";
|
||||
}
|
||||
for (String v : vals) {
|
||||
if (StringUtils.isNotBlank(v) && !"-".equals(v.trim())) {
|
||||
return v.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 询价事实里已有陆运选项/始发站,运费兜底名就不能再写成空运费。
|
||||
*/
|
||||
@@ -1013,7 +1113,7 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
/**
|
||||
* 智能体回写 AI 映射。校验白名单与 feeRegion 增行配置;未映射必填 → mapping_incomplete。
|
||||
* <p>
|
||||
* mergeExisting=true 时:与库中旧 mapping 补缺合并(旧字段/旧区域优先,新结果只补没有的)。
|
||||
* mergeExisting=true 时:与库中旧 mapping 智能合并(对齐标签的旧坐标保留,对不上的改用新坐标)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -1054,13 +1154,34 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
versionMapper.updateById(ver);
|
||||
return;
|
||||
}
|
||||
// 未换文件重新解析:旧映射优先,避免已对上的字段被新一轮 AI 冲掉
|
||||
// 未换文件重新解析:仍对齐的旧坐标保留;对不上标签的改用新坐标
|
||||
if (mergeExisting && StringUtils.isNotBlank(ver.getMappingJson())) {
|
||||
try {
|
||||
JSONObject old = JSON.parseObject(ver.getMappingJson());
|
||||
if (old != null) {
|
||||
mapping = mergeMappingPreferExisting(old, mapping);
|
||||
log.info("模板映射补缺合并 versionId={} keepFields={}",
|
||||
org.apache.poi.ss.usermodel.Workbook wb = null;
|
||||
try {
|
||||
if (StringUtils.isNotBlank(ver.getObjectKey())) {
|
||||
Path local = resolveObjectPath(ver.getObjectKey());
|
||||
try (InputStream in = Files.newInputStream(local)) {
|
||||
wb = org.apache.poi.ss.usermodel.WorkbookFactory.create(in);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("合并校验打开模板失败 versionId={} err={}", versionId, ex.getMessage());
|
||||
}
|
||||
try {
|
||||
mapping = mergeMappingPreferExisting(old, mapping, wb);
|
||||
} finally {
|
||||
if (wb != null) {
|
||||
try {
|
||||
wb.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("模板映射智能合并 versionId={} oldFields={}",
|
||||
versionId,
|
||||
old.getJSONObject("fields") == null ? 0 : old.getJSONObject("fields").size());
|
||||
}
|
||||
@@ -1088,7 +1209,7 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
} else if (mergeExisting && !"failed".equals(ver.getParseStatus())) {
|
||||
// 合并后重算摘要点数,避免沿用 AI 单轮字数
|
||||
int pts = ver.getMappingPointCount() == null ? 0 : ver.getMappingPointCount();
|
||||
String base = "已解析 " + pts + " 个映射点(保留原映射并补缺)";
|
||||
String base = "已解析 " + pts + " 个映射点(对齐的保留,对不上的改用新坐标)";
|
||||
if (StringUtils.isNotBlank(parseMessage) && parseMessage.contains("未解析到")) {
|
||||
int idx = parseMessage.indexOf(";未解析到");
|
||||
if (idx > 0) {
|
||||
@@ -1103,10 +1224,23 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
}
|
||||
|
||||
/**
|
||||
* 补缺合并:旧 fields / feeRegions / routeRegions 优先;新结果只补充旧里没有的 code/id。
|
||||
* 不删旧项;mappingGaps 以新结果为准(智能体已按合并后复评时更准,此处兜底保留新 gaps)。
|
||||
* 重新解析合并(无 Workbook 时退化为旧坐标优先,兼容单测)。
|
||||
*/
|
||||
static JSONObject mergeMappingPreferExisting(JSONObject oldMapping, JSONObject newMapping) {
|
||||
public static JSONObject mergeMappingPreferExisting(JSONObject oldMapping, JSONObject newMapping) {
|
||||
return mergeMappingPreferExisting(oldMapping, newMapping, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新解析合并:费用/线路区仍按 id 旧优先;fields 在对照模板后智能取舍。
|
||||
* <p>
|
||||
* 规则:旧无→用新;新无→留旧;两者相同→留旧;旧坐标仍贴标签→留旧;
|
||||
* 旧坐标对不上标签(有 Workbook 可校验)→改用新坐标。
|
||||
* mappingGaps 以新结果为准。
|
||||
*
|
||||
* @param workbook 可空;有则校验旧坐标是否仍对齐标签
|
||||
*/
|
||||
public static JSONObject mergeMappingPreferExisting(JSONObject oldMapping, JSONObject newMapping,
|
||||
org.apache.poi.ss.usermodel.Workbook workbook) {
|
||||
JSONObject out = new JSONObject(true);
|
||||
if (newMapping != null) {
|
||||
out.putAll(newMapping);
|
||||
@@ -1114,29 +1248,99 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
if (oldMapping == null) {
|
||||
return out;
|
||||
}
|
||||
// fields:先放新的,再用旧的覆盖同名 → 旧优先
|
||||
JSONObject fields = new JSONObject(true);
|
||||
JSONObject newFields = newMapping == null ? null : newMapping.getJSONObject("fields");
|
||||
JSONObject oldFields = oldMapping.getJSONObject("fields");
|
||||
JSONObject fields = new JSONObject(true);
|
||||
if (newFields != null) {
|
||||
fields.putAll(newFields);
|
||||
}
|
||||
JSONObject newMeta = newMapping == null ? null : newMapping.getJSONObject("fieldMeta");
|
||||
JSONObject oldMeta = oldMapping.getJSONObject("fieldMeta");
|
||||
JSONObject metaOut = new JSONObject(true);
|
||||
if (newMeta != null) {
|
||||
metaOut.putAll(newMeta);
|
||||
}
|
||||
Set<String> codes = new LinkedHashSet<>();
|
||||
if (oldFields != null) {
|
||||
for (String code : oldFields.keySet()) {
|
||||
Object cell = oldFields.get(code);
|
||||
if (cell != null && StringUtils.isNotBlank(String.valueOf(cell))) {
|
||||
fields.put(code, cell);
|
||||
codes.addAll(oldFields.keySet());
|
||||
}
|
||||
if (newFields != null) {
|
||||
codes.addAll(newFields.keySet());
|
||||
}
|
||||
for (String code : codes) {
|
||||
String oldCell = oldFields == null ? null : StringUtils.trimToNull(oldFields.getString(code));
|
||||
String newCell = newFields == null ? null : StringUtils.trimToNull(newFields.getString(code));
|
||||
String chosen;
|
||||
boolean keptOld;
|
||||
if (StringUtils.isBlank(oldCell)) {
|
||||
chosen = newCell;
|
||||
keptOld = false;
|
||||
} else if (StringUtils.isBlank(newCell)) {
|
||||
chosen = oldCell;
|
||||
keptOld = true;
|
||||
} else if (oldCell.equalsIgnoreCase(newCell)) {
|
||||
chosen = oldCell;
|
||||
keptOld = true;
|
||||
// 新旧都是 B3 单选时仍要纠到 RFQ 标签格
|
||||
if ("quote_no".equals(code) && workbook != null
|
||||
&& !QuoteTemplateExcelSupport.fieldMappingStillAligned(workbook, code, oldCell)) {
|
||||
String better = QuoteTemplateExcelSupport.relocateQuoteNoCell(workbook, oldCell);
|
||||
if (StringUtils.isNotBlank(better) && !better.equalsIgnoreCase(oldCell)) {
|
||||
chosen = better;
|
||||
keptOld = false;
|
||||
JSONObject qm = metaOut.getJSONObject("quote_no");
|
||||
if (qm == null) {
|
||||
qm = new JSONObject(true);
|
||||
}
|
||||
qm.put("sameCellAfterColon", true);
|
||||
metaOut.put("quote_no", qm);
|
||||
log.info("映射坐标纠偏 code={} old={} -> new={}", code, oldCell, chosen);
|
||||
}
|
||||
}
|
||||
} else if (workbook != null
|
||||
&& QuoteTemplateExcelSupport.fieldMappingStillAligned(workbook, code, oldCell)) {
|
||||
chosen = oldCell;
|
||||
keptOld = true;
|
||||
} else if (workbook != null) {
|
||||
// 旧坐标对不上标签 → 用新;quote_no 若仍落在单选格则再纠到标签格冒号后
|
||||
chosen = newCell;
|
||||
keptOld = false;
|
||||
if ("quote_no".equals(code)) {
|
||||
String better = QuoteTemplateExcelSupport.relocateQuoteNoCell(workbook, newCell);
|
||||
if (StringUtils.isNotBlank(better)) {
|
||||
chosen = better;
|
||||
}
|
||||
// 纠到标签格后,出单必须同格冒号后写
|
||||
JSONObject qm = metaOut.getJSONObject("quote_no");
|
||||
if (qm == null) {
|
||||
qm = new JSONObject(true);
|
||||
}
|
||||
qm.put("sameCellAfterColon", true);
|
||||
metaOut.put("quote_no", qm);
|
||||
}
|
||||
log.info("映射坐标纠偏 code={} old={} -> new={}", code, oldCell, chosen);
|
||||
} else {
|
||||
// 无表可校验时保守保留旧坐标
|
||||
chosen = oldCell;
|
||||
keptOld = true;
|
||||
}
|
||||
if (StringUtils.isNotBlank(chosen)) {
|
||||
fields.put(code, chosen);
|
||||
}
|
||||
if (keptOld && oldMeta != null && oldMeta.getJSONObject(code) != null) {
|
||||
metaOut.put(code, oldMeta.getJSONObject(code));
|
||||
}
|
||||
}
|
||||
out.put("fields", fields);
|
||||
if (!metaOut.isEmpty()) {
|
||||
out.put("fieldMeta", metaOut);
|
||||
}
|
||||
out.put("feeRegions", mergeRegionsById(
|
||||
oldMapping.getJSONArray("feeRegions"),
|
||||
newMapping == null ? null : newMapping.getJSONArray("feeRegions")));
|
||||
out.put("routeRegions", mergeRegionsById(
|
||||
oldMapping.getJSONArray("routeRegions"),
|
||||
newMapping == null ? null : newMapping.getJSONArray("routeRegions")));
|
||||
// 说明性标签:齐全时清空;否则保留新的
|
||||
if (out.getJSONArray("mappingGaps") != null && out.getJSONArray("mappingGaps").isEmpty()) {
|
||||
out.put("unmappedRequired", new JSONArray());
|
||||
out.put("unmappedLabels", new JSONArray());
|
||||
|
||||
+4
-2
@@ -12,7 +12,7 @@ import java.util.Map;
|
||||
* 陆运拉群人员匹配:只看员工管理里岗位=产品·陆运且启用的人。
|
||||
* <p>
|
||||
* 用本单线路类别对负责线路:按逗号/顿号拆整词,禁止把「中港/中亚/中欧」按斜杠切开。
|
||||
* 没有「陆运全线」快捷项;四条都要跟就须后台四条都勾。
|
||||
* 负责线路含「陆运全线」时视为命中全部线路类别(与海运「海运全线」对称)。
|
||||
* 禁止:按销售 userid 伪造名单;不查企微通讯录。
|
||||
*/
|
||||
public final class LandStaffMatcher {
|
||||
@@ -47,7 +47,9 @@ public final class LandStaffMatcher {
|
||||
if (StringUtils.isBlank(row.getWecomId())) {
|
||||
continue;
|
||||
}
|
||||
if (!routeTokens(row.getRoutes()).contains(want)) {
|
||||
List<String> tokens = routeTokens(row.getRoutes());
|
||||
// 陆运全线:跟海运全线一样,一条勾选覆盖全部线路类别
|
||||
if (!tokens.contains("陆运全线") && !tokens.contains(want)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
|
||||
+799
-35
File diff suppressed because it is too large
Load Diff
+201
-8
@@ -300,6 +300,8 @@ public class InquiryTmsQueryService {
|
||||
quote.put("source", "TMS");
|
||||
quote.put("source_label", "TMS 标准报价");
|
||||
quote.put("fee_lines", buildFeeLinesJson(data, freightMoney.amount, otherMoney.amount, headerCurrency));
|
||||
// 海运 feeItems 是分段桶:polCostItems / oceanCostItems / podCostItems,单独透出给模板分区回填
|
||||
putSeaFeeBuckets(quote, data);
|
||||
// 锁舱必须带着 TMS 报价编号和舱位选项,不能只落金额。
|
||||
// 真实 TMS 的 quoteId 形如 AIR-数字-数字,optionNo 是 AIR-OPT01(中间没有第二根横杠)。
|
||||
String quoteId = firstText(data.get("quoteId"), data.get("quote_id"),
|
||||
@@ -318,6 +320,12 @@ public class InquiryTmsQueryService {
|
||||
? String.valueOf(airOptions)
|
||||
: JSON.toJSONString(airOptions));
|
||||
}
|
||||
// 陆运线路段:与空运 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);
|
||||
@@ -325,6 +333,127 @@ public class InquiryTmsQueryService {
|
||||
return quote;
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运 landSegments 原样+规范化写入 quote。
|
||||
* <p>
|
||||
* 为何:模板 routeRegions 的 tmsSource=landSegments;若不落库/不入 quote,
|
||||
* 出单只能写出始发目的时效,车型价/分腿价全空。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void putLandSegments(Map<String, String> quote, Map<String, Object> data, Map<String, Object> tms) {
|
||||
if (quote == null) {
|
||||
return;
|
||||
}
|
||||
Object raw = first(
|
||||
data == null ? null : data.get("landSegments"),
|
||||
data == null ? null : data.get("land_segments"),
|
||||
tms == null ? null : tms.get("landSegments"),
|
||||
tms == null ? null : tms.get("land_segments"));
|
||||
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("landSegments", json);
|
||||
// 同步一份 route_rows,字段名对齐填表(transit_time / salePrice 等)
|
||||
List<Map<String, String>> rows = normalizeLandSegmentRows(raw);
|
||||
if (!rows.isEmpty()) {
|
||||
quote.put("route_rows", JSON.toJSONString(rows));
|
||||
}
|
||||
// feeItems 为空时,用各腿售价补一条运输费,避免报价明细全空
|
||||
String feeLines = quote.get("fee_lines");
|
||||
if ((StringUtils.isBlank(feeLines) || "[]".equals(feeLines.trim())) && !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"));
|
||||
if (StringUtils.isBlank(amount) || "-".equals(amount)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> line = new LinkedHashMap<>();
|
||||
line.put("name", "运输费");
|
||||
line.put("amount", amount);
|
||||
if (StringUtils.isNotBlank(seg.get("currency"))) {
|
||||
line.put("currency", seg.get("currency"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(seg.get("remark"))) {
|
||||
line.put("remark", seg.get("remark"));
|
||||
}
|
||||
lines.add(line);
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
quote.put("fee_lines", JSON.toJSONString(lines));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* landSegments[] → 填表行:统一 origin/destination/salePrice/transit_time/vehicleType。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<Map<String, String>> normalizeLandSegmentRows(Object raw) {
|
||||
List<Map<String, String>> out = new ArrayList<>();
|
||||
List<?> list;
|
||||
if (raw instanceof List) {
|
||||
list = (List<?>) raw;
|
||||
} else if (raw instanceof String) {
|
||||
try {
|
||||
Object parsed = JSON.parse(String.valueOf(raw));
|
||||
if (!(parsed instanceof List)) {
|
||||
return out;
|
||||
}
|
||||
list = (List<?>) parsed;
|
||||
} catch (Exception e) {
|
||||
return out;
|
||||
}
|
||||
} else {
|
||||
return out;
|
||||
}
|
||||
for (Object item : list) {
|
||||
if (!(item instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> seg = (Map<?, ?>) item;
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
putExtra(row, "segmentSequence", firstText(seg.get("segmentSequence"), seg.get("sequence")));
|
||||
putExtra(row, "routeLegSequence", firstText(seg.get("routeLegSequence"), seg.get("legSequence")));
|
||||
putExtra(row, "origin", firstText(seg.get("origin"), seg.get("originName"), seg.get("originCode")));
|
||||
putExtra(row, "originCode", firstText(seg.get("originCode")));
|
||||
putExtra(row, "destination", firstText(seg.get("destination"), seg.get("destinationName"), seg.get("destinationCode")));
|
||||
putExtra(row, "destinationCode", firstText(seg.get("destinationCode")));
|
||||
putExtra(row, "vehicleType", firstText(seg.get("vehicleType"), seg.get("vehicle_type"), seg.get("truckType")));
|
||||
putExtra(row, "landTransportType", firstText(seg.get("landTransportType")));
|
||||
putExtra(row, "routeCategory", firstText(seg.get("routeCategory")));
|
||||
putExtra(row, "currency", firstText(seg.get("currency"), seg.get("settlementCurrency")));
|
||||
String sale = firstText(seg.get("salePrice"), seg.get("sale_price"));
|
||||
String cost = firstText(seg.get("costPrice"), seg.get("cost_price"));
|
||||
putExtra(row, "salePrice", sale);
|
||||
putExtra(row, "costPrice", cost);
|
||||
putExtra(row, "amount", firstText(sale, cost));
|
||||
String transit = firstText(seg.get("transitDays"), seg.get("transit_time"), seg.get("transitTime"),
|
||||
seg.get("aging"));
|
||||
putExtra(row, "transitDays", transit);
|
||||
putExtra(row, "transit_time", transit);
|
||||
putExtra(row, "remark", firstText(seg.get("remark"), seg.get("note"), seg.get("description")));
|
||||
// 腿上若带分项费率扩展字段,原样带上供「提货费/元/CBM」列匹配
|
||||
String[][] rateKeys = {
|
||||
{"pickupFee", "pickup_fee"}, {"deliveryFee", "delivery_fee"},
|
||||
{"pricePerCbm", "price_per_cbm"}, {"pricePerKg", "price_per_kg"},
|
||||
{"minCharge", "min_charge"}, {"landFreight", "land_freight"}
|
||||
};
|
||||
for (String[] pair : rateKeys) {
|
||||
putExtra(row, pair[1], firstText(seg.get(pair[0]), seg.get(pair[1])));
|
||||
putExtra(row, pair[0], firstText(seg.get(pair[0])));
|
||||
}
|
||||
if (StringUtils.isNotBlank(row.get("origin")) || StringUtils.isNotBlank(row.get("destination"))
|
||||
|| StringUtils.isNotBlank(row.get("amount"))) {
|
||||
out.add(row);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把本次 TMS 报价写回工单主表,供后台列表/详情展示。
|
||||
* 没有的成本/毛利率不编;售价用销售合计。
|
||||
@@ -351,6 +480,8 @@ public class InquiryTmsQueryService {
|
||||
/**
|
||||
* 抽出销售侧价格明细行。优先 TMS 费用清单,没有再用常见费用字段兜底。
|
||||
* 每行带自己的币种;没有就继承表头,仍然不编造。
|
||||
* 海运 feeItems 常是 [{polCostItems:[...]},{oceanCostItems:[...]},{podCostItems:[...]}],
|
||||
* 需展开各桶再扁平,否则顶层无 feeName 会抽成空列表。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String buildFeeLinesJson(Map<String, Object> data, String freight, String other, String headerCurrency) {
|
||||
@@ -362,14 +493,22 @@ public class InquiryTmsQueryService {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> row = (Map<?, ?>) item;
|
||||
String name = firstText(row.get("feeName"), row.get("name"), row.get("chargeName"),
|
||||
row.get("itemName"), row.get("feeType"));
|
||||
String amount = firstText(row.get("salePrice"), row.get("amount"), row.get("price"),
|
||||
row.get("feeAmount"), row.get("totalPrice"));
|
||||
String lineCurrency = TmsQuoteCurrency.fromFeeRow(row);
|
||||
addFeeLine(lines, name, amount,
|
||||
StringUtils.isNotBlank(lineCurrency) ? lineCurrency : headerCurrency,
|
||||
extrasFromFee(row));
|
||||
// 海运分段桶:展开子列表
|
||||
boolean nested = false;
|
||||
for (String bucket : new String[]{"polCostItems", "oceanCostItems", "podCostItems"}) {
|
||||
Object sub = row.get(bucket);
|
||||
if (sub instanceof List) {
|
||||
nested = true;
|
||||
for (Object one : (List<?>) sub) {
|
||||
if (one instanceof Map) {
|
||||
appendFeeLineFromRow(lines, (Map<?, ?>) one, headerCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!nested) {
|
||||
appendFeeLineFromRow(lines, row, headerCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
@@ -384,6 +523,60 @@ public class InquiryTmsQueryService {
|
||||
return JSON.toJSONString(lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 海运三段费用桶单独落 quote,供模板 feeRegions 按 tmsSource 分区写,禁止三段共用扁平 fee_lines。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void putSeaFeeBuckets(Map<String, String> quote, Map<String, Object> data) {
|
||||
if (quote == null || data == null) {
|
||||
return;
|
||||
}
|
||||
Object raw = first(data.get("feeItems"), data.get("feeList"));
|
||||
if (!(raw instanceof List)) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<Map<String, String>>> buckets = new LinkedHashMap<>();
|
||||
buckets.put("polCostItems", new ArrayList<>());
|
||||
buckets.put("oceanCostItems", new ArrayList<>());
|
||||
buckets.put("podCostItems", new ArrayList<>());
|
||||
String headerCurrency = TmsQuoteCurrency.fromHeader(data);
|
||||
for (Object item : (List<?>) raw) {
|
||||
if (!(item instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> row = (Map<?, ?>) item;
|
||||
for (String bucket : buckets.keySet()) {
|
||||
Object sub = row.get(bucket);
|
||||
if (!(sub instanceof List)) {
|
||||
continue;
|
||||
}
|
||||
for (Object one : (List<?>) sub) {
|
||||
if (one instanceof Map) {
|
||||
appendFeeLineFromRow(buckets.get(bucket), (Map<?, ?>) one, headerCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, List<Map<String, String>>> e : buckets.entrySet()) {
|
||||
if (!e.getValue().isEmpty()) {
|
||||
String json = JSON.toJSONString(e.getValue());
|
||||
quote.put(e.getKey(), json);
|
||||
quote.put("fee_rows_" + e.getKey(), json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendFeeLineFromRow(List<Map<String, String>> lines, Map<?, ?> row, String headerCurrency) {
|
||||
String name = firstText(row.get("feeName"), row.get("name"), row.get("chargeName"),
|
||||
row.get("itemName"), row.get("feeType"));
|
||||
String amount = firstText(row.get("salePrice"), row.get("amount"), row.get("price"),
|
||||
row.get("feeAmount"), row.get("totalPrice"));
|
||||
String lineCurrency = TmsQuoteCurrency.fromFeeRow(row);
|
||||
addFeeLine(lines, name, amount,
|
||||
StringUtils.isNotBlank(lineCurrency) ? lineCurrency : headerCurrency,
|
||||
extrasFromFee(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用行上的单位 / 数量 / 单价 / 备注只取 TMS 原文,没有就空着。
|
||||
*/
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
-- 询价后台菜单按钮:让角色树展开「查看/导出」等明细(对齐旧运营后台)
|
||||
-- 库:inquiry_robot;幂等。
|
||||
-- 根因:子菜单 is_leaf=1 时 Jeecg queryTreeList 不递归 menu_type=2 按钮。
|
||||
|
||||
-- 1) 有子节点的菜单必须非叶子
|
||||
UPDATE sys_permission
|
||||
SET is_leaf = 0
|
||||
WHERE id IN (
|
||||
'b1000000000000000000000000000001',
|
||||
'b1000000000000000000000000000002',
|
||||
'b1000000000000000000000000000003',
|
||||
'b1000000000000000000000000000004',
|
||||
'b1000000000000000000000000000005',
|
||||
'b1000000000000000000000000000006',
|
||||
'b1000000000000000000000000000010',
|
||||
'b1000000000000000000000000000011',
|
||||
'b1000000000000000000000000000012',
|
||||
'b1000000000000000000000000000013'
|
||||
);
|
||||
|
||||
-- 2) 页面菜单不要挂 perms(权限码只在按钮上)
|
||||
UPDATE sys_permission
|
||||
SET perms = NULL
|
||||
WHERE id IN (
|
||||
'b1000000000000000000000000000002',
|
||||
'b1000000000000000000000000000003',
|
||||
'b1000000000000000000000000000004',
|
||||
'b1000000000000000000000000000005',
|
||||
'b1000000000000000000000000000006'
|
||||
);
|
||||
|
||||
-- 3) 已有按钮改名为旧后台口径
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000021';
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000031';
|
||||
UPDATE sys_permission SET name='关闭工单' WHERE id='b1000000000000000000000000000032';
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000041';
|
||||
UPDATE sys_permission SET name='新增' WHERE id='b1000000000000000000000000000042';
|
||||
UPDATE sys_permission SET name='编辑' WHERE id='b1000000000000000000000000000043';
|
||||
UPDATE sys_permission SET name='停用' WHERE id='b1000000000000000000000000000044';
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000051';
|
||||
UPDATE sys_permission SET name='新增' WHERE id='b1000000000000000000000000000052';
|
||||
UPDATE sys_permission SET name='编辑' WHERE id='b1000000000000000000000000000053';
|
||||
UPDATE sys_permission SET name='停用' WHERE id='b1000000000000000000000000000054';
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000061';
|
||||
UPDATE sys_permission SET name='上传' WHERE id='b1000000000000000000000000000062';
|
||||
UPDATE sys_permission SET name='编辑' WHERE id='b1000000000000000000000000000063';
|
||||
UPDATE sys_permission SET name='启停' WHERE id='b1000000000000000000000000000064';
|
||||
UPDATE sys_permission SET name='查看' WHERE id='b1000000000000000000000000000071';
|
||||
UPDATE sys_permission SET name='编辑' WHERE id='b1000000000000000000000000000072';
|
||||
|
||||
-- 4) 补旧后台有、当前缺的按钮
|
||||
INSERT IGNORE INTO sys_permission
|
||||
(id,parent_id,name,is_route,menu_type,perms,perms_type,sort_no,always_show,is_leaf,keep_alive,hidden,hide_tab,create_by,create_time,del_flag,rule_flag,status,internal_or_external)
|
||||
VALUES
|
||||
-- 分析看板:导出 / 更新数据
|
||||
('b1000000000000000000000000000022','b1000000000000000000000000000002','导出',0,2,'inquiry:dashboard:export','1',2,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
('b1000000000000000000000000000023','b1000000000000000000000000000002','更新数据',0,2,'inquiry:dashboard:refresh','1',3,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
-- 询价工单:导出 / 查看详情
|
||||
('b1000000000000000000000000000033','b1000000000000000000000000000003','导出',0,2,'inquiry:workorder:export','1',2,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
('b1000000000000000000000000000034','b1000000000000000000000000000003','查看详情',0,2,'inquiry:workorder:detail','1',3,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
-- 账号管理
|
||||
('b1000000000000000000000000000081','b1000000000000000000000000000011','查看',0,2,'inquiry:account:view','1',1,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
('b1000000000000000000000000000082','b1000000000000000000000000000011','编辑',0,2,'inquiry:account:edit','1',2,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
-- 角色管理
|
||||
('b1000000000000000000000000000083','b1000000000000000000000000000012','查看',0,2,'inquiry:role:view','1',1,0,1,0,0,0,'admin',NOW(),0,0,'1',0),
|
||||
('b1000000000000000000000000000084','b1000000000000000000000000000012','编辑',0,2,'inquiry:role:edit','1',2,0,1,0,0,0,'admin',NOW(),0,0,'1',0);
|
||||
|
||||
-- 关闭工单排序靠后(详情后)
|
||||
UPDATE sys_permission SET sort_no = 4 WHERE id = 'b1000000000000000000000000000032';
|
||||
|
||||
-- 5) admin 角色补授权全部 b100 权限
|
||||
INSERT IGNORE INTO sys_role_permission (id, role_id, permission_id)
|
||||
SELECT MD5(CONCAT('ytd-inq-menu:', r.id, ':', p.id)), r.id, p.id
|
||||
FROM sys_role r
|
||||
JOIN sys_permission p ON p.id LIKE 'b100%' AND p.del_flag = 0
|
||||
WHERE r.role_code = 'admin';
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
-- 账号「最后登录时间」:列表展示真实登录,不再误用 createTime。
|
||||
-- 库:inquiry_robot;幂等。
|
||||
|
||||
-- 1) 列
|
||||
SET @has_col := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_user'
|
||||
AND column_name = 'last_login_time'
|
||||
);
|
||||
SET @sql := IF(@has_col = 0,
|
||||
'ALTER TABLE sys_user ADD COLUMN last_login_time datetime NULL DEFAULT NULL COMMENT ''最后登录时间'' AFTER update_time',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2) 用登录日志回填(log_type=1);已有值不覆盖
|
||||
UPDATE sys_user u
|
||||
INNER JOIN (
|
||||
SELECT COALESCE(NULLIF(userid, ''), username) AS uname, MAX(create_time) AS last_t
|
||||
FROM sys_log
|
||||
WHERE log_type = 1
|
||||
AND (userid IS NOT NULL OR username IS NOT NULL)
|
||||
GROUP BY COALESCE(NULLIF(userid, ''), username)
|
||||
) x ON x.uname = u.username
|
||||
SET u.last_login_time = x.last_t
|
||||
WHERE u.last_login_time IS NULL
|
||||
AND x.last_t IS NOT NULL;
|
||||
Reference in New Issue
Block a user