落地转人工:三次补问或明确接管后出入口,真正转人工后冻结本票并允许新开询价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 16:42:13 +08:00
co-authored by Cursor
parent 26dd5d72a1
commit 7eedf28e8d
23 changed files with 1499 additions and 132 deletions
@@ -8,7 +8,7 @@ import java.util.Map;
import java.util.Set;
/**
* 询价工单六态与允许流转规则。
* 询价工单状态与允许流转规则(含转人工)。
* <p>
* 职责:集中定义状态字面量与 transition 合法性,避免 Controller/Service 散落魔法字符串。
* 副作用:无;纯内存规则。线程安全(不可变 Map)。
@@ -22,24 +22,26 @@ public final class TicketStatuses {
public static final String DEAL = "已成交";
public static final String LOST = "未成交";
public static final String CLOSED = "已关闭";
public static final String HANDOFF = "转人工";
private static final Set<String> ALL = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
INQUIRING, QUOTED, NEGOTIATING, DEAL, LOST, CLOSED
INQUIRING, QUOTED, NEGOTIATING, DEAL, LOST, CLOSED, HANDOFF
)));
/**
* from -> 允许的 to 集合。
* 终态(已成交/未成交/已关闭)默认不可再流转,除非显式「关闭」从非终态进入已关闭。
* 终态:已成交/未成交/已关闭不可再流转;转人工只能再关单。
* 协商中允许再进协商中:只顺延再问,不重开询价。
*/
private static final Map<String, Set<String>> ALLOWED;
static {
Map<String, Set<String>> m = new HashMap<>();
m.put(INQUIRING, setOf(QUOTED, NEGOTIATING, CLOSED));
m.put(QUOTED, setOf(NEGOTIATING, DEAL, LOST, CLOSED));
m.put(INQUIRING, setOf(QUOTED, NEGOTIATING, CLOSED, HANDOFF));
m.put(QUOTED, setOf(NEGOTIATING, DEAL, LOST, CLOSED, HANDOFF));
// 协商中可再选协商中:顺延下一轮再问,次数不限。
m.put(NEGOTIATING, setOf(QUOTED, DEAL, LOST, CLOSED, NEGOTIATING));
m.put(NEGOTIATING, setOf(QUOTED, DEAL, LOST, CLOSED, NEGOTIATING, HANDOFF));
m.put(HANDOFF, setOf(CLOSED));
m.put(DEAL, Collections.emptySet());
m.put(LOST, Collections.emptySet());
m.put(CLOSED, Collections.emptySet());
@@ -53,6 +55,13 @@ public final class TicketStatuses {
return status != null && ALL.contains(status);
}
/**
* 转人工后询价字段与报价不再更新。
*/
public static boolean isFrozen(String status) {
return HANDOFF.equals(status);
}
/**
* 校验是否允许从 from 转到 to。
*
@@ -785,6 +785,11 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("error", "ticket_not_found");
return out;
}
if (TicketStatuses.isFrozen(ticket.getStatus())) {
out.put("ok", false);
out.put("error", "handoff_frozen");
return out;
}
Map<String, String> merged = new LinkedHashMap<>();
if (StringUtils.isNotBlank(ticket.getCollabFactsJson())) {
JSONObject old = JSON.parseObject(ticket.getCollabFactsJson());
@@ -935,6 +940,12 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("error", "ticket_not_found");
return out;
}
if (TicketStatuses.isFrozen(ticket.getStatus())) {
out.put("ok", false);
out.put("error", "handoff_frozen");
out.put("status", ticket.getStatus());
return out;
}
Object raw = body == null ? null : body.get("quote");
Map<String, Object> quote = raw instanceof Map ? (Map<String, Object>) raw : new LinkedHashMap<>();
String remark = text(body, "remark");
@@ -215,6 +215,10 @@ public class InquiryTmsQueryService {
if (ticket == null) {
return;
}
if (TicketStatuses.isFrozen(ticket.getStatus())) {
log.info("tms.quote skip frozen wo={} status={}", workOrderNo, ticket.getStatus());
return;
}
InquiryQuote row = new InquiryQuote();
row.setTicketId(ticket.getId());
row.setWorkOrderNo(workOrderNo);