补齐成交跟进:按线路推成交通知、收下未成交原因、协商中按工作日再问。
成交走询价消息服务;陆运/海运私聊下一句能记下原因;协商中回执写明顺延三个工作日和下次 9:00。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+29
@@ -122,6 +122,10 @@ public class InquiryAgentController {
|
||||
Object to = body == null ? null : body.get("toStatus");
|
||||
req.setWorkOrderNo(no == null ? "" : String.valueOf(no));
|
||||
req.setToStatus(to == null ? "" : String.valueOf(to));
|
||||
req.setRemark(firstStr(body, "remark"));
|
||||
req.setLostReason(firstStr(body, "lostReason", "lost_reason"));
|
||||
req.setNextFollowupAt(firstStr(body, "nextFollowupAt", "next_followup_at"));
|
||||
req.setDealNotifySummary(firstStr(body, "dealNotifySummary", "deal_notify_summary"));
|
||||
try {
|
||||
org.jeecg.modules.inquiry.entity.InquiryTicket ticket =
|
||||
workOrderService.transition(req, "agent");
|
||||
@@ -135,6 +139,31 @@ public class InquiryAgentController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOperation("智能体补未成交原因(不改六态)")
|
||||
@PostMapping("/ticket/patchLostReason")
|
||||
public Result<Map<String, Object>> patchLostReason(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.patchLostReason(
|
||||
firstStr(body, "workOrderNo", "work_order_no"),
|
||||
firstStr(body, "lostReason", "lost_reason"),
|
||||
"agent"));
|
||||
}
|
||||
|
||||
@ApiOperation("认领到期协商再问(Worker HTTP 槽)")
|
||||
@PostMapping("/ticket/claimDueNegotiate")
|
||||
public Result<Map<String, Object>> claimDueNegotiate(HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
Map<String, Object> out = new java.util.LinkedHashMap<>();
|
||||
out.put("ok", true);
|
||||
out.put("tickets", workOrderService.claimDueNegotiate());
|
||||
return Result.OK(out);
|
||||
}
|
||||
|
||||
@ApiOperation("智能体续办读单(只读,校验发起人)")
|
||||
@PostMapping("/ticket/get")
|
||||
public Result<Map<String, Object>> get(@RequestBody Map<String, Object> body,
|
||||
|
||||
+3
-1
@@ -30,6 +30,7 @@ public final class TicketStatuses {
|
||||
/**
|
||||
* from -> 允许的 to 集合。
|
||||
* 终态(已成交/未成交/已关闭)默认不可再流转,除非显式「关闭」从非终态进入已关闭。
|
||||
* 协商中允许再进协商中:只顺延再问,不重开询价。
|
||||
*/
|
||||
private static final Map<String, Set<String>> ALLOWED;
|
||||
|
||||
@@ -37,7 +38,8 @@ public final class TicketStatuses {
|
||||
Map<String, Set<String>> m = new HashMap<>();
|
||||
m.put(INQUIRING, setOf(QUOTED, NEGOTIATING, CLOSED));
|
||||
m.put(QUOTED, setOf(NEGOTIATING, DEAL, LOST, CLOSED));
|
||||
m.put(NEGOTIATING, setOf(QUOTED, DEAL, LOST, CLOSED));
|
||||
// 协商中可再选协商中:顺延下一轮再问,次数不限。
|
||||
m.put(NEGOTIATING, setOf(QUOTED, DEAL, LOST, CLOSED, NEGOTIATING));
|
||||
m.put(DEAL, Collections.emptySet());
|
||||
m.put(LOST, Collections.emptySet());
|
||||
m.put(CLOSED, Collections.emptySet());
|
||||
|
||||
+9
@@ -20,4 +20,13 @@ public class TicketTransitionRequest {
|
||||
|
||||
@ApiModelProperty("备注/关单原因")
|
||||
private String remark;
|
||||
|
||||
@ApiModelProperty("未成交原因(仅未成交)")
|
||||
private String lostReason;
|
||||
|
||||
@ApiModelProperty("协商中下次再问时间")
|
||||
private String nextFollowupAt;
|
||||
|
||||
@ApiModelProperty("成交通知结果摘要")
|
||||
private String dealNotifySummary;
|
||||
}
|
||||
|
||||
+22
@@ -115,6 +115,28 @@ public class InquiryTicket implements Serializable {
|
||||
@ApiModelProperty("关单原因")
|
||||
private String closeReason;
|
||||
|
||||
/**
|
||||
* 未成交原因:销售下一句任意文字;没回则空着。
|
||||
* 不是 closeReason,后台要单独展示。
|
||||
*/
|
||||
@ApiModelProperty("未成交原因")
|
||||
private String lostReason;
|
||||
|
||||
/**
|
||||
* 协商中下次再问时刻(第四个工作日 09:00)。
|
||||
* 中途改成交/未成交或再问发出后清空。
|
||||
*/
|
||||
@ApiModelProperty("协商中下次再问时间")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date nextFollowupAt;
|
||||
|
||||
/**
|
||||
* 成交通知结果:已推哪些负责人 / 无匹配负责人 / 通知未发出。
|
||||
*/
|
||||
@ApiModelProperty("成交通知结果")
|
||||
private String dealNotifySummary;
|
||||
|
||||
@ApiModelProperty("业务版本")
|
||||
private Integer version;
|
||||
|
||||
|
||||
+11
@@ -109,4 +109,15 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
|
||||
* 空运群激活/切换当前工单。一单一群,未结案才能绑。不改六态。
|
||||
*/
|
||||
Map<String, Object> activateAirGroup(Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 未成交后补原因。已成交/其它六态拒绝。不改六态。
|
||||
*/
|
||||
Map<String, Object> patchLostReason(String workOrderNo, String lostReason, String operator);
|
||||
|
||||
/**
|
||||
* 认领已到期的协商中再问:清空 nextFollowupAt,避免 Worker 重复发。
|
||||
* 只改到期行,不加全局锁。
|
||||
*/
|
||||
java.util.List<java.util.Map<String, Object>> claimDueNegotiate();
|
||||
}
|
||||
|
||||
+3
@@ -174,6 +174,9 @@ public class InquiryStaffRoleServiceImpl extends ServiceImpl<InquiryStaffRoleMap
|
||||
one.put("wecomId", row.getWecomId());
|
||||
one.put("roleCode", row.getRoleCode());
|
||||
one.put("status", row.getStatus());
|
||||
// 成交跟进按「是负责人 + 负责线路」匹配,缺这两项智能体对不上人。
|
||||
one.put("isLeader", row.getIsLeader());
|
||||
one.put("routes", StringUtils.defaultString(row.getRoutes()));
|
||||
out.add(one);
|
||||
}
|
||||
return out;
|
||||
|
||||
+155
-3
@@ -209,11 +209,27 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
if (TicketStatuses.CLOSED.equals(to) && StringUtils.isNotBlank(req.getRemark())) {
|
||||
ticket.setCloseReason(req.getRemark());
|
||||
}
|
||||
if (TicketStatuses.LOST.equals(to)) {
|
||||
ticket.setNextFollowupAt(null);
|
||||
if (StringUtils.isNotBlank(req.getLostReason())) {
|
||||
ticket.setLostReason(StringUtils.left(req.getLostReason().trim(), 500));
|
||||
}
|
||||
} else if (TicketStatuses.DEAL.equals(to)) {
|
||||
ticket.setNextFollowupAt(null);
|
||||
if (StringUtils.isNotBlank(req.getDealNotifySummary())) {
|
||||
ticket.setDealNotifySummary(StringUtils.left(req.getDealNotifySummary().trim(), 500));
|
||||
}
|
||||
} else if (TicketStatuses.NEGOTIATING.equals(to)) {
|
||||
Date follow = parseDateTime(req.getNextFollowupAt());
|
||||
ticket.setNextFollowupAt(follow);
|
||||
}
|
||||
ticket.setVersion(ticket.getVersion() == null ? 2 : ticket.getVersion() + 1);
|
||||
ticket.setUpdateBy(operator);
|
||||
ticket.setUpdateTime(new Date());
|
||||
updateById(ticket);
|
||||
appendLog(ticket, from, to, "transition", operator, req.getRemark());
|
||||
String remark = StringUtils.defaultIfBlank(req.getRemark(),
|
||||
StringUtils.defaultIfBlank(req.getDealNotifySummary(), req.getLostReason()));
|
||||
appendLog(ticket, from, to, "transition", operator, remark);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@@ -569,9 +585,13 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
}
|
||||
String status = StringUtils.defaultString(ticket.getStatus());
|
||||
String waitPhase;
|
||||
if (TicketStatuses.DEAL.equals(status) || TicketStatuses.LOST.equals(status)
|
||||
|| TicketStatuses.NEGOTIATING.equals(status) || TicketStatuses.CLOSED.equals(status)) {
|
||||
if (TicketStatuses.DEAL.equals(status) || TicketStatuses.CLOSED.equals(status)) {
|
||||
waitPhase = "done";
|
||||
} else if (TicketStatuses.LOST.equals(status)) {
|
||||
waitPhase = StringUtils.isBlank(ticket.getLostReason()) ? "wait_lost_reason" : "done";
|
||||
} else if (TicketStatuses.NEGOTIATING.equals(status)) {
|
||||
// 协商中会话不能结束:还要再问成交,也可中途改成交/未成交。
|
||||
waitPhase = "wait_deal";
|
||||
} else if (TicketStatuses.QUOTED.equals(status) && !quote.isEmpty()) {
|
||||
// 文件已发出:成交跟进。六态仍是已报价,不能退回协同/出方案卡
|
||||
if (quoteFileAlreadyIssued(no, quote)) {
|
||||
@@ -592,6 +612,12 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
out.put("facts", facts);
|
||||
out.put("quote", quote);
|
||||
out.put("wait_phase", waitPhase);
|
||||
out.put("lost_reason", StringUtils.defaultString(ticket.getLostReason()));
|
||||
out.put("lostReason", StringUtils.defaultString(ticket.getLostReason()));
|
||||
out.put("deal_notify_summary", StringUtils.defaultString(ticket.getDealNotifySummary()));
|
||||
if (ticket.getNextFollowupAt() != null) {
|
||||
out.put("next_followup_at", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(ticket.getNextFollowupAt()));
|
||||
}
|
||||
out.put("collab_chat_id", StringUtils.defaultString(ticket.getCollabChatId()));
|
||||
Map<String, String> collabFacts = new LinkedHashMap<>();
|
||||
if (StringUtils.isNotBlank(ticket.getCollabFactsJson())) {
|
||||
@@ -773,6 +799,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
out.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
|
||||
out.put("facts", parseFactsJson(ticket.getFactsJson()));
|
||||
out.put("quote", latestQuotePayload(ticket.getWorkOrderNo()));
|
||||
out.put("lost_reason", StringUtils.defaultString(ticket.getLostReason()));
|
||||
out.put("lostReason", StringUtils.defaultString(ticket.getLostReason()));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1027,6 +1055,130 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
.last("LIMIT 1"), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未成交后补原因。不改六态;已有原因可覆盖。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> patchLostReason(String workOrderNo, String lostReason, String operator) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
String no = StringUtils.trimToEmpty(workOrderNo);
|
||||
String reason = StringUtils.left(StringUtils.trimToEmpty(lostReason), 500);
|
||||
out.put("ok", false);
|
||||
if (StringUtils.isBlank(no) || StringUtils.isBlank(reason)) {
|
||||
out.put("error", "workOrderNo_or_reason_blank");
|
||||
return out;
|
||||
}
|
||||
InquiryTicket ticket = findByWorkOrderNo(no);
|
||||
if (ticket == null) {
|
||||
out.put("error", "ticket_not_found");
|
||||
return out;
|
||||
}
|
||||
if (!TicketStatuses.LOST.equals(ticket.getStatus())) {
|
||||
out.put("error", "not_lost");
|
||||
out.put("status", ticket.getStatus());
|
||||
return out;
|
||||
}
|
||||
ticket.setLostReason(reason);
|
||||
ticket.setUpdateBy(StringUtils.defaultIfBlank(operator, "agent"));
|
||||
ticket.setUpdateTime(new Date());
|
||||
updateById(ticket);
|
||||
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "lost_reason", operator, reason);
|
||||
out.put("ok", true);
|
||||
out.put("work_order_no", no);
|
||||
out.put("lost_reason", reason);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认领到期协商再问:状态仍是协商中且 next_followup_at 已到。
|
||||
* 清空再问时刻,Worker 只发这一轮;销售再选协商中才会排下一轮。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Map<String, Object>> claimDueNegotiate() {
|
||||
Date now = new Date();
|
||||
List<InquiryTicket> due = list(new LambdaQueryWrapper<InquiryTicket>()
|
||||
.eq(InquiryTicket::getStatus, TicketStatuses.NEGOTIATING)
|
||||
.isNotNull(InquiryTicket::getNextFollowupAt)
|
||||
.le(InquiryTicket::getNextFollowupAt, now)
|
||||
.last("LIMIT 50"));
|
||||
List<Map<String, Object>> out = new ArrayList<>();
|
||||
if (due == null || due.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (InquiryTicket ticket : due) {
|
||||
ticket.setNextFollowupAt(null);
|
||||
ticket.setUpdateBy("agent");
|
||||
ticket.setUpdateTime(now);
|
||||
updateById(ticket);
|
||||
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "negotiate_followup", "agent", "协商再问已发出");
|
||||
out.add(toDueNegotiateRow(ticket));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker 再问需要的最小快照:会话通道、字段、报价。
|
||||
*/
|
||||
private Map<String, Object> toDueNegotiateRow(InquiryTicket ticket) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("work_order_no", ticket.getWorkOrderNo());
|
||||
row.put("workOrderNo", ticket.getWorkOrderNo());
|
||||
row.put("sales_wecom_id", StringUtils.defaultString(ticket.getSalesWecomId()));
|
||||
row.put("salesWecomId", StringUtils.defaultString(ticket.getSalesWecomId()));
|
||||
row.put("sales_name", StringUtils.defaultString(ticket.getSalesName()));
|
||||
row.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
|
||||
row.put("collab_chat_id", StringUtils.defaultString(ticket.getCollabChatId()));
|
||||
row.put("status", ticket.getStatus());
|
||||
Map<String, String> facts = new LinkedHashMap<>();
|
||||
if (StringUtils.isNotBlank(ticket.getFactsJson())) {
|
||||
JSONObject stored = JSON.parseObject(ticket.getFactsJson());
|
||||
if (stored != null) {
|
||||
for (String key : stored.keySet()) {
|
||||
if (stored.get(key) != null) {
|
||||
facts.put(key, String.valueOf(stored.get(key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(ticket.getOrigin())) {
|
||||
facts.putIfAbsent("起运港", ticket.getOrigin());
|
||||
}
|
||||
if (StringUtils.isNotBlank(ticket.getDestination())) {
|
||||
facts.putIfAbsent("目的港", ticket.getDestination());
|
||||
}
|
||||
row.put("facts", facts);
|
||||
Map<String, Object> quote = new LinkedHashMap<>();
|
||||
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, ticket.getWorkOrderNo())
|
||||
.orderByDesc(InquiryQuote::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
if (quotes != null && !quotes.isEmpty() && StringUtils.isNotBlank(quotes.get(0).getPayloadJson())) {
|
||||
JSONObject parsed = JSON.parseObject(quotes.get(0).getPayloadJson());
|
||||
if (parsed != null) {
|
||||
quote.putAll(parsed);
|
||||
}
|
||||
}
|
||||
row.put("quote", quote);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static Date parseDateTime(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String text = raw.trim().replace("T", " ");
|
||||
if (text.length() >= 19) {
|
||||
text = text.substring(0, 19);
|
||||
}
|
||||
try {
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(text);
|
||||
} catch (Exception e) {
|
||||
return parseDate(raw);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendLog(InquiryTicket ticket, String from, String to, String action, String operator, String remark) {
|
||||
InquiryTicketStatusLog log = new InquiryTicketStatusLog();
|
||||
log.setTicketId(ticket.getId());
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- 成交跟进:未成交原因 / 协商再问时刻 / 成交通知摘要。
|
||||
-- 仅改 inquiry_robot;幂等需手工确认列是否已存在。
|
||||
-- 库:inquiry_robot
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `inq_ticket`
|
||||
ADD COLUMN `lost_reason` varchar(500) NULL COMMENT '未成交原因,不是关单原因' AFTER `close_reason`;
|
||||
|
||||
ALTER TABLE `inq_ticket`
|
||||
ADD COLUMN `next_followup_at` datetime NULL COMMENT '协商中下次再问(工作日 09:00)' AFTER `lost_reason`;
|
||||
|
||||
ALTER TABLE `inq_ticket`
|
||||
ADD COLUMN `deal_notify_summary` varchar(500) NULL COMMENT '成交通知:已推负责人/无匹配/未发出' AFTER `next_followup_at`;
|
||||
Reference in New Issue
Block a user