协商中再问按后台超时小时顺延并可强制清跟进时刻;工单与账号支持按筛选导出 Excel。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-13 12:34:03 +08:00
co-authored by Cursor
parent 97f0219dfd
commit 0b2f0b76c4
36 changed files with 867 additions and 64 deletions
@@ -387,6 +387,8 @@ public class InquiryAgentController {
return Result.error("timeoutCode 必填");
}
responseTimeoutService.ensureTimeoutCodes();
// 协商再问配置可能尚未跑 SQL 迁移:首次按码查询时幂等补种子
responseTimeoutService.ensureDealNegotiateRow();
org.jeecg.modules.inquiry.entity.InquiryResponseTimeout row =
responseTimeoutService.getByTimeoutCode(code);
Map<String, Object> out = new java.util.LinkedHashMap<>();
@@ -41,6 +41,7 @@ public class InquiryResponseTimeoutController
MODE_LABEL.put("AIR", "空运");
MODE_LABEL.put("SEA", "海运");
MODE_LABEL.put("LAND", "陆运");
MODE_LABEL.put("DEAL", "成交跟进");
ROLE_LABEL.put("sales", "销售");
ROLE_LABEL.put("product", "产品");
ROLE_LABEL.put("air_route", "航线");
@@ -60,9 +61,10 @@ public class InquiryResponseTimeoutController
@ApiOperation("全量列表(按 sort_no,供系统设置页一次加载)")
@GetMapping("/listAll")
public Result<List<InquiryResponseTimeout>> listAll() {
// 打开超时响应页时顺带纠偏旧运输类型文案、回填唯一编码(幂等)
// 打开超时响应页时顺带纠偏旧运输类型文案、回填唯一编码、补协商再问种子(幂等)
responseTimeoutService.ensureCanonicalTransportTypeLabels();
responseTimeoutService.ensureTimeoutCodes();
responseTimeoutService.ensureDealNegotiateRow();
QueryWrapper<InquiryResponseTimeout> qw = new QueryWrapper<>();
qw.orderByAsc("sort_no");
return Result.OK(responseTimeoutService.list(qw));
@@ -14,10 +14,12 @@ import org.jeecg.modules.inquiry.dto.TicketAbnormalRequest;
import org.jeecg.modules.inquiry.dto.TicketTransitionRequest;
import org.jeecg.modules.inquiry.entity.InquiryTicket;
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
import org.apache.shiro.authz.annotation.Logical;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@@ -66,6 +68,26 @@ public class InquiryWorkOrderController {
return viewList(pageNo, pageSize, req);
}
/**
* 按当前列表筛选导出 Excel。有 export 或 list 权限即可(避免只开了查询的角色点导出 403)。
*/
@AutoLog(value = "工单-导出Excel")
@ApiOperation("按筛选导出工单 Excel")
@RequiresPermissions(value = {"inquiry:workorder:export", "inquiry:workorder:list"}, logical = Logical.OR)
@GetMapping("/view/export")
public void viewExport(HttpServletRequest req, HttpServletResponse response) throws Exception {
Map<String, String> filters = new HashMap<>();
copyParam(req, filters, "keyword");
copyParam(req, filters, "status");
copyParam(req, filters, "bizType");
copyParam(req, filters, "businessLine");
copyParam(req, filters, "eventException");
copyParam(req, filters, "systemException");
copyParam(req, filters, "dateFrom");
copyParam(req, filters, "dateTo");
workOrderService.exportExcel(filters, response);
}
@AutoLog(value = "工单-聚合详情")
@ApiOperation("聚合详情")
@RequiresPermissions("inquiry:workorder:list")
@@ -0,0 +1,73 @@
package org.jeecg.modules.inquiry.controller;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.inquiry.entity.InquiryTicket;
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 强制清空协商再问时刻。
* <p>
* 背景:claimDueNegotiate 若用 updateById 写 null,MyBatis-Plus 会跳过,
* Worker 每分钟重复认领并群发「确认成交状态」(WO202610060024)。
* 本接口用 UpdateWrapper 强制 SET null,供智能体认领后补清。
*/
@Api(tags = "询价-协商再问清理")
@RestController
@RequestMapping("/inquiry/agent/ticket")
public class NegotiateFollowupClearController {
@Resource
private InquiryTicketMapper ticketMapper;
@ApiOperation("强制清空 next_followup_at")
@PostMapping("/clearNextFollowup")
public Result<Map<String, Object>> clearNextFollowup(
@RequestBody Map<String, Object> body,
HttpServletRequest http) {
String no = first(body, "workOrderNo", "work_order_no");
Map<String, Object> out = new LinkedHashMap<>();
if (StringUtils.isBlank(no)) {
out.put("ok", false);
out.put("error", "missing_work_order_no");
return Result.OK(out);
}
boolean ok = ticketMapper.update(
null,
new LambdaUpdateWrapper<InquiryTicket>()
.eq(InquiryTicket::getWorkOrderNo, no.trim())
.set(InquiryTicket::getNextFollowupAt, null)
.set(InquiryTicket::getUpdateBy, "agent")
.set(InquiryTicket::getUpdateTime, new Date())) > 0;
out.put("ok", ok);
out.put("work_order_no", no.trim());
out.put("cleared", ok);
return Result.OK(out);
}
private static String first(Map<String, Object> body, String... keys) {
if (body == null) {
return "";
}
for (String key : keys) {
Object val = body.get(key);
if (val != null && StringUtils.isNotBlank(String.valueOf(val))) {
return String.valueOf(val).trim();
}
}
return "";
}
}
@@ -20,6 +20,7 @@ import java.util.Date;
* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。
* timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。
* 本表是配置真相;工单超时判定读 timeoutHours(允许小数,0=不监测)。
* 成交跟进 deal_negotiate:协商中再问工作小时(0=不排再问),transportMode=DEAL。
* </p>
*/
@Data
@@ -179,7 +179,7 @@ public class InquiryTicket implements Serializable {
private String lostReason;
/**
* 协商中下次再问时刻(第四个工作日 09:00)。
* 协商中下次再问时刻(工作小时顺延,小时数读 deal_negotiate 超时配置)。
* 中途改成交/未成交或再问发出后清空。
*/
@ApiModelProperty("协商中下次再问时间")
@@ -35,6 +35,14 @@ public interface IInquiryResponseTimeoutService extends IService<InquiryResponse
*/
void ensureTimeoutCodes();
/**
* 确保「协商中再问成交」配置行存在(编码 deal_negotiate,默认 48 工作小时)。
* <p>
* 打开超时响应页时幂等插入;已有则不动小时数,避免覆盖运营改过的值。
* </p>
*/
void ensureDealNegotiateRow();
/**
* 按唯一业务编码查超时配置;无则 null。
*/
@@ -22,6 +22,15 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
*/
IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters);
/**
* 按与 {@link #pageView} 相同的筛选条件导出 Excel(不分页,有行数上限)。
* 副作用:只写 HTTP 响应流,不改工单六态。
*
* @param filters 与列表同源的筛选
* @param response HTTP 响应
*/
void exportExcel(Map<String, String> filters, javax.servlet.http.HttpServletResponse response) throws java.io.IOException;
/**
* 聚合详情:工单 + 报价列表 + 附件 + 时间线 + 开放异常。
*/
@@ -8,6 +8,7 @@ import org.jeecg.modules.inquiry.service.IInquiryResponseTimeoutService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
@@ -64,11 +65,19 @@ public class InquiryResponseTimeoutServiceImpl
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");
// 成交跟进:协商中再问(跨业务线,transport_mode=DEAL)
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000030", "deal_negotiate");
}
/** 协商中再问成交:唯一编码,智能体 finish_deal 读此码取工作小时。 */
public static final String CODE_DEAL_NEGOTIATE = "deal_negotiate";
private static final String ID_DEAL_NEGOTIATE = "rt100000000000000000000000000030";
/** 本进程内只纠偏一次,避免每次打开系统设置都打 UPDATE */
private volatile boolean labelsEnsured;
private volatile boolean codesEnsured;
private volatile boolean dealNegotiateEnsured;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -152,6 +161,44 @@ public class InquiryResponseTimeoutServiceImpl
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void ensureDealNegotiateRow() {
if (dealNegotiateEnsured) {
return;
}
synchronized (this) {
if (dealNegotiateEnsured) {
return;
}
try {
InquiryResponseTimeout existed = getByTimeoutCode(CODE_DEAL_NEGOTIATE);
if (existed != null) {
dealNegotiateEnsured = true;
return;
}
// 缺行则种子插入;小时默认 48,之后只许运营在后台改小时
InquiryResponseTimeout row = new InquiryResponseTimeout();
row.setId(ID_DEAL_NEGOTIATE);
row.setTimeoutCode(CODE_DEAL_NEGOTIATE);
row.setTransportMode("DEAL");
row.setRoleCode("sales");
row.setRouteCategory("");
row.setTransportType("协商中再问成交");
row.setTimeoutHours(new BigDecimal("48"));
row.setSortNo(5);
row.setCreateBy("system");
row.setCreateTime(new Date());
row.setUpdateBy("system");
row.setUpdateTime(new Date());
save(row);
dealNegotiateEnsured = true;
} catch (Exception e) {
log.warn("ensureDealNegotiateRow 跳过: {}", e.getMessage());
}
}
}
@Override
public InquiryResponseTimeout getByTimeoutCode(String timeoutCode) {
if (StringUtils.isBlank(timeoutCode)) {
@@ -32,6 +32,7 @@ import org.jeecg.modules.inquiry.support.InquiryQuoteFileSupport;
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
import org.jeecg.modules.inquiry.support.InquiryTicketFactsSupport;
import org.jeecg.modules.inquiry.support.InquiryTicketPriceSupport;
import org.jeecg.modules.inquiry.support.InquiryWorkOrderExcelExport;
import org.jeecg.modules.inquiry.tms.InquiryTmsQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -39,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -82,6 +85,28 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
@Override
public IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters) {
LambdaQueryWrapper<InquiryTicket> qw = buildListQuery(filters);
IPage<InquiryTicket> result = page(page, qw);
enrichDisplayFields(result.getRecords());
return result;
}
@Override
public void exportExcel(Map<String, String> filters, HttpServletResponse response) throws IOException {
// 与列表同筛选;分页上限防止一次导出拖垮内存
Page<InquiryTicket> page = new Page<>(1, InquiryWorkOrderExcelExport.MAX_ROWS);
IPage<InquiryTicket> result = page(page, buildListQuery(filters));
enrichDisplayFields(result.getRecords());
InquiryWorkOrderExcelExport.write(result.getRecords(), response);
}
/**
* 列表 / 导出共用筛选:关键字、六态、业务类型、异常、创建日、数据范围。
*/
private LambdaQueryWrapper<InquiryTicket> buildListQuery(Map<String, String> filters) {
if (filters == null) {
filters = Collections.emptyMap();
}
LambdaQueryWrapper<InquiryTicket> qw = new LambdaQueryWrapper<>();
String keyword = filters.get("keyword");
if (StringUtils.isNotBlank(keyword)) {
@@ -126,9 +151,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
// 与看板同源:账号勾「只看自己」时收紧列表(非超管)
InquiryDataScopeSupport.applySelfOnly(qw);
qw.orderByDesc(InquiryTicket::getCreateTime);
IPage<InquiryTicket> result = page(page, qw);
enrichDisplayFields(result.getRecords());
return result;
return qw;
}
@Override
@@ -1520,7 +1543,10 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
/**
* 认领到期协商再问:状态仍是协商中且 next_followup_at 已到。
* 清空再问时刻,Worker 只发这一轮;销售再选协商中才会排下一轮。
* <p>
* 必须用 UpdateWrapper 强制把 next_followup_at 写成 null:MyBatis-Plus updateById
* 默认跳过 null,库里时刻清不掉,Worker 每分钟会再认领并群发「确认成交状态」
* (WO202610060024 同一话术连发多条)。多 Worker 时按行条件更新,更新失败则本行不返回。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -1536,12 +1562,38 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
return out;
}
for (InquiryTicket ticket : due) {
// 条件更新:仅当再问时刻仍未清空时认领成功,避免双 Worker 各发一轮
boolean claimed = lambdaUpdate()
.eq(InquiryTicket::getId, ticket.getId())
.eq(InquiryTicket::getStatus, TicketStatuses.NEGOTIATING)
.isNotNull(InquiryTicket::getNextFollowupAt)
.le(InquiryTicket::getNextFollowupAt, now)
.set(InquiryTicket::getNextFollowupAt, null)
.set(InquiryTicket::getUpdateBy, "agent")
.set(InquiryTicket::getUpdateTime, now)
.update();
if (!claimed) {
continue;
}
ticket.setNextFollowupAt(null);
ticket.setUpdateBy("agent");
ticket.setUpdateTime(now);
updateById(ticket);
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "negotiate_followup", "agent", "协商再问已发出");
out.add(toDueNegotiateRow(ticket));
// #region agent log
try {
java.nio.file.Path dbg = java.nio.file.Paths.get(
"d:/own/友通达询价智能体(新)/debug-44a132.log");
String line = "{\"sessionId\":\"44a132\",\"runId\":\"post-fix\",\"hypothesisId\":\"H-dup\","
+ "\"location\":\"claimDueNegotiate\",\"message\":\"claimed_clear_followup\","
+ "\"data\":{\"workOrderNo\":\"" + ticket.getWorkOrderNo() + "\"},"
+ "\"timestamp\":" + System.currentTimeMillis() + "}\n";
java.nio.file.Files.write(dbg, line.getBytes(java.nio.charset.StandardCharsets.UTF_8),
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
} catch (Exception ignored) {
// 调试日志失败不挡认领
}
// #endregion
}
return out;
}
@@ -0,0 +1,128 @@
package org.jeecg.modules.inquiry.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.jeecg.modules.inquiry.entity.InquiryQuote;
import org.jeecg.modules.inquiry.entity.InquiryTicket;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 询价工单列表 → Excel(xlsx)。
* <p>
* 职责:按列表同口径列写出文件流;不查库、不改六态。
* 线程:仅在导出请求线程写响应,禁止塞进回调/Worker。
* 为何:后台「导出工单」要按当前筛选落 Excel,与列表筛选同源。
*/
public final class InquiryWorkOrderExcelExport {
private static final String[] HEADERS = {
"工单编号", "发起人", "业务类型", "询价需求", "起运地", "目的地", "客户",
"报价信息", "单据状态", "成本价", "销售价", "利润点",
"事件异常", "系统异常", "系统异常原因", "创建时间"
};
/** 单次导出上限,防止一次拖垮 Tomcat 内存;超过则只写前 N 条。 */
public static final int MAX_ROWS = 20000;
private InquiryWorkOrderExcelExport() {
}
/**
* 写出 xlsx 到 HTTP 响应。
*
* @param tickets 已 enrich 展示字段的工单
* @param response HTTP 响应
*/
public static void write(List<InquiryTicket> tickets, HttpServletResponse response) throws IOException {
String fileName = "询价工单_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));
try (SXSSFWorkbook wb = new SXSSFWorkbook(200)) {
Sheet sheet = wb.createSheet("询价工单");
Row head = sheet.createRow(0);
for (int i = 0; i < HEADERS.length; i++) {
Cell c = head.createCell(i);
c.setCellValue(HEADERS[i]);
}
SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int limit = Math.min(tickets == null ? 0 : tickets.size(), MAX_ROWS);
for (int i = 0; i < limit; i++) {
InquiryTicket t = tickets.get(i);
Row row = sheet.createRow(i + 1);
writeCell(row, 0, t.getWorkOrderNo());
writeCell(row, 1, firstNonBlank(t.getSalesName(), t.getSalesWecomId()));
writeCell(row, 2, firstNonBlank(t.getBizTypeLabel(), t.getBusinessLine()));
writeCell(row, 3, firstNonBlank(t.getDemandTitle(), t.getDemandDetail()));
writeCell(row, 4, t.getOrigin());
writeCell(row, 5, t.getDestination());
writeCell(row, 6, t.getCustomerName());
writeCell(row, 7, quoteSummary(t));
writeCell(row, 8, t.getStatus());
writeCell(row, 9, t.getCostPrice());
writeCell(row, 10, t.getSalePrice());
writeCell(row, 11, t.getProfitRate());
writeCell(row, 12, dash(t.getEventException()));
writeCell(row, 13, dash(t.getSystemException()));
writeCell(row, 14, dash(t.getSystemExceptionReason()));
writeCell(row, 15, t.getCreateTime() == null ? "" : dt.format(t.getCreateTime()));
}
wb.write(response.getOutputStream());
response.flushBuffer();
}
}
private static String quoteSummary(InquiryTicket t) {
List<InquiryQuote> quotes = t.getQuotes();
if (quotes == null || quotes.isEmpty()) {
return "";
}
return quotes.stream()
.filter(q -> q != null)
.map(q -> {
String src = StringUtils.defaultIfBlank(q.getSourceLabel(), q.getSource());
String price = StringUtils.defaultString(q.getPriceText());
if (StringUtils.isBlank(src) && StringUtils.isBlank(price)) {
return "";
}
return (StringUtils.defaultString(src) + " " + price).trim();
})
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining(";"));
}
private static void writeCell(Row row, int idx, String value) {
Cell c = row.createCell(idx);
c.setCellValue(value == null ? "" : value);
}
private static String firstNonBlank(String a, String b) {
if (StringUtils.isNotBlank(a)) {
return a.trim();
}
if (StringUtils.isNotBlank(b)) {
return b.trim();
}
return "";
}
private static String dash(String v) {
if (StringUtils.isBlank(v) || "-".equals(v.trim())) {
return "-";
}
return v.trim();
}
}
@@ -0,0 +1,13 @@
-- 协商中回执:由「三个工作日 9:00」改为「N 个工作小时 + 具体时刻」。
-- 变量 followup_hours=顺延小时文案;followup_when=再问时刻(如 9月24日15:30)。
-- 仅改 inquiry_robot;幂等。
SET NAMES utf8mb4;
UPDATE `inq_bot_reply`
SET `reply_text` = '已记录为「协商中」。已顺延 {{followup_hours}} 个工作小时,将于 {{followup_when}} 再次发起成交状态询问。',
`update_by` = 'system',
`update_time` = NOW()
WHERE `reply_code` = 'negotiating.recorded'
OR `id` = 'brv10000000000000000000000000039'
OR `trigger_scene` = '成交跟进中选择「协商中」';
@@ -67,7 +67,7 @@ VALUES
('brv10000000000000000000000000036','quoted','已报价','quoted.template_missing','后台未维护报价模板','生成报价单失败,请联系IT运维维护后台报价模板。',1,4,'admin',NOW()),
('brv10000000000000000000000000037','quoted','已报价','quoted.file_ready','最终报价单生成完成','已生成{{kind}}最终报价单:{{filename}}(工单 {{ticket_no}})',1,5,'admin',NOW()),
('brv10000000000000000000000000038','deal','已成交','deal.recorded_won','销售选择「成交」','已记录为「已成交」。工单 {{ticket_no}} 已关闭智能体跟进。',1,1,'admin',NOW()),
('brv10000000000000000000000000039','negotiating','协商中','negotiating.recorded','成交跟进中选择「协商中」','已记录为「协商中」。已顺延三个工作日,{{followup_when}}的9:00再次发起成交状态询问。',1,1,'admin',NOW()),
('brv10000000000000000000000000039','negotiating','协商中','negotiating.recorded','成交跟进中选择「协商中」','已记录为「协商中」。已顺延 {{followup_hours}} 个工作小时,将于 {{followup_when}} 再次发起成交状态询问。',1,1,'admin',NOW()),
('brv10000000000000000000000000040','lost','未成交','lost.ask_reason','成交跟进中选择「未成交」且未写原因','已记录为「未成交」。请补充未成交原因。',1,1,'admin',NOW()),
('brv10000000000000000000000000041','lost','未成交','lost.reason_ack','销售回复未成交原因','已将未成交原因“{{lost_reason}}”记录在后台,工单 {{ticket_no}}已关闭,智能体不再跟进该工单,请悉知。',1,2,'admin',NOW()),
('brv10000000000000000000000000042','closed','已关闭','closed.air_cannot_activate','空运工单已是终态仍要激活','工单{{ticket_no}}已是「{{status}}」,不能再激活。',1,1,'admin',NOW()),
@@ -0,0 +1,29 @@
-- 协商中再问成交:按工作小时顺延(默认 48),后台「超时响应」页可改。
-- 编码 deal_negotiate;运输方式 DEAL(成交跟进分块);0=不排再问。
-- 仅改 inquiry_robot;幂等可重复执行。
SET NAMES utf8mb4;
INSERT INTO `inq_response_timeout` (
`id`, `timeout_code`, `transport_mode`, `role_code`, `route_category`,
`transport_type`, `timeout_hours`, `sort_no`, `create_by`, `create_time`, `update_by`, `update_time`
)
SELECT
'rt100000000000000000000000000030',
'deal_negotiate',
'DEAL',
'sales',
'',
CONVERT(UNHEX('e58d8fe59586e4b8ade5868de997aee68890e4baa4') USING utf8mb4),
48,
5,
'system',
NOW(),
'system',
NOW()
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM (
SELECT `id` FROM `inq_response_timeout` WHERE `timeout_code`='deal_negotiate'
) AS `existed`
);