协商中再问按后台超时小时顺延并可强制清跟进时刻;工单与账号支持按筛选导出 Excel。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
@@ -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<>();
|
||||
|
||||
+3
-1
@@ -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));
|
||||
|
||||
+22
@@ -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")
|
||||
|
||||
+73
@@ -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 "";
|
||||
}
|
||||
}
|
||||
+1
@@ -20,6 +20,7 @@ import java.util.Date;
|
||||
* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。
|
||||
* timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。
|
||||
* 本表是配置真相;工单超时判定读 timeoutHours(允许小数,0=不监测)。
|
||||
* 成交跟进 deal_negotiate:协商中再问工作小时(0=不排再问),transportMode=DEAL。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
|
||||
+1
-1
@@ -179,7 +179,7 @@ public class InquiryTicket implements Serializable {
|
||||
private String lostReason;
|
||||
|
||||
/**
|
||||
* 协商中下次再问时刻(第四个工作日 09:00)。
|
||||
* 协商中下次再问时刻(工作小时顺延,小时数读 deal_negotiate 超时配置)。
|
||||
* 中途改成交/未成交或再问发出后清空。
|
||||
*/
|
||||
@ApiModelProperty("协商中下次再问时间")
|
||||
|
||||
+8
@@ -35,6 +35,14 @@ public interface IInquiryResponseTimeoutService extends IService<InquiryResponse
|
||||
*/
|
||||
void ensureTimeoutCodes();
|
||||
|
||||
/**
|
||||
* 确保「协商中再问成交」配置行存在(编码 deal_negotiate,默认 48 工作小时)。
|
||||
* <p>
|
||||
* 打开超时响应页时幂等插入;已有则不动小时数,避免覆盖运营改过的值。
|
||||
* </p>
|
||||
*/
|
||||
void ensureDealNegotiateRow();
|
||||
|
||||
/**
|
||||
* 按唯一业务编码查超时配置;无则 null。
|
||||
*/
|
||||
|
||||
+9
@@ -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;
|
||||
|
||||
/**
|
||||
* 聚合详情:工单 + 报价列表 + 附件 + 时间线 + 开放异常。
|
||||
*/
|
||||
|
||||
+47
@@ -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)) {
|
||||
|
||||
+57
-5
@@ -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;
|
||||
}
|
||||
|
||||
+128
@@ -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();
|
||||
}
|
||||
}
|
||||
+13
@@ -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` = '成交跟进中选择「协商中」';
|
||||
+1
-1
@@ -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()),
|
||||
|
||||
+29
@@ -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`
|
||||
);
|
||||
Reference in New Issue
Block a user