完善报价单填表与币种回填,并补齐账号末次登录、角色按钮权限与陆运全线匹配。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+7
@@ -138,4 +138,11 @@ public class LoginUser {
|
||||
*/
|
||||
private Boolean dashboardSelfOnly;
|
||||
|
||||
/**
|
||||
* 最后登录时间(账号列表展示;登录成功写入)。
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date lastLoginTime;
|
||||
|
||||
}
|
||||
|
||||
+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;
|
||||
+9
@@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* 陆运拉群人员匹配:不连库。
|
||||
* <p>
|
||||
* 只认产品·陆运启用岗;负责线路按逗号整词对线路类别,禁止按斜杠切开「中港/中亚/中欧」。
|
||||
* 「陆运全线」命中任意线路类别。
|
||||
*/
|
||||
public class LandStaffMatcherTest {
|
||||
|
||||
@@ -37,6 +38,14 @@ public class LandStaffMatcherTest {
|
||||
assertEquals(0, LandStaffMatcher.match(Arrays.asList(row), "东南亚").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landAllRoutesHitsAnyCategory() {
|
||||
InquiryStaffRole all = staff("land", "active", "z", "全线岗", "陆运全线");
|
||||
assertEquals(1, LandStaffMatcher.match(Arrays.asList(all), "东南亚").size());
|
||||
assertEquals(1, LandStaffMatcher.match(Arrays.asList(all), "中港/中亚/中欧").size());
|
||||
assertEquals(1, LandStaffMatcher.match(Arrays.asList(all), "国内长途/零担").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoreSeaRole() {
|
||||
InquiryStaffRole sea = staff("sea", "active", "s", "海", "东南亚");
|
||||
|
||||
+6
@@ -26,6 +26,12 @@ public class SeaStaffMatcherTest {
|
||||
assertEquals("p1", hits.get(0).get("wecomId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaAllLineAliasPulls() {
|
||||
InquiryStaffRole row = staff("sea", "active", "p1b", "全线产品", "海运全线");
|
||||
assertEquals(1, SeaStaffMatcher.match(Arrays.asList(row), "上海", "洛杉矶").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void specificPortPullsWhenMatch() {
|
||||
InquiryStaffRole row = staff("sea", "active", "p2", "李产品", "上海,宁波");
|
||||
|
||||
+174
-8
@@ -132,7 +132,7 @@ public class QuoteTemplateFillTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaFeeRemarkSplitsAndWidensColumn() throws Exception {
|
||||
public void seaFeeRemarkStripsInternalMarkup() throws Exception {
|
||||
Path src = Files.createTempFile("quote-remark-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-remark-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
@@ -156,13 +156,9 @@ public class QuoteTemplateFillTest {
|
||||
try (Workbook wb = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = wb.getSheetAt(0);
|
||||
org.apache.poi.ss.usermodel.Row row = sheet.getRow(18);
|
||||
String remark = row.getCell(8).getStringCellValue();
|
||||
assertTrue(remark.contains("计价单位:柜"));
|
||||
assertTrue(remark.contains("加点系数:0.0000000"));
|
||||
assertTrue(remark.contains("\n"));
|
||||
assertTrue(row.getCell(8).getCellStyle().getWrapText());
|
||||
assertTrue(sheet.getColumnWidth(8) >= 18 * 256);
|
||||
assertTrue(row.getHeightInPoints() >= 30f);
|
||||
String remark = row.getCell(8) == null ? "" : row.getCell(8).getStringCellValue();
|
||||
assertEquals("", remark == null ? "" : remark.trim());
|
||||
assertEquals(15.0, row.getCell(7).getNumericCellValue(), 0.01);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
@@ -591,6 +587,176 @@ public class QuoteTemplateFillTest {
|
||||
assertEquals(2, out.getJSONArray("fee_rows").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildFillFactsProjectsLandRouteFromFeeLinesWhenNoSegments() {
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
facts.put("起运港", "泉州市");
|
||||
facts.put("目的港", "南京市");
|
||||
facts.put("运输类型", "国内运输拼车");
|
||||
Map<String, Object> quote = new HashMap<>();
|
||||
quote.put("total", "110.00");
|
||||
quote.put("currency", "CNY");
|
||||
quote.put("eta", "48天");
|
||||
quote.put("fee_lines", "[{\"name\":\"运输费\",\"amount\":\"110.00\",\"currency\":\"CNY\",\"remark\":\"票结\"}]");
|
||||
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||
.buildFillFacts("WO202609190017", facts, quote, "LinMianXun");
|
||||
assertEquals("泉州市", out.getString("origin"));
|
||||
assertEquals("南京市", out.getString("destination"));
|
||||
assertEquals("48天", out.getString("transit_time"));
|
||||
assertTrue(out.getJSONArray("route_rows") != null && !out.getJSONArray("route_rows").isEmpty());
|
||||
assertEquals("110.00", out.getJSONArray("route_rows").getJSONObject(0).getString("salePrice"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 陆运拼车:H5「报价日期:」常被标进 fixedCells;同格冒号后必须仍能写出日期。
|
||||
* 与有效期误绑同一格时,只写报价日期,不让 TMS valid_until 盖掉标签含义。
|
||||
*/
|
||||
@Test
|
||||
public void quoteDateWritesAfterColonEvenWhenFixedYellow() throws Exception {
|
||||
Path src = Files.createTempFile("quote-date-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-date-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("国内运输拼车报价模板");
|
||||
row(sheet, 4).createCell(7).setCellValue("报价日期:");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
JSONObject fields = new JSONObject(true);
|
||||
fields.put("valid_until", "国内运输拼车报价模板!H5");
|
||||
fields.put("quote_date", "国内运输拼车报价模板!H5");
|
||||
mapping.put("fields", fields);
|
||||
JSONObject meta = new JSONObject(true);
|
||||
JSONObject after = new JSONObject(true);
|
||||
after.put("sameCellAfterColon", true);
|
||||
meta.put("quote_date", after);
|
||||
meta.put("valid_until", after);
|
||||
mapping.put("fieldMeta", meta);
|
||||
mapping.put("fixedCells", JSONArray.parseArray("[\"国内运输拼车报价模板!H5\"]"));
|
||||
mapping.put("fixedYellowCells", JSONArray.parseArray("[\"国内运输拼车报价模板!H5\"]"));
|
||||
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("quote_date", "2026-09-19");
|
||||
facts.put("valid_until", "2026-04-30");
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
assertEquals("报价日期:2026-09-19", read(filled.getSheetAt(0), 4, 7));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品口径:单元格标黄 = 固定占位,出单不赋值。
|
||||
* 陆运「项目二」整段黄底时,即使 mapping 配了 feeRegions,也不得改写模板占位行。
|
||||
*/
|
||||
@Test
|
||||
public void yellowFeeRegionRowsAreNotOverwritten() throws Exception {
|
||||
Path src = Files.createTempFile("quote-yellow-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-yellow-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("国内运输拼车报价模板");
|
||||
row(sheet, 11).createCell(1).setCellValue("项目二:其它费用");
|
||||
org.apache.poi.ss.usermodel.CellStyle yellow = wb.createCellStyle();
|
||||
yellow.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.YELLOW.getIndex());
|
||||
yellow.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
||||
org.apache.poi.ss.usermodel.Row data = row(sheet, 13);
|
||||
org.apache.poi.ss.usermodel.Cell cName = data.createCell(2);
|
||||
cName.setCellValue("打包费");
|
||||
cName.setCellStyle(yellow);
|
||||
org.apache.poi.ss.usermodel.Cell cAmt = data.createCell(7);
|
||||
cAmt.setCellValue("明确收费");
|
||||
cAmt.setCellStyle(yellow);
|
||||
row(sheet, 18).createCell(1).setCellValue("交易条件说明");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("fields", new JSONObject());
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject region = new JSONObject(true);
|
||||
region.put("id", "other_charges");
|
||||
region.put("title", "项目二:其它费用");
|
||||
region.put("anchor", "国内运输拼车报价模板!B14");
|
||||
region.put("stopAtText", JSONArray.parseArray("[\"交易条件说明\"]"));
|
||||
region.put("grow", "insert_before_stop");
|
||||
JSONObject columns = new JSONObject(true);
|
||||
columns.put("name", "C");
|
||||
columns.put("amount", "H");
|
||||
region.put("columns", columns);
|
||||
regions.add(region);
|
||||
mapping.put("feeRegions", regions);
|
||||
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"运输费\",\"amount\":\"110.00\",\"unit\":\"票数\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertEquals("打包费", read(sheet, 13, 2));
|
||||
assertEquals("明确收费", read(sheet, 13, 7));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaDynamicContainerHeaderReplacedWithBoxType() throws Exception {
|
||||
Path src = Files.createTempFile("quote-box-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-box-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("海运询价单");
|
||||
row(sheet, 16).createCell(0).setCellValue("始发港费用/Origin Charges");
|
||||
org.apache.poi.ss.usermodel.Row head = row(sheet, 17);
|
||||
head.createCell(0).setCellValue("项目/Items");
|
||||
head.createCell(2).setCellValue("箱型\n(动态填充箱型)");
|
||||
head.createCell(7).setCellValue("金额/Ammount");
|
||||
row(sheet, 18).createCell(0).setCellValue("操作费");
|
||||
row(sheet, 27).createCell(0).setCellValue("始发港费用汇总/POL Local Charges");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
mapping.put("sheet", "海运询价单");
|
||||
mapping.put("fields", new JSONObject());
|
||||
JSONArray regions = new JSONArray();
|
||||
JSONObject region = new JSONObject(true);
|
||||
region.put("id", "origin_charges");
|
||||
region.put("title", "始发港费用/Origin Charges");
|
||||
region.put("tmsSource", "polCostItems");
|
||||
region.put("headerRow", 18);
|
||||
region.put("anchor", "海运询价单!A19");
|
||||
region.put("dynamicContainerHeader", true);
|
||||
region.put("stopAtText", JSONArray.parseArray("[\"始发港费用汇总\"]"));
|
||||
JSONObject columns = new JSONObject(true);
|
||||
columns.put("name", "A");
|
||||
columns.put("container_rate", "C");
|
||||
columns.put("amount", "H");
|
||||
region.put("columns", columns);
|
||||
regions.add(region);
|
||||
mapping.put("feeRegions", regions);
|
||||
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("container_type_qty", "20GP*1");
|
||||
facts.put("fee_rows_polCostItems", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"操作费\",\"amount\":\"680\",\"currency\":\"CNY\",\"unit_price\":\"680\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
String head = read(filled.getSheetAt(0), 17, 2);
|
||||
assertTrue(head.contains("20GP"), "header=" + head);
|
||||
assertTrue(head.contains("(") && head.contains(")"), "header missing parens: " + head);
|
||||
assertTrue(!head.contains("动态填充"), "header still placeholder: " + head);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
private static org.apache.poi.ss.usermodel.Row row(Sheet sheet, int r) {
|
||||
org.apache.poi.ss.usermodel.Row exist = sheet.getRow(r);
|
||||
return exist == null ? sheet.createRow(r) : exist;
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package org.jeecg.modules.inquiry.template;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* RFQ 纠偏、重新解析智能合并、备注过滤。
|
||||
* <p>
|
||||
* 产品口径:B3 已有单选等内容时,编号写在 A3「RFQ No:」冒号后,不覆盖 B3、不跳到 F3。
|
||||
*/
|
||||
public class QuoteTemplateMappingMergeTest {
|
||||
|
||||
@Test
|
||||
public void b3RadioIsNotAlignedQuoteNo() throws Exception {
|
||||
try (Workbook wb = seaHeaderWorkbook()) {
|
||||
assertFalse(QuoteTemplateExcelSupport.fieldMappingStillAligned(wb, "quote_no", "海运询价单!B3"));
|
||||
assertTrue(QuoteTemplateExcelSupport.fieldMappingStillAligned(wb, "quote_no", "海运询价单!A3"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void relocateQuoteNoSkipsRadioToLabelA3() throws Exception {
|
||||
try (Workbook wb = seaHeaderWorkbook()) {
|
||||
String better = QuoteTemplateExcelSupport.relocateQuoteNoCell(wb, "海运询价单!B3");
|
||||
assertEquals("海运询价单!A3", better);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeReplacesB3WithA3() throws Exception {
|
||||
try (Workbook wb = seaHeaderWorkbook()) {
|
||||
JSONObject old = fields("quote_no", "海运询价单!B3");
|
||||
JSONObject neu = fields("quote_no", "海运询价单!B3");
|
||||
JSONObject merged = InquiryQuoteTemplateServiceImpl.mergeMappingPreferExisting(old, neu, wb);
|
||||
assertEquals("海运询价单!A3", merged.getJSONObject("fields").getString("quote_no"));
|
||||
assertTrue(merged.getJSONObject("fieldMeta").getJSONObject("quote_no")
|
||||
.getBooleanValue("sameCellAfterColon"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fillWritesQuoteNoAfterColonOnA3() throws Exception {
|
||||
Path src = Files.createTempFile("quote-rfq-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-rfq-out", ".xlsx");
|
||||
try (Workbook wb = seaHeaderWorkbook()) {
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
JSONObject f = new JSONObject(true);
|
||||
f.put("quote_no", "海运询价单!B3");
|
||||
mapping.put("fields", f);
|
||||
// A3 在固定区也应能冒号后写入
|
||||
mapping.put("fixedCells", com.alibaba.fastjson.JSONArray.parseArray("[\"海运询价单!A3\"]"));
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("quote_no", "WO202609190002");
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook wb = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = wb.getSheet("海运询价单");
|
||||
assertEquals("RFQ No:WO202609190002", sheet.getRow(2).getCell(0).getStringCellValue());
|
||||
// B3 单选不得被工单号覆盖
|
||||
assertEquals("○ Port to Port\n○ 其它,3工作天回复",
|
||||
sheet.getRow(2).getCell(1).getStringCellValue().replace("\r\n", "\n"));
|
||||
// F3 不应被误写
|
||||
assertTrue(sheet.getRow(2).getCell(5) == null
|
||||
|| org.apache.commons.lang3.StringUtils.isBlank(sheet.getRow(2).getCell(5).getStringCellValue()));
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formatFeeRemarkDropsMarkup() {
|
||||
assertEquals("", QuoteTemplateExcelSupport.formatFeeRemark("计价单位:柜;加点系数:0.0000000"));
|
||||
assertEquals("紧急出运", QuoteTemplateExcelSupport.formatFeeRemark("紧急出运;加点系数:0.1"));
|
||||
}
|
||||
|
||||
private static JSONObject fields(String code, String cell) {
|
||||
JSONObject mapping = new JSONObject(true);
|
||||
JSONObject f = new JSONObject(true);
|
||||
f.put(code, cell);
|
||||
mapping.put("fields", f);
|
||||
mapping.put("feeRegions", new com.alibaba.fastjson.JSONArray());
|
||||
mapping.put("routeRegions", new com.alibaba.fastjson.JSONArray());
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迷你海运表头:A3=RFQ No:,B3=单选,F3=空(旧逻辑曾误写这里)。
|
||||
*/
|
||||
private static Workbook seaHeaderWorkbook() {
|
||||
Workbook wb = new XSSFWorkbook();
|
||||
Sheet sheet = wb.createSheet("海运询价单");
|
||||
Row row = sheet.createRow(2);
|
||||
row.createCell(0).setCellValue("RFQ No:");
|
||||
row.createCell(1).setCellValue("○ Port to Port\n○ 其它,3工作天回复");
|
||||
row.createCell(5).setCellValue("");
|
||||
return wb;
|
||||
}
|
||||
}
|
||||
+60
-12
@@ -2,11 +2,14 @@ package org.jeecg.modules.inquiry.tms;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -114,24 +117,69 @@ public class TmsQuoteCurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toAgentQuoteKeepsRealAirLockIds() {
|
||||
Map<String, Object> option = new HashMap<>();
|
||||
option.put("optionNo", "AIR-OPT01");
|
||||
option.put("originAirportCode", "ZUH");
|
||||
option.put("destinationAirportCode", "CRK");
|
||||
public void toAgentQuoteSplitsSeaFeeBuckets() {
|
||||
Map<String, Object> pol = new HashMap<>();
|
||||
pol.put("feeName", "操作费");
|
||||
pol.put("amount", 680);
|
||||
pol.put("currency", "CNY");
|
||||
Map<String, Object> ocean = new HashMap<>();
|
||||
ocean.put("feeName", "海运费");
|
||||
ocean.put("amount", 100);
|
||||
ocean.put("currency", "USD");
|
||||
Map<String, Object> pod = new HashMap<>();
|
||||
pod.put("feeName", "文件费");
|
||||
pod.put("amount", 450);
|
||||
pod.put("currency", "CNY");
|
||||
List<Map<String, Object>> feeItems = new ArrayList<>();
|
||||
feeItems.add(Collections.singletonMap("polCostItems", Collections.singletonList(pol)));
|
||||
feeItems.add(Collections.singletonMap("oceanCostItems", Collections.singletonList(ocean)));
|
||||
feeItems.add(Collections.singletonMap("podCostItems", Collections.singletonList(pod)));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("quoteFound", true);
|
||||
data.put("quoteId", "AIR-2094721692181016578-2094721692654972929");
|
||||
data.put("totalSalePrice", "11792.00");
|
||||
data.put("totalSalePrice", "1230");
|
||||
data.put("currency", "CNY");
|
||||
data.put("chargeableWeightKg", 1072);
|
||||
data.put("airOptions", Collections.singletonList(option));
|
||||
data.put("feeItems", feeItems);
|
||||
Map<String, Object> tms = new HashMap<>();
|
||||
tms.put("code", 1);
|
||||
tms.put("tmsData", data);
|
||||
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
|
||||
assertEquals("AIR-2094721692181016578-2094721692654972929", quote.get("quoteId"));
|
||||
assertTrue(quote.get("airOptions").contains("AIR-OPT01"));
|
||||
assertEquals("1072", quote.get("chargeableWeightKg"));
|
||||
assertTrue(quote.get("fee_lines").contains("操作费"));
|
||||
assertTrue(quote.get("fee_lines").contains("海运费"));
|
||||
assertTrue(quote.get("fee_lines").contains("文件费"));
|
||||
assertTrue(quote.get("fee_rows_polCostItems").contains("操作费"));
|
||||
assertFalse(quote.get("fee_rows_polCostItems").contains("海运费"));
|
||||
assertTrue(quote.get("fee_rows_oceanCostItems").contains("海运费"));
|
||||
assertTrue(quote.get("fee_rows_podCostItems").contains("文件费"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toAgentQuoteProjectsLandSegments() {
|
||||
Map<String, Object> seg = new HashMap<>();
|
||||
seg.put("segmentSequence", 1);
|
||||
seg.put("routeLegSequence", 1);
|
||||
seg.put("origin", "泉州市");
|
||||
seg.put("destination", "南京市");
|
||||
seg.put("vehicleType", "3T");
|
||||
seg.put("salePrice", "2800");
|
||||
seg.put("currency", "CNY");
|
||||
seg.put("transitDays", "3");
|
||||
seg.put("remark", "普货");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("quoteFound", true);
|
||||
data.put("totalSalePrice", "2800");
|
||||
data.put("currency", "CNY");
|
||||
data.put("feeItems", Collections.emptyList());
|
||||
data.put("isMulti", 1);
|
||||
data.put("landSegments", Collections.singletonList(seg));
|
||||
Map<String, Object> tms = new HashMap<>();
|
||||
tms.put("code", 1);
|
||||
tms.put("tmsData", data);
|
||||
Map<String, String> quote = InquiryTmsQueryService.toAgentQuote(tms);
|
||||
assertEquals("1", quote.get("isMulti"));
|
||||
assertTrue(quote.get("landSegments").contains("泉州市"));
|
||||
assertTrue(quote.get("route_rows").contains("\"salePrice\":\"2800\""));
|
||||
assertTrue(quote.get("route_rows").contains("\"transit_time\":\"3\""));
|
||||
assertTrue(quote.get("fee_lines").contains("运输费"));
|
||||
assertTrue(quote.get("fee_lines").contains("2800"));
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -4,6 +4,7 @@ import cn.hutool.core.util.RandomUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -465,6 +466,18 @@ public class LoginController {
|
||||
// 获取用户部门信息
|
||||
JSONObject obj = new JSONObject(new LinkedHashMap<>());
|
||||
|
||||
// 询价账号管理:写入最后登录时间,列表不再误用 createTime(种子常为 2019)
|
||||
Date loginAt = new Date();
|
||||
sysUser.setLastLoginTime(loginAt);
|
||||
try {
|
||||
sysUserService.update(
|
||||
new LambdaUpdateWrapper<SysUser>()
|
||||
.eq(SysUser::getId, sysUser.getId())
|
||||
.set(SysUser::getLastLoginTime, loginAt));
|
||||
} catch (Exception e) {
|
||||
log.warn("写入最后登录时间失败 username={}", username, e);
|
||||
}
|
||||
|
||||
//1.生成token
|
||||
String token = JwtUtil.sign(username, syspassword);
|
||||
// 设置token缓存有效时间
|
||||
|
||||
+10
@@ -161,6 +161,16 @@ public class SysUser implements Serializable {
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 最后登录时间。
|
||||
* 账号管理列表展示;登录成功时写入。禁止用 createTime 冒充(种子账号常为 2019)。
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField("last_login_time")
|
||||
private Date lastLoginTime;
|
||||
|
||||
/**
|
||||
* 同步工作流引擎1同步0不同步
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user