陆运选项不全时改为复制填空清单,方便销售按编号回填。

停发填写样例图,同群只跟当前工单,重量体积统一为 KG/CBM。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 17:10:13 +08:00
co-authored by Cursor
parent 2334524bb8
commit 378bb892b3
33 changed files with 1204 additions and 265 deletions
@@ -814,8 +814,8 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
out.put("container_type_qty", first(src, "箱型箱量", "container_type_qty"));
out.put("pieces", first(src, "件数", "pieces"));
// 销售常写「重量」,合同键是毛重;两边都认
out.put("weight_kg", first(src, "毛重", "重量", "weight_kg"));
out.put("volume_cbm", first(src, "体积", "volume_cbm", "volume"));
out.put("weight_kg", first(src, "毛重", "重量", "重量(KG)", "weight_kg"));
out.put("volume_cbm", first(src, "体积", "体积(CBM)", "volume_cbm", "volume"));
// 去掉单位后缀,避免模板「KG/CBM」列旁再写一遍单位
String w = out.getString("weight_kg");
if (StringUtils.isNotBlank(w)) {
@@ -825,7 +825,9 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
if (StringUtils.isNotBlank(vol)) {
out.put("volume_cbm", vol.replaceAll("(?i)\\s*(CBM|立方米|方)\\s*$", "").trim());
}
out.put("quote_date", first(src, "报价日期", "quote_date", "询价日期"));
// 询价/走货/发货/出货/提货的日期或时间,都是报价日期
out.put("quote_date", first(src, "报价日期", "quote_date", "询价日期", "走货日期", "询价时间",
"走货时间", "发货时间", "发货日期", "出货日期", "出货时间", "提货时间", "提货日期"));
out.put("trade_terms", first(src, "贸易条款", "trade_terms"));
out.put("运输分类", first(src, "运输分类"));
out.put("cargo_ready_date", first(src, "货好时间", "cargo_ready_date"));
@@ -833,6 +835,16 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
String oil = first(src, "是否含油", "has_oil");
String elec = first(src, "是否含电", "has_battery");
String mag = first(src, "是否含磁", "has_magnet");
// 空运品名正好是普货:报价单含电、含磁空着或只写横杠时默认否。海运陆运不套这条。
String cargoName = StringUtils.trimToEmpty(first(src, "品名", "货物品名", "commodity"));
if (looksAirInquiry(src, q, businessLine) && "普货".equals(cargoName)) {
if (collabBlank(elec)) {
elec = "否";
}
if (collabBlank(mag)) {
mag = "否";
}
}
String oem = first(src, "oil_elec_mag", "是否含油,电,磁");
if (StringUtils.isBlank(oem) && (StringUtils.isNotBlank(oil) || StringUtils.isNotBlank(elec) || StringUtils.isNotBlank(mag))) {
oem = "油:" + (StringUtils.isNotBlank(oil) ? oil : "-")
@@ -1059,6 +1071,12 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
return "";
}
/** 协同项没写,或只写了横杠。横杠是空占位,不能当成已经填过。 */
private static boolean collabBlank(String raw) {
String val = StringUtils.trimToEmpty(raw);
return val.isEmpty() || "-".equals(val) || "—".equals(val) || "-".equals(val);
}
/**
* 空运销售面没有时效:出单不把 eta / transit_* 填进模板。
* 业务线 AIR / 运输方式含空运 / 报价带 airOptions 都算空运。
@@ -822,11 +822,11 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("ok", true);
out.put("status", status);
out.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
String salesWecomId = StringUtils.defaultString(ticket.getSalesWecomId());
out.put("salesWecomId", salesWecomId);
out.put("sales_wecom_id", salesWecomId);
String ticketSalesWecomId = StringUtils.defaultString(ticket.getSalesWecomId());
out.put("salesWecomId", ticketSalesWecomId);
out.put("sales_wecom_id", ticketSalesWecomId);
String salesName = InquiryTicketDisplaySupport.resolveSalesDisplayName(
ticket.getSalesName(), salesWecomId, resolveStaffDisplayName(salesWecomId));
ticket.getSalesName(), ticketSalesWecomId, resolveStaffDisplayName(ticketSalesWecomId));
out.put("sales_name", salesName);
out.put("salesName", salesName);
out.put("facts", facts);
@@ -983,7 +983,9 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
/**
* 记下海运协同群 chat_id 与成员。不改六态、不写报价版本。
* 记下协同群 chat_id 与成员。不改六态、不写报价版本。
* <p>
* 同一群只留这一张单,陆运/海运/空运旧绑定一并摘掉。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -1000,6 +1002,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
ticket.setCollabChatId(chatId);
ticket.setCollabMemberIds(memberIdsJson(body));
ticket.setUpdateTime(new Date());
unbindOtherTicketsInRoom(chatId, no);
updateById(ticket);
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "bind_collab_group", "agent", chatId);
out.put("ok", true);
@@ -1064,6 +1067,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
/**
* 按协同群找回工单。群里补字段用。只读。
* <p>
* 同群若仍有多张单,取最近更新的一张,避免 LIMIT 1 无排序拿到最早的陆运旧单。
*/
@Override
public Map<String, Object> findByCollabChat(String chatId) {
@@ -1075,6 +1080,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
InquiryTicket ticket = getOne(new LambdaQueryWrapper<InquiryTicket>()
.eq(InquiryTicket::getCollabChatId, id)
.orderByDesc(InquiryTicket::getUpdateTime)
.last("LIMIT 1"), false);
if (ticket == null) {
return out;
@@ -1241,7 +1247,10 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
/**
* 空运群当前工单:清掉同群其它空运单绑定,一单一群。不改六态。
* 空运群当前工单:清掉同群其它工单绑定,一单一群。不改六态。
* <p>
* 陆运/海运旧单也要摘掉。测服 WO202609230023 激活后补协同,
* 按群找回仍拿到同群陆运 WO202609230007,摘要字段全空。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -1260,20 +1269,10 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
return out;
}
List<InquiryTicket> others = list(new LambdaQueryWrapper<InquiryTicket>()
.eq(InquiryTicket::getCollabChatId, room)
.ne(InquiryTicket::getWorkOrderNo, no));
if (others != null) {
for (InquiryTicket other : others) {
if (!"AIR".equalsIgnoreCase(StringUtils.trimToEmpty(other.getBusinessLine()))) {
continue;
}
other.setCollabChatId("");
other.setUpdateTime(new Date());
updateById(other);
}
}
unbindOtherTicketsInRoom(room, no);
ticket.setCollabChatId(room);
// 品名是普货:激活时就把含电、含磁写成否,群摘要不再显示横杠。
seedAirGeneralCargoCollab(ticket);
ticket.setUpdateTime(new Date());
updateById(ticket);
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "activate_air_group", "agent", room);
@@ -1288,9 +1287,79 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("facts", parseFactsJson(ticket.getFactsJson()));
out.put("quote", latestQuotePayload(no));
out.put("collab_chat_id", room);
out.put("collab_facts", parseFactsJson(ticket.getCollabFactsJson()));
return out;
}
/**
* 同一协同群只留 keep 这一张单。陆运、海运、空运旧绑定都清。
*/
private void unbindOtherTicketsInRoom(String room, String keepWorkOrderNo) {
String chat = StringUtils.trimToEmpty(room);
String keep = StringUtils.trimToEmpty(keepWorkOrderNo);
if (StringUtils.isBlank(chat)) {
return;
}
List<InquiryTicket> others = list(new LambdaQueryWrapper<InquiryTicket>()
.eq(InquiryTicket::getCollabChatId, chat)
.ne(InquiryTicket::getWorkOrderNo, keep));
if (others == null) {
return;
}
Date now = new Date();
for (InquiryTicket other : others) {
other.setCollabChatId("");
other.setUpdateTime(now);
updateById(other);
}
}
/**
* 空运品名正好是「普货」时,含电、含磁还空着就写成否。
* <p>
* 已经写了「是」或「否」的不改。摘要里的横杠当成没写。
* 只改传入工单上的协同 JSON,由调用方落库。海运、陆运不进这里。
*/
private void seedAirGeneralCargoCollab(InquiryTicket ticket) {
if (ticket == null || !"AIR".equalsIgnoreCase(StringUtils.trimToEmpty(ticket.getBusinessLine()))) {
return;
}
String name = StringUtils.trimToEmpty(ticket.getGoodsName());
if (!"普货".equals(name)) {
Map<String, String> facts = parseFactsJson(ticket.getFactsJson());
name = StringUtils.trimToEmpty(facts.get("品名"));
if (!"普货".equals(name)) {
name = StringUtils.trimToEmpty(facts.get("货物品名"));
}
}
if (!"普货".equals(name)) {
return;
}
JSONObject collab = new JSONObject(true);
if (StringUtils.isNotBlank(ticket.getCollabFactsJson())) {
JSONObject parsed = JSON.parseObject(ticket.getCollabFactsJson());
if (parsed != null) {
collab.putAll(parsed);
}
}
boolean changed = false;
for (String key : new String[]{"是否含电", "是否含磁"}) {
if (collabBlank(collab.getString(key))) {
collab.put(key, "否");
changed = true;
}
}
if (changed) {
ticket.setCollabFactsJson(JSON.toJSONString(collab));
}
}
/** 协同项没写,或只写了横杠。 */
private static boolean collabBlank(String raw) {
String val = StringUtils.trimToEmpty(raw);
return val.isEmpty() || "-".equals(val) || "—".equals(val) || "-".equals(val);
}
private static Map<String, String> parseFactsJson(String raw) {
Map<String, String> facts = new LinkedHashMap<>();
if (StringUtils.isBlank(raw)) {
@@ -45,20 +45,20 @@ public final class InquiryTicketDisplaySupport {
List<String> quantities = new ArrayList<>();
if ("AIR".equals(businessLine) || containsChinese(lineLabel, "空运")) {
addLabel(quantities, "件数", ticket.getPieces());
addUnit(quantities, "毛重", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
addUnit(quantities, "重量(KG)", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积(CBM)", ticket.getVolumeCbm(), "CBM");
} else if ("SEA".equals(businessLine) || containsChinese(lineLabel, "海运")) {
addLabel(quantities, "件数", ticket.getPieces());
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
addUnit(quantities, "重量(KG)", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积(CBM)", ticket.getVolumeCbm(), "CBM");
} else if ("LAND".equals(businessLine) || containsChinese(lineLabel, "陆运")) {
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积(CBM)", ticket.getVolumeCbm(), "CBM");
addUnit(quantities, "重量(KG)", ticket.getWeightKg(), "kg");
addLabel(quantities, "件数", ticket.getPieces());
} else {
addLabel(quantities, "件数", ticket.getPieces());
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
addUnit(quantities, "重量(KG)", ticket.getWeightKg(), "kg");
addUnit(quantities, "体积(CBM)", ticket.getVolumeCbm(), "CBM");
}
addLine(lines, join(quantities, " · "));
@@ -41,6 +41,16 @@ public final class QuoteTemplateExcelSupport {
static {
FILL_BY_LABEL.put("询价日期", "quote_date");
FILL_BY_LABEL.put("报价日期", "quote_date");
// 销售口中的报价日期。货好时间、提货地址不在这里。
FILL_BY_LABEL.put("走货日期", "quote_date");
FILL_BY_LABEL.put("询价时间", "quote_date");
FILL_BY_LABEL.put("走货时间", "quote_date");
FILL_BY_LABEL.put("发货时间", "quote_date");
FILL_BY_LABEL.put("发货日期", "quote_date");
FILL_BY_LABEL.put("出货日期", "quote_date");
FILL_BY_LABEL.put("出货时间", "quote_date");
FILL_BY_LABEL.put("提货时间", "quote_date");
FILL_BY_LABEL.put("提货日期", "quote_date");
FILL_BY_LABEL.put("品名", "commodity");
FILL_BY_LABEL.put("货物数量", "quantity");
FILL_BY_LABEL.put("整柜或拼柜", "fcl_or_lcl");
@@ -70,7 +80,9 @@ public final class QuoteTemplateExcelSupport {
FILL_BY_LABEL.put("目的地", "destination");
FILL_BY_LABEL.put("件数", "pieces");
FILL_BY_LABEL.put("重量", "weight_kg");
FILL_BY_LABEL.put("重量(KG)", "weight_kg");
FILL_BY_LABEL.put("体积", "volume_cbm");
FILL_BY_LABEL.put("体积(CBM)", "volume_cbm");
FILL_BY_LABEL.put("始发地", "origin");
FILL_BY_LABEL.put("始发站", "origin");
FILL_BY_LABEL.put("目的地", "destination");
@@ -530,8 +542,8 @@ public final class QuoteTemplateExcelSupport {
m.put("trade_terms", "贸易条款");
m.put("cargo_ready_date", "货好时间");
m.put("pieces", "件数");
m.put("weight_kg", "重量");
m.put("volume_cbm", "体积");
m.put("weight_kg", "重量(KG)");
m.put("volume_cbm", "体积(CBM)");
m.put("total_amount", "报价合计");
return m;
}
@@ -96,8 +96,8 @@
{ "code": "trade_terms", "label": "贸易条款", "requiredHint": false },
{ "code": "cargo_ready_date", "label": "货好时间", "requiredHint": false },
{ "code": "pieces", "label": "件数", "requiredHint": false },
{ "code": "weight_kg", "label": "重量", "requiredHint": false },
{ "code": "volume_cbm", "label": "体积", "requiredHint": false },
{ "code": "weight_kg", "label": "重量(KG)", "requiredHint": false },
{ "code": "volume_cbm", "label": "体积(CBM)", "requiredHint": false },
{ "code": "chargeable_weight_kg", "label": "计费重", "requiredHint": false },
{ "code": "delivery_address", "label": "派送地址", "requiredHint": false },
{ "code": "pickup_address", "label": "提货地址", "requiredHint": false },
@@ -131,6 +131,15 @@
"报价编号": "quote_no",
"报价日期": "quote_date",
"询价日期": "quote_date",
"走货日期": "quote_date",
"询价时间": "quote_date",
"走货时间": "quote_date",
"发货时间": "quote_date",
"发货日期": "quote_date",
"出货日期": "quote_date",
"出货时间": "quote_date",
"提货时间": "quote_date",
"提货日期": "quote_date",
"有效期至": "valid_until",
"报价有效期": "valid_until",
"币别": "currency",
@@ -152,8 +161,10 @@
"件数": "pieces",
"件 数": "pieces",
"重量": "weight_kg",
"重量(KG)": "weight_kg",
"重 量": "weight_kg",
"体积": "volume_cbm",
"体积(CBM)": "volume_cbm",
"体 积": "volume_cbm",
"费用总计": "total_amount",
"报价合计": "total_amount",
@@ -30,12 +30,12 @@ public class InquiryTicketDisplaySupportTest {
List<String> lines = InquiryTicketDisplaySupport.requirementSummaryLines(ticket);
assertEquals("空运 · ZUH → CRK", lines.get(0));
assertEquals("件数 10 · 毛重 1kg · 体积 8CBM", lines.get(1));
assertEquals("件数 10 · 重量(KG) 1kg · 体积(CBM) 8CBM", lines.get(1));
assertEquals("品名 茶叶", lines.get(2));
InquiryTicketDisplaySupport.applyRequirementSummary(ticket);
assertEquals("空运 · ZUH → CRK", ticket.getDemandTitle());
assertEquals("件数 10 · 毛重 1kg · 体积 8CBM\n品名 茶叶", ticket.getDemandDetail());
assertEquals("件数 10 · 重量(KG) 1kg · 体积(CBM) 8CBM\n品名 茶叶", ticket.getDemandDetail());
}
@Test
@@ -112,6 +112,22 @@ public class QuoteTemplateFillTest {
assertTrue(StringUtils.isBlank(out.getString("时效")));
}
@Test
public void buildFillFactsAirGeneralCargoDefaultsBatteryAndMagnet() {
Map<String, Object> facts = new HashMap<>();
facts.put("品名", "普货");
facts.put("运输方式", "空运");
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
.buildFillFacts("WO202609230017", facts, new HashMap<>(), "WuJiLin", "AIR");
assertEquals("否", out.getString("是否含电"));
assertEquals("否", out.getString("是否含磁"));
facts.put("是否含电", "是");
JSONObject kept = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
.buildFillFacts("WO202609230017", facts, new HashMap<>(), "WuJiLin", "AIR");
assertEquals("是", kept.getString("是否含电"));
assertEquals("否", kept.getString("是否含磁"));
}
@Test
public void writeFeeDetailColumnsOntoNamedFeeRow() throws Exception {
Path src = Files.createTempFile("quote-fee-src", ".xlsx");