补齐超时响应唯一 timeoutCode 与智能体按码查询,并增强报价模板预览与解析落库。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-18 22:25:04 +08:00
co-authored by Cursor
parent 19f94252f0
commit 6cd6803147
15 changed files with 706 additions and 35 deletions
@@ -38,6 +38,9 @@ public class InquiryAgentController {
@Autowired
private org.jeecg.modules.inquiry.service.IInquiryStaffRoleService staffRoleService;
@Autowired
private org.jeecg.modules.inquiry.service.IInquiryResponseTimeoutService responseTimeoutService;
@Autowired
private org.jeecg.modules.inquiry.tms.InquiryTmsCabinService cabinService;
@@ -234,6 +237,36 @@ public class InquiryAgentController {
return Result.OK(staffRoleService.findDisplayByName(firstStr(body, "name")));
}
@ApiOperation("按唯一超时编码取小时数(timeoutCode,如 sea_sales)")
@PostMapping("/responseTimeout/byCode")
public Result<Map<String, Object>> responseTimeoutByCode(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
String code = firstStr(body, "timeoutCode", "timeout_code", "code");
if (StringUtils.isBlank(code)) {
return Result.error("timeoutCode 必填");
}
responseTimeoutService.ensureTimeoutCodes();
org.jeecg.modules.inquiry.entity.InquiryResponseTimeout row =
responseTimeoutService.getByTimeoutCode(code);
Map<String, Object> out = new java.util.LinkedHashMap<>();
if (row == null) {
out.put("found", false);
out.put("timeoutCode", code);
return Result.OK(out);
}
out.put("found", true);
out.put("timeoutCode", row.getTimeoutCode());
out.put("timeoutHours", row.getTimeoutHours());
out.put("transportMode", row.getTransportMode());
out.put("roleCode", row.getRoleCode());
out.put("routeCategory", row.getRouteCategory());
out.put("transportType", row.getTransportType());
return Result.OK(out);
}
@ApiOperation("记下海运协同群(不改六态)")
@PostMapping("/ticket/bindCollabGroup")
public Result<Map<String, Object>> bindCollabGroup(@RequestBody Map<String, Object> body,
@@ -20,9 +20,10 @@ import java.util.List;
import java.util.Map;
/**
* 响应超时配置 API(系统设置页)。
* 响应超时配置 API(后台「超时响应」页)。
* <p>
* 只提供全量列表与改小时数;无 add/delete,维度由 SQL 种子固定。
* 每条有唯一 timeoutCode(如 sea_sales),智能体单码调用;roleCode 仅为角色维度可重复。
* 本接口供管理后台使用;工单超时判定尚未接入。
* </p>
*/
@@ -59,8 +60,9 @@ public class InquiryResponseTimeoutController
@ApiOperation("全量列表(按 sort_no,供系统设置页一次加载)")
@GetMapping("/listAll")
public Result<List<InquiryResponseTimeout>> listAll() {
// 打开系统设置时顺带纠偏旧运输类型文案(幂等,已是新文案则无写)
// 打开超时响应页时顺带纠偏旧运输类型文案、回填唯一编码(幂等)
responseTimeoutService.ensureCanonicalTransportTypeLabels();
responseTimeoutService.ensureTimeoutCodes();
QueryWrapper<InquiryResponseTimeout> qw = new QueryWrapper<>();
qw.orderByAsc("sort_no");
return Result.OK(responseTimeoutService.list(qw));
@@ -18,6 +18,7 @@ import java.util.Date;
* 响应超时配置,对应 inq_response_timeout。
* <p>
* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。
* timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。
* 本表是配置真相;工单超时判定后续再读本表,本期仅 CRUD 小时数。
* </p>
*/
@@ -33,10 +34,13 @@ public class InquiryResponseTimeout implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private String id;
@ApiModelProperty("唯一业务编码(智能体调用),如 sea_sales / land_hk_express")
private String timeoutCode;
@ApiModelProperty("运输方式 AIR/SEA/LAND")
private String transportMode;
@ApiModelProperty("角色 sales/product/air_route")
@ApiModelProperty("角色维度 sales/product/air_route(可重复,非调用主键)")
private String roleCode;
@ApiModelProperty("线路类别(仅陆运产品有值,其余空串)")
@@ -28,4 +28,15 @@ public interface IInquiryResponseTimeoutService extends IService<InquiryResponse
* </p>
*/
void ensureCanonicalTransportTypeLabels();
/**
* 回填每条规则的唯一 timeoutCode(已有则跳过)。
* 智能体只凭 timeoutCode 取超时小时。
*/
void ensureTimeoutCodes();
/**
* 按唯一业务编码查超时配置;无则 null。
*/
InquiryResponseTimeout getByTimeoutCode(String timeoutCode);
}
@@ -132,7 +132,10 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
m.put("objectKey", ver.getObjectKey());
m.put("parseMessage", ver.getParseMessage());
m.put("unmappedRequired", extractUnmapped(ver.getMappingJson()));
m.put("unmappedLabels", extractUnmappedLabels(ver.getMappingJson()));
m.put("mappingGaps", extractMappingGaps(ver.getMappingJson()));
// 列表提示用:合并后的未解析条数(有则运营要点开看)
m.put("unparsedHintCount", countUnparsedHints(ver.getMappingJson()));
} else {
m.put("versionId", null);
m.put("fileName", "");
@@ -1355,14 +1358,14 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
}
/**
* 从 mappingJson 取出智能体对照清单算出的未映射说明(中文)。
* AI 自报「表上标签未落点」原文。
*/
private static List<String> extractMappingGaps(String mappingJson) {
private static List<String> extractUnmappedLabels(String mappingJson) {
if (StringUtils.isBlank(mappingJson)) {
return Collections.emptyList();
}
try {
JSONArray arr = JSON.parseObject(mappingJson).getJSONArray("mappingGaps");
JSONArray arr = JSON.parseObject(mappingJson).getJSONArray("unmappedLabels");
if (arr == null || arr.isEmpty()) {
return Collections.emptyList();
}
@@ -1379,6 +1382,58 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
}
}
/**
* 从 mappingJson 取出智能体对照清单算出的未映射说明(中文)。
* 若 mappingGaps 为空,用 unmappedLabels / unmappedRequired 兜底,保证列表能提示。
*/
private static List<String> extractMappingGaps(String mappingJson) {
if (StringUtils.isBlank(mappingJson)) {
return Collections.emptyList();
}
try {
JSONObject obj = JSON.parseObject(mappingJson);
JSONArray arr = obj.getJSONArray("mappingGaps");
List<String> out = new ArrayList<>();
if (arr != null) {
for (int i = 0; i < arr.size(); i++) {
String s = arr.getString(i);
if (StringUtils.isNotBlank(s)) {
out.add(s);
}
}
}
if (!out.isEmpty()) {
return out;
}
JSONArray labels = obj.getJSONArray("unmappedLabels");
if (labels != null) {
for (int i = 0; i < labels.size(); i++) {
String s = labels.getString(i);
if (StringUtils.isNotBlank(s)) {
out.add("表标签未落点:" + s);
}
}
}
JSONArray required = obj.getJSONArray("unmappedRequired");
if (required != null) {
for (int i = 0; i < required.size(); i++) {
String s = required.getString(i);
if (StringUtils.isNotBlank(s)) {
out.add("字段未映射:" + s);
}
}
}
return out;
} catch (Exception e) {
return Collections.emptyList();
}
}
/** 列表角标用的未解析条数(与 extractMappingGaps 口径一致)。 */
private static int countUnparsedHints(String mappingJson) {
return extractMappingGaps(mappingJson).size();
}
private static String mapParseStatusLabel(String status) {
if ("parsed".equals(status)) {
return "已解析";
@@ -1,5 +1,6 @@
package org.jeecg.modules.inquiry.service.impl;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.inquiry.entity.InquiryResponseTimeout;
import org.jeecg.modules.inquiry.mapper.InquiryResponseTimeoutMapper;
@@ -9,13 +10,15 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 响应超时配置:只改 timeout_hours,不动维度列(文案纠偏除外)。
* 响应超时配置:只改 timeout_hours,不动维度列(文案纠偏与编码回填除外)。
* <p>
* 为何单独方法而不用 updateById 整行:避免前端误传改写运输方式/角色等固定种子。
* 运输类型纠偏是产品确认的一次性旧→新映射,不开放通用改维度接口。
* timeoutCode 每条唯一,供智能体单码调用。
* </p>
*/
@Service
@@ -23,12 +26,20 @@ public class InquiryResponseTimeoutServiceImpl
extends ServiceImpl<InquiryResponseTimeoutMapper, InquiryResponseTimeout>
implements IInquiryResponseTimeoutService {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(InquiryResponseTimeoutServiceImpl.class);
/**
* 陆运产品运输类型:旧逗号写法 → 产品确认的括号写法。
* 急件文案不变,不在此映射。
*/
private static final Map<String, String> LAND_PRODUCT_TYPE_RENAMES = new LinkedHashMap<>();
/**
* 种子 id → 唯一业务编码。智能体只认 timeoutCode,不认 roleCode。
*/
private static final Map<String, String> ID_TO_TIMEOUT_CODE = new LinkedHashMap<>();
static {
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,中港、东南亚常用线路/常规车型",
@@ -45,10 +56,31 @@ public class InquiryResponseTimeoutServiceImpl
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,非常用线路/临时线路/特种车型",
"普件(非常用线路/临时线路/特种车型)");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000001", "air_sales");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000002", "air_route");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000003", "sea_sales");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000004", "land_sales");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000005", "sea_product_ptp");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000006", "sea_product_other");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000007", "land_hk_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000008", "land_hk_cross");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000009", "land_hk_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000010", "land_hk_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000011", "land_ltl_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000012", "land_ltl_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000013", "land_ltl_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000014", "land_city_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000015", "land_city_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000016", "land_city_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000017", "land_sea_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000018", "land_sea_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000019", "land_sea_express");
}
/** 本进程内只纠偏一次,避免每次打开系统设置都打 UPDATE */
private volatile boolean labelsEnsured;
private volatile boolean codesEnsured;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -94,4 +126,51 @@ public class InquiryResponseTimeoutServiceImpl
labelsEnsured = true;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void ensureTimeoutCodes() {
if (codesEnsured) {
return;
}
synchronized (this) {
if (codesEnsured) {
return;
}
try {
Date now = new Date();
List<InquiryResponseTimeout> all = list();
for (InquiryResponseTimeout row : all) {
if (row == null || StringUtils.isNotBlank(row.getTimeoutCode())) {
continue;
}
String code = ID_TO_TIMEOUT_CODE.get(row.getId());
if (StringUtils.isBlank(code)) {
// 非种子行:用 id 兜底,保证唯一
code = "rt_" + row.getId();
}
lambdaUpdate()
.eq(InquiryResponseTimeout::getId, row.getId())
.set(InquiryResponseTimeout::getTimeoutCode, code)
.set(InquiryResponseTimeout::getUpdateBy, "system")
.set(InquiryResponseTimeout::getUpdateTime, now)
.update();
}
codesEnsured = true;
} catch (Exception e) {
// 列未迁移时不挡列表;运维执行 migration_inq_response_timeout_code_v1.sql 后即可
log.warn("ensureTimeoutCodes 跳过(请确认已执行 timeout_code 迁移): {}", e.getMessage());
}
}
}
@Override
public InquiryResponseTimeout getByTimeoutCode(String timeoutCode) {
if (StringUtils.isBlank(timeoutCode)) {
return null;
}
return lambdaQuery()
.eq(InquiryResponseTimeout::getTimeoutCode, timeoutCode.trim())
.one();
}
}
@@ -0,0 +1,37 @@
-- 超时响应:每条规则增加唯一业务编码 timeout_code,供智能体单码调用。
-- 仅改 inquiry_robot;幂等可重复执行。
-- 库:inquiry_robot
SET NAMES utf8mb4;
-- 1) 加列(已存在则跳过:手工执行前请先 DESCRIBE 确认)
ALTER TABLE `inq_response_timeout`
ADD COLUMN `timeout_code` varchar(64) NULL COMMENT '唯一业务编码,智能体调用' AFTER `id`;
-- 2) 按主键回填(与种子 id 对齐)
UPDATE `inq_response_timeout` SET `timeout_code`='air_sales' WHERE `id`='rt100000000000000000000000000001' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='air_route' WHERE `id`='rt100000000000000000000000000002' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='sea_sales' WHERE `id`='rt100000000000000000000000000003' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sales' WHERE `id`='rt100000000000000000000000000004' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='sea_product_ptp' WHERE `id`='rt100000000000000000000000000005' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='sea_product_other' WHERE `id`='rt100000000000000000000000000006' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_common' WHERE `id`='rt100000000000000000000000000007' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_cross' WHERE `id`='rt100000000000000000000000000008' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_special' WHERE `id`='rt100000000000000000000000000009' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_express' WHERE `id`='rt100000000000000000000000000010' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_common' WHERE `id`='rt100000000000000000000000000011' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_special' WHERE `id`='rt100000000000000000000000000012' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_express' WHERE `id`='rt100000000000000000000000000013' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_common' WHERE `id`='rt100000000000000000000000000014' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_special' WHERE `id`='rt100000000000000000000000000015' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_express' WHERE `id`='rt100000000000000000000000000016' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_common' WHERE `id`='rt100000000000000000000000000017' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_special' WHERE `id`='rt100000000000000000000000000018' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_express' WHERE `id`='rt100000000000000000000000000019' AND (`timeout_code` IS NULL OR `timeout_code`='');
-- 3) 唯一索引(列已全部非空后再建;若仍有 NULL 会失败)
UPDATE `inq_response_timeout` SET `timeout_code`=CONCAT('rt_', `id`) WHERE `timeout_code` IS NULL OR `timeout_code`='';
ALTER TABLE `inq_response_timeout`
MODIFY COLUMN `timeout_code` varchar(64) NOT NULL COMMENT '唯一业务编码,智能体调用';
ALTER TABLE `inq_response_timeout`
ADD UNIQUE KEY `uk_inq_resp_timeout_code` (`timeout_code`);