机器人回复补调用编码;集拼报价条带字段写到下一行,业务员落中文名。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -319,7 +319,12 @@ def apply_group_adjust(
|
|||||||
flow=flow,
|
flow=flow,
|
||||||
chat_id=room,
|
chat_id=room,
|
||||||
work_order_no=no,
|
work_order_no=no,
|
||||||
facts=_facts_for_fill(facts, collab, business_line=line),
|
facts=_facts_for_fill(
|
||||||
|
facts,
|
||||||
|
collab,
|
||||||
|
business_line=line,
|
||||||
|
sales_name=str(view.get("sales_name") or view.get("salesName") or ""),
|
||||||
|
),
|
||||||
quote=result.quote,
|
quote=result.quote,
|
||||||
sales_id=sender_id,
|
sales_id=sender_id,
|
||||||
business_line=line,
|
business_line=line,
|
||||||
|
|||||||
@@ -232,6 +232,19 @@ def overlay_labeled_facts(
|
|||||||
return {"filepath": str(tmp), "file_b64": out_b64}
|
return {"filepath": str(tmp), "file_b64": out_b64}
|
||||||
|
|
||||||
|
|
||||||
|
def _band_value_goes_below(ws, label_cell) -> bool:
|
||||||
|
"""同一行凑齐业务员、客户名称、始发地,就是集拼询价条带。"""
|
||||||
|
text = str(label_cell.value or "").replace(" ", "")
|
||||||
|
if not any(key in text for key in ("客户名称", "业务员", "始发地", "货物类型")):
|
||||||
|
return False
|
||||||
|
hits = 0
|
||||||
|
for cell in ws[label_cell.row]:
|
||||||
|
compact = str(cell.value or "").replace(" ", "").replace(":", "").replace(":", "")
|
||||||
|
if compact in {"业务员", "客户名称", "始发地", "货物类型", "条款", "发货日期"}:
|
||||||
|
hits += 1
|
||||||
|
return hits >= 3
|
||||||
|
|
||||||
|
|
||||||
def _match_collab_label(text: str) -> tuple[str, ...] | None:
|
def _match_collab_label(text: str) -> tuple[str, ...] | None:
|
||||||
raw = (text or "").replace(":", ":").strip()
|
raw = (text or "").replace(":", ":").strip()
|
||||||
for marker, keys in _COLLAB_LABEL_MARKERS:
|
for marker, keys in _COLLAB_LABEL_MARKERS:
|
||||||
@@ -241,7 +254,23 @@ def _match_collab_label(text: str) -> tuple[str, ...] | None:
|
|||||||
|
|
||||||
|
|
||||||
def _label_value_cell(ws, label_cell):
|
def _label_value_cell(ws, label_cell):
|
||||||
"""标签右侧格子;合并标签则从合并区右边开始。"""
|
"""标签右侧格子;合并标签则从合并区右边开始。
|
||||||
|
|
||||||
|
跨境集拼蓝条是「业务员 | 客户名称 | 始发地」,值在下一行。
|
||||||
|
写到右侧会把客户名称填进「始发地」表头。
|
||||||
|
"""
|
||||||
|
if _band_value_goes_below(ws, label_cell):
|
||||||
|
col = label_cell.column
|
||||||
|
merged = getattr(ws, "merged_cells", None)
|
||||||
|
if merged is not None:
|
||||||
|
for rng in merged.ranges:
|
||||||
|
if (
|
||||||
|
rng.min_row <= label_cell.row <= rng.max_row
|
||||||
|
and rng.min_col <= label_cell.column <= rng.max_col
|
||||||
|
):
|
||||||
|
col = rng.min_col
|
||||||
|
break
|
||||||
|
return ws.cell(row=label_cell.row + 1, column=col)
|
||||||
row_idx = label_cell.row
|
row_idx = label_cell.row
|
||||||
start_col = label_cell.column + 1
|
start_col = label_cell.column + 1
|
||||||
merged = getattr(ws, "merged_cells", None)
|
merged = getattr(ws, "merged_cells", None)
|
||||||
|
|||||||
@@ -678,7 +678,12 @@ def send_filled_sheet(
|
|||||||
flow=flow,
|
flow=flow,
|
||||||
chat_id=room,
|
chat_id=room,
|
||||||
work_order_no=no,
|
work_order_no=no,
|
||||||
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
|
facts=_facts_for_fill(
|
||||||
|
facts,
|
||||||
|
collab,
|
||||||
|
business_line=line or "SEA",
|
||||||
|
sales_name=str(view.get("sales_name") or view.get("salesName") or ""),
|
||||||
|
),
|
||||||
quote=quote,
|
quote=quote,
|
||||||
sales_id=str(
|
sales_id=str(
|
||||||
(sess.sender_id if sess else "")
|
(sess.sender_id if sess else "")
|
||||||
@@ -724,6 +729,7 @@ def _facts_for_fill(
|
|||||||
facts: dict[str, str],
|
facts: dict[str, str],
|
||||||
collab: dict[str, str],
|
collab: dict[str, str],
|
||||||
business_line: str = "SEA",
|
business_line: str = "SEA",
|
||||||
|
sales_name: str = "",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
询价字段 + 本单协同项。中文键和填表英文码一起带上。
|
询价字段 + 本单协同项。中文键和填表英文码一起带上。
|
||||||
@@ -744,6 +750,11 @@ def _facts_for_fill(
|
|||||||
if oem:
|
if oem:
|
||||||
merged["oil_elec_mag"] = oem
|
merged["oil_elec_mag"] = oem
|
||||||
merged["是否含油,电,磁"] = oem
|
merged["是否含油,电,磁"] = oem
|
||||||
|
# 报价单业务员写中文名。企微账号留给主账去对员工表,不要盖掉已有姓名。
|
||||||
|
name = (sales_name or "").strip()
|
||||||
|
if name and any("\u4e00" <= ch <= "\u9fff" for ch in name):
|
||||||
|
merged["sales_name"] = name
|
||||||
|
merged["业务员"] = name
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
@@ -1056,7 +1067,12 @@ def _persist_quote_sheet(
|
|||||||
result = renderer(
|
result = renderer(
|
||||||
work_order_no=no,
|
work_order_no=no,
|
||||||
business_line=line,
|
business_line=line,
|
||||||
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
|
facts=_facts_for_fill(
|
||||||
|
facts,
|
||||||
|
collab,
|
||||||
|
business_line=line or "SEA",
|
||||||
|
sales_name=str(view.get("sales_name") or view.get("salesName") or ""),
|
||||||
|
),
|
||||||
quote=quote,
|
quote=quote,
|
||||||
format="xlsx",
|
format="xlsx",
|
||||||
sales_wecom_id=str(
|
sales_wecom_id=str(
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ from agent.redis_coord.runtime import get_redis_runtime
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_TTL_SEC = 24 * 3600
|
|
||||||
|
|
||||||
|
|
||||||
class BookmarkStore(Protocol):
|
class BookmarkStore(Protocol):
|
||||||
"""单测可注入;生产走 Redis。"""
|
"""单测可注入;生产走 Redis。"""
|
||||||
@@ -72,10 +70,10 @@ def save_flow_bookmark(
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
client = get_redis_runtime().client
|
client = get_redis_runtime().client
|
||||||
|
# 核对卡不设过期。确定前一直留着,离开核对或建单后由 delete 清掉。
|
||||||
client.raw.set(
|
client.raw.set(
|
||||||
client.key(FLOW_BOOKMARK, name, sid),
|
client.key(FLOW_BOOKMARK, name, sid),
|
||||||
json.dumps(payload, ensure_ascii=False),
|
json.dumps(payload, ensure_ascii=False),
|
||||||
ex=_TTL_SEC,
|
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("写询价书签失败 kind=%s sender=%s", name, sid)
|
logger.exception("写询价书签失败 kind=%s sender=%s", name, sid)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ from agent.redis_coord.runtime import get_redis_runtime
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_TTL_SEC = 24 * 3600
|
|
||||||
|
|
||||||
|
|
||||||
def save_pending_land_confirm(
|
def save_pending_land_confirm(
|
||||||
sender_id: str,
|
sender_id: str,
|
||||||
@@ -42,10 +40,10 @@ def save_pending_land_confirm(
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
client = get_redis_runtime().client
|
client = get_redis_runtime().client
|
||||||
|
# 核对卡不设过期。建单成功后由 clear 清掉。
|
||||||
client.raw.set(
|
client.raw.set(
|
||||||
client.key(PENDING_LAND_CONFIRM, sid),
|
client.key(PENDING_LAND_CONFIRM, sid),
|
||||||
json.dumps(payload, ensure_ascii=False),
|
json.dumps(payload, ensure_ascii=False),
|
||||||
ex=_TTL_SEC,
|
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"land.pending saved sender=%s phase=%s wo=%s keys=%s",
|
"land.pending saved sender=%s phase=%s wo=%s keys=%s",
|
||||||
@@ -59,7 +57,7 @@ def save_pending_land_confirm(
|
|||||||
|
|
||||||
|
|
||||||
def load_pending_land_confirm(sender_id: str) -> dict[str, Any]:
|
def load_pending_land_confirm(sender_id: str) -> dict[str, Any]:
|
||||||
"""取出待确定陆运字段。没有或过期返回空。"""
|
"""取出待确定陆运字段。没有则空。"""
|
||||||
sid = (sender_id or "").strip()
|
sid = (sender_id or "").strip()
|
||||||
if not sid:
|
if not sid:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
+12
-13
@@ -16,12 +16,12 @@ import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 机器人回复语 API。
|
* 机器人回复语 API。
|
||||||
* 支持按分类拉取全部条目(后台九分类页)与分页查询。
|
* 支持按分类拉取全部条目(后台九分类页)与分页查询。
|
||||||
|
* 调用编码 replyCode 给智能体日后按码取文案;本期只维护、不向智能体下发。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Api(tags = "询价-机器人回复语")
|
@Api(tags = "询价-机器人回复语")
|
||||||
@@ -50,13 +50,20 @@ public class InquiryBotReplyController extends JeecgController<InquiryBotReply,
|
|||||||
return Result.OK(botReplyService.page(new Page<>(pageNo, pageSize), qw));
|
return Result.OK(botReplyService.page(new Page<>(pageNo, pageSize), qw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量列表,供分类页一次加载。
|
||||||
|
* 必须显式大分页:Jeecg 分页拦截器可能把无 Page 的 list() 收成前 10 条,后台会看起来像没显示全。
|
||||||
|
*/
|
||||||
@ApiOperation("全量列表(按分类排序,供分类页一次加载)")
|
@ApiOperation("全量列表(按分类排序,供分类页一次加载)")
|
||||||
@RequiresPermissions("inquiry:reply:list")
|
@RequiresPermissions("inquiry:reply:list")
|
||||||
@GetMapping("/listAll")
|
@GetMapping("/listAll")
|
||||||
public Result<List<InquiryBotReply>> listAll() {
|
public Result<List<InquiryBotReply>> listAll() {
|
||||||
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
|
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
|
||||||
qw.orderByAsc("category_code").orderByAsc("sort_no");
|
qw.orderByAsc("category_code").orderByAsc("sort_no").orderByAsc("id");
|
||||||
return Result.OK(botReplyService.list(qw));
|
Page<InquiryBotReply> page = new Page<>(1, 2000);
|
||||||
|
page.setSearchCount(false);
|
||||||
|
page.setMaxLimit(2000L);
|
||||||
|
return Result.OK(botReplyService.page(page, qw).getRecords());
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoLog(value = "回复语-新增")
|
@AutoLog(value = "回复语-新增")
|
||||||
@@ -64,14 +71,7 @@ public class InquiryBotReplyController extends JeecgController<InquiryBotReply,
|
|||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
public Result<String> add(@RequestBody InquiryBotReply body) {
|
public Result<String> add(@RequestBody InquiryBotReply body) {
|
||||||
validate(body);
|
validate(body);
|
||||||
body.setCreateTime(new Date());
|
botReplyService.createReply(body);
|
||||||
if (body.getEnabled() == null) {
|
|
||||||
body.setEnabled(1);
|
|
||||||
}
|
|
||||||
if (body.getSortNo() == null) {
|
|
||||||
body.setSortNo(0);
|
|
||||||
}
|
|
||||||
botReplyService.save(body);
|
|
||||||
return Result.OK("添加成功");
|
return Result.OK("添加成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +83,7 @@ public class InquiryBotReplyController extends JeecgController<InquiryBotReply,
|
|||||||
return Result.error("id 不能为空");
|
return Result.error("id 不能为空");
|
||||||
}
|
}
|
||||||
validate(body);
|
validate(body);
|
||||||
body.setUpdateTime(new Date());
|
botReplyService.updateReply(body);
|
||||||
botReplyService.updateById(body);
|
|
||||||
return Result.OK("更新成功");
|
return Result.OK("更新成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -16,7 +16,8 @@ import java.util.Date;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 机器人回复语,对应 inq_bot_reply。
|
* 机器人回复语,对应 inq_bot_reply。
|
||||||
* 按分类维护触发场景与正文;智能体出站文案可后续读取本表(本期后台 CRUD)。
|
* 按分类维护触发场景与正文;列表以现网整理表为准(migration_inq_bot_reply_replace_v1)。
|
||||||
|
* 智能体出站文案可后续按场景读取本表。
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@@ -36,6 +37,13 @@ public class InquiryBotReply implements Serializable {
|
|||||||
@ApiModelProperty("分类标题")
|
@ApiModelProperty("分类标题")
|
||||||
private String categoryTitle;
|
private String categoryTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体日后按此码取文案,与触发说明解耦。
|
||||||
|
* 系统预置项只读;空表示尚未接入智能体。
|
||||||
|
*/
|
||||||
|
@ApiModelProperty("调用编码")
|
||||||
|
private String replyCode;
|
||||||
|
|
||||||
@ApiModelProperty("触发场景")
|
@ApiModelProperty("触发场景")
|
||||||
private String triggerScene;
|
private String triggerScene;
|
||||||
|
|
||||||
|
|||||||
+13
@@ -2,8 +2,21 @@ package org.jeecg.modules.inquiry.mapper;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 询价员工岗位。报价单业务员要中文名时,员工表没有姓名再读后台账号 realname。
|
||||||
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface InquiryStaffRoleMapper extends BaseMapper<InquiryStaffRole> {
|
public interface InquiryStaffRoleMapper extends BaseMapper<InquiryStaffRole> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台账号中文名。工号 work_no 与企微 userid 相同。
|
||||||
|
* 只读,查不到返回空。
|
||||||
|
*/
|
||||||
|
@Select("SELECT realname FROM sys_user WHERE del_flag = 0 AND status = 1 AND work_no = #{wecomId} "
|
||||||
|
+ "AND realname IS NOT NULL AND TRIM(realname) <> '' LIMIT 1")
|
||||||
|
String findRealnameByWorkNo(@Param("wecomId") String wecomId);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -5,10 +5,21 @@ import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 机器人回复语服务。
|
* 机器人回复语服务。
|
||||||
|
* 预置调用编码不可改;编码全局唯一(空值允许多条未接入项)。
|
||||||
*/
|
*/
|
||||||
public interface IInquiryBotReplyService extends IService<InquiryBotReply> {
|
public interface IInquiryBotReplyService extends IService<InquiryBotReply> {
|
||||||
|
|
||||||
void enable(String id);
|
void enable(String id);
|
||||||
|
|
||||||
void disable(String id);
|
void disable(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增。校验调用编码格式与唯一;空白编码存 null。
|
||||||
|
*/
|
||||||
|
void createReply(InquiryBotReply body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑正文/启停。已有调用编码不可被请求覆盖。
|
||||||
|
*/
|
||||||
|
void updateReply(InquiryBotReply body);
|
||||||
}
|
}
|
||||||
|
|||||||
+66
@@ -1,6 +1,8 @@
|
|||||||
package org.jeecg.modules.inquiry.service.impl;
|
package org.jeecg.modules.inquiry.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
||||||
import org.jeecg.modules.inquiry.mapper.InquiryBotReplyMapper;
|
import org.jeecg.modules.inquiry.mapper.InquiryBotReplyMapper;
|
||||||
import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
|
import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
|
||||||
@@ -8,14 +10,19 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回复语启停与 CRUD。
|
* 回复语启停与 CRUD。
|
||||||
|
* 副作用:写 inq_bot_reply;调用编码唯一。不打 TMS、不唤醒智能体。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMapper, InquiryBotReply>
|
public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMapper, InquiryBotReply>
|
||||||
implements IInquiryBotReplyService {
|
implements IInquiryBotReplyService {
|
||||||
|
|
||||||
|
/** 调用编码:小写字母开头,点分段,供智能体日后按码查找。 */
|
||||||
|
private static final Pattern REPLY_CODE = Pattern.compile("^[a-z][a-z0-9_.]{1,62}$");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void enable(String id) {
|
public void enable(String id) {
|
||||||
@@ -28,6 +35,39 @@ public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMappe
|
|||||||
setEnabled(id, 0);
|
setEnabled(id, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void createReply(InquiryBotReply body) {
|
||||||
|
normalizeCode(body);
|
||||||
|
assertCodeUnique(body.getReplyCode(), null);
|
||||||
|
if (body.getEnabled() == null) {
|
||||||
|
body.setEnabled(1);
|
||||||
|
}
|
||||||
|
if (body.getSortNo() == null) {
|
||||||
|
body.setSortNo(0);
|
||||||
|
}
|
||||||
|
body.setCreateTime(new Date());
|
||||||
|
save(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateReply(InquiryBotReply body) {
|
||||||
|
InquiryBotReply old = getById(body.getId());
|
||||||
|
if (old == null) {
|
||||||
|
throw new IllegalArgumentException("回复语不存在: " + body.getId());
|
||||||
|
}
|
||||||
|
// 已落库的系统编码不能改,避免智能体日后对不上。
|
||||||
|
if (StringUtils.isNotBlank(old.getReplyCode())) {
|
||||||
|
body.setReplyCode(old.getReplyCode());
|
||||||
|
} else {
|
||||||
|
normalizeCode(body);
|
||||||
|
assertCodeUnique(body.getReplyCode(), old.getId());
|
||||||
|
}
|
||||||
|
body.setUpdateTime(new Date());
|
||||||
|
updateById(body);
|
||||||
|
}
|
||||||
|
|
||||||
private void setEnabled(String id, int enabled) {
|
private void setEnabled(String id, int enabled) {
|
||||||
InquiryBotReply row = getById(id);
|
InquiryBotReply row = getById(id);
|
||||||
if (row == null) {
|
if (row == null) {
|
||||||
@@ -37,4 +77,30 @@ public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMappe
|
|||||||
row.setUpdateTime(new Date());
|
row.setUpdateTime(new Date());
|
||||||
updateById(row);
|
updateById(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalizeCode(InquiryBotReply body) {
|
||||||
|
String code = StringUtils.trimToNull(body.getReplyCode());
|
||||||
|
if (code == null) {
|
||||||
|
body.setReplyCode(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!REPLY_CODE.matcher(code).matches()) {
|
||||||
|
throw new IllegalArgumentException("调用编码格式不正确,须为小写字母开头的点分段码");
|
||||||
|
}
|
||||||
|
body.setReplyCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCodeUnique(String replyCode, String excludeId) {
|
||||||
|
if (StringUtils.isBlank(replyCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
|
||||||
|
qw.eq("reply_code", replyCode);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
qw.ne("id", excludeId);
|
||||||
|
}
|
||||||
|
if (count(qw) > 0) {
|
||||||
|
throw new IllegalArgumentException("调用编码已存在: " + replyCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-8
@@ -3,6 +3,7 @@ package org.jeecg.modules.inquiry.service.impl;
|
|||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -13,8 +14,13 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.modules.inquiry.entity.InquiryQuoteTemplate;
|
import org.jeecg.modules.inquiry.entity.InquiryQuoteTemplate;
|
||||||
import org.jeecg.modules.inquiry.entity.InquiryQuoteTemplateVersion;
|
import org.jeecg.modules.inquiry.entity.InquiryQuoteTemplateVersion;
|
||||||
|
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||||
|
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||||
import org.jeecg.modules.inquiry.mapper.InquiryQuoteTemplateMapper;
|
import org.jeecg.modules.inquiry.mapper.InquiryQuoteTemplateMapper;
|
||||||
import org.jeecg.modules.inquiry.mapper.InquiryQuoteTemplateVersionMapper;
|
import org.jeecg.modules.inquiry.mapper.InquiryQuoteTemplateVersionMapper;
|
||||||
|
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
|
||||||
|
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
|
||||||
|
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
|
||||||
import org.jeecg.modules.inquiry.service.IInquiryQuoteTemplateService;
|
import org.jeecg.modules.inquiry.service.IInquiryQuoteTemplateService;
|
||||||
import org.jeecg.modules.inquiry.template.QuoteTemplateAiParseClient;
|
import org.jeecg.modules.inquiry.template.QuoteTemplateAiParseClient;
|
||||||
import org.jeecg.modules.inquiry.template.QuoteTemplateExcelSupport;
|
import org.jeecg.modules.inquiry.template.QuoteTemplateExcelSupport;
|
||||||
@@ -65,6 +71,14 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
@Autowired
|
@Autowired
|
||||||
private InquiryQuoteTemplateVersionMapper versionMapper;
|
private InquiryQuoteTemplateVersionMapper versionMapper;
|
||||||
|
|
||||||
|
/** 出单时把企微账号换成员工管理里的姓名。测试构造服务时可以没有。 */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private InquiryStaffRoleMapper staffRoleMapper;
|
||||||
|
|
||||||
|
/** 出单时补工单上的客户名称。测试构造服务时可以没有。 */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private InquiryTicketMapper ticketMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private QuoteTemplateObjectStore objectStore;
|
private QuoteTemplateObjectStore objectStore;
|
||||||
|
|
||||||
@@ -756,7 +770,8 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
log.warn("报价出单 mappingJson 非法 wo={} err={}", workOrderNo, e.getMessage());
|
log.warn("报价出单 mappingJson 非法 wo={} err={}", workOrderNo, e.getMessage());
|
||||||
return fail;
|
return fail;
|
||||||
}
|
}
|
||||||
JSONObject fillFacts = buildFillFacts(workOrderNo, facts, quote, salesWecomId, businessLine);
|
JSONObject fillFacts = buildFillFacts(
|
||||||
|
workOrderNo, enrichQuotePartyFacts(workOrderNo, facts, salesWecomId), quote, salesWecomId, businessLine);
|
||||||
String safeNo = StringUtils.defaultIfBlank(workOrderNo, "WO").replaceAll("[^A-Za-z0-9_-]", "");
|
String safeNo = StringUtils.defaultIfBlank(workOrderNo, "WO").replaceAll("[^A-Za-z0-9_-]", "");
|
||||||
Path targetDir = Paths.get(StringUtils.defaultIfBlank(uploadPath, "."), BIZ_PATH, "render");
|
Path targetDir = Paths.get(StringUtils.defaultIfBlank(uploadPath, "."), BIZ_PATH, "render");
|
||||||
Path target = targetDir.resolve(safeNo + "-" + System.currentTimeMillis() + ".xlsx");
|
Path target = targetDir.resolve(safeNo + "-" + System.currentTimeMillis() + ".xlsx");
|
||||||
@@ -791,6 +806,108 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
return buildFillFacts(workOrderNo, facts, quote, salesWecomId, null);
|
return buildFillFacts(workOrderNo, facts, quote, salesWecomId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出单前补业务员姓名和客户名称。
|
||||||
|
* <p>
|
||||||
|
* 业务员:员工管理姓名优先,其次工单上已有的中文名。企微 userid 不写进报价单。
|
||||||
|
* 客户名称:询价事实或工单客户。没填就空着,不用业务员账号去占。
|
||||||
|
* 副作用:只读员工表和工单,不改库。
|
||||||
|
*/
|
||||||
|
private Map<String, Object> enrichQuotePartyFacts(String workOrderNo, Map<String, Object> facts,
|
||||||
|
String salesWecomId) {
|
||||||
|
Map<String, Object> src = facts == null ? new LinkedHashMap<>() : new LinkedHashMap<>(facts);
|
||||||
|
InquiryTicket ticket = findTicket(workOrderNo);
|
||||||
|
String wecomId = StringUtils.defaultIfBlank(salesWecomId, ticket == null ? "" : ticket.getSalesWecomId());
|
||||||
|
String staffName = lookupStaffName(wecomId);
|
||||||
|
String fromTicket = chineseName(ticket == null ? "" : ticket.getSalesName());
|
||||||
|
String fromFacts = chineseName(first(src, "sales_name", "业务员", "salesName"));
|
||||||
|
String fromJson = chineseName(factFromTicketJson(ticket, "业务员", "sales_name", "salesName"));
|
||||||
|
String named = StringUtils.defaultIfBlank(staffName, StringUtils.defaultIfBlank(fromTicket,
|
||||||
|
StringUtils.defaultIfBlank(fromFacts, fromJson)));
|
||||||
|
if (StringUtils.isNotBlank(named)) {
|
||||||
|
src.put("sales_name", named);
|
||||||
|
src.put("业务员", named);
|
||||||
|
} else {
|
||||||
|
src.put("sales_name", "");
|
||||||
|
src.put("业务员", "");
|
||||||
|
}
|
||||||
|
String customer = chineseName(first(src, "customer_name", "客户名称", "customerName"));
|
||||||
|
if (StringUtils.isBlank(customer) && ticket != null) {
|
||||||
|
customer = chineseName(ticket.getCustomerName());
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(customer)) {
|
||||||
|
customer = chineseName(factFromTicketJson(ticket, "客户名称", "customer_name", "customerName"));
|
||||||
|
}
|
||||||
|
if (customer.equals(named) || customer.equals(StringUtils.trimToEmpty(wecomId))) {
|
||||||
|
customer = "";
|
||||||
|
}
|
||||||
|
src.put("customer_name", customer);
|
||||||
|
src.put("客户名称", customer);
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按工单号读主单。没有表或没有单号时返回空,出单仍继续。 */
|
||||||
|
private InquiryTicket findTicket(String workOrderNo) {
|
||||||
|
if (ticketMapper == null || StringUtils.isBlank(workOrderNo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ticketMapper.selectOne(new LambdaQueryWrapper<InquiryTicket>()
|
||||||
|
.eq(InquiryTicket::getWorkOrderNo, workOrderNo.trim())
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 员工中文名。先员工管理,没有再读后台账号的真实姓名。
|
||||||
|
* 企微账号、纯英文拼音不算姓名。查不到返回空,报价单不回退成账号。
|
||||||
|
*/
|
||||||
|
private String lookupStaffName(String salesWecomId) {
|
||||||
|
if (staffRoleMapper == null || StringUtils.isBlank(salesWecomId)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String uid = salesWecomId.trim();
|
||||||
|
List<InquiryStaffRole> rows = staffRoleMapper.selectList(new LambdaQueryWrapper<InquiryStaffRole>()
|
||||||
|
.eq(InquiryStaffRole::getWecomId, uid)
|
||||||
|
.eq(InquiryStaffRole::getStatus, "active"));
|
||||||
|
if (rows != null) {
|
||||||
|
for (InquiryStaffRole row : rows) {
|
||||||
|
String name = chineseName(row == null ? "" : row.getName());
|
||||||
|
if (StringUtils.isNotBlank(name)) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chineseName(staffRoleMapper.findRealnameByWorkNo(uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工单事实里的中文名。factsJson 不是合法 JSON 时当没有。 */
|
||||||
|
private static String factFromTicketJson(InquiryTicket ticket, String... keys) {
|
||||||
|
if (ticket == null || StringUtils.isBlank(ticket.getFactsJson())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSONObject obj = JSON.parseObject(ticket.getFactsJson());
|
||||||
|
return obj == null ? "" : first(obj, keys);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 报价单只收中文姓名。企微账号和纯英文拼音返回空。 */
|
||||||
|
private static String chineseName(String raw) {
|
||||||
|
String text = StringUtils.trimToEmpty(raw);
|
||||||
|
if (StringUtils.isBlank(text) || InquiryTicketDisplaySupport.looksLikeWecomUserId(text)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
boolean han = false;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
if (Character.UnicodeScript.of(text.charAt(i)) == Character.UnicodeScript.HAN) {
|
||||||
|
han = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return han ? text : "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 出单填表投影。空运不写时效:业务确认空运没有时效字段。
|
* 出单填表投影。空运不写时效:业务确认空运没有时效字段。
|
||||||
*
|
*
|
||||||
@@ -905,7 +1022,15 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
if (StringUtils.isNotBlank(currency)) {
|
if (StringUtils.isNotBlank(currency)) {
|
||||||
out.put("currency", currency);
|
out.put("currency", currency);
|
||||||
}
|
}
|
||||||
out.put("sales_name", StringUtils.defaultString(salesWecomId));
|
// 报价单「业务员」「客户名称」只写中文。企微账号 LinMianXun 不回退到格子里。
|
||||||
|
String named = chineseName(first(src, "sales_name", "业务员", "salesName"));
|
||||||
|
out.put("sales_name", named);
|
||||||
|
String customer = chineseName(first(src, "customer_name", "客户名称", "customerName"));
|
||||||
|
if (customer.equals(named) || customer.equals(StringUtils.trimToEmpty(salesWecomId))) {
|
||||||
|
customer = "";
|
||||||
|
}
|
||||||
|
out.put("customer_name", customer);
|
||||||
|
out.put("客户名称", customer);
|
||||||
boolean hideAirEta = looksAirInquiry(src, q, businessLine);
|
boolean hideAirEta = looksAirInquiry(src, q, businessLine);
|
||||||
String eta = first(q, "eta", "transit_days", "transitDays");
|
String eta = first(q, "eta", "transit_days", "transitDays");
|
||||||
if (!hideAirEta && StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
|
if (!hideAirEta && StringUtils.isNotBlank(eta) && !"-".equals(eta)) {
|
||||||
@@ -1013,9 +1138,12 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 陆运线路:透传 landSegments / route_rows,供 routeRegions 填项目一
|
// 陆运线路:透传 landSegments / route_rows,供 routeRegions 填项目一
|
||||||
JSONArray landSegs = QuoteTemplateExcelSupport.parseFeeRows(q.get("route_rows"));
|
JSONArray routeRows = QuoteTemplateExcelSupport.parseFeeRows(q.get("route_rows"));
|
||||||
if (landSegs.isEmpty()) {
|
JSONArray rawLand = QuoteTemplateExcelSupport.parseFeeRows(q.get("landSegments"));
|
||||||
landSegs = QuoteTemplateExcelSupport.parseFeeRows(q.get("landSegments"));
|
JSONArray landSegs = routeRows.isEmpty() ? rawLand : routeRows;
|
||||||
|
// 已落库的 route_rows 是规范化扁平行,分档公斤价还在原始 landSegments 上。
|
||||||
|
if (!routeRows.isEmpty()) {
|
||||||
|
QuoteTemplateExcelSupport.copyMissingWeightPrices(landSegs, rawLand);
|
||||||
}
|
}
|
||||||
if (!landSegs.isEmpty()) {
|
if (!landSegs.isEmpty()) {
|
||||||
out.put("landSegments", landSegs);
|
out.put("landSegments", landSegs);
|
||||||
@@ -1077,9 +1205,9 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
|||||||
one.put("salePrice", landAmt);
|
one.put("salePrice", landAmt);
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(one.getString("origin")) || StringUtils.isNotBlank(one.getString("destination"))) {
|
if (StringUtils.isNotBlank(one.getString("origin")) || StringUtils.isNotBlank(one.getString("destination"))) {
|
||||||
JSONArray routeRows = new JSONArray();
|
JSONArray fallbackRows = new JSONArray();
|
||||||
routeRows.add(one);
|
fallbackRows.add(one);
|
||||||
out.put("route_rows", routeRows);
|
out.put("route_rows", fallbackRows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hideAirEta) {
|
if (hideAirEta) {
|
||||||
|
|||||||
+677
-7
@@ -9,6 +9,7 @@ import org.apache.poi.ss.usermodel.*;
|
|||||||
import org.apache.poi.ss.util.CellReference;
|
import org.apache.poi.ss.util.CellReference;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFCell;
|
import org.apache.poi.xssf.usermodel.XSSFCell;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFColor;
|
import org.apache.poi.xssf.usermodel.XSSFColor;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFFont;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -104,6 +105,8 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
FILL_BY_LABEL.put("报价单编号", "quote_no");
|
FILL_BY_LABEL.put("报价单编号", "quote_no");
|
||||||
FILL_BY_LABEL.put("报价编号", "quote_no");
|
FILL_BY_LABEL.put("报价编号", "quote_no");
|
||||||
FILL_BY_LABEL.put("贸易条款", "trade_terms");
|
FILL_BY_LABEL.put("贸易条款", "trade_terms");
|
||||||
|
// 跨境集拼表头是「条款」,不是「贸易条款」。不对上的话,销售填了条款也写不进这一列。
|
||||||
|
FILL_BY_LABEL.put("条款", "trade_terms");
|
||||||
FILL_BY_LABEL.put("货好时间", "cargo_ready_date");
|
FILL_BY_LABEL.put("货好时间", "cargo_ready_date");
|
||||||
FILL_BY_LABEL.put("商品海关编码", "hs_code");
|
FILL_BY_LABEL.put("商品海关编码", "hs_code");
|
||||||
FILL_BY_LABEL.put("HS编码", "hs_code");
|
FILL_BY_LABEL.put("HS编码", "hs_code");
|
||||||
@@ -611,6 +614,7 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
}
|
}
|
||||||
JSONObject fieldMeta = mapping.getJSONObject("fieldMeta");
|
JSONObject fieldMeta = mapping.getJSONObject("fieldMeta");
|
||||||
JSONObject fields = mapping.getJSONObject("fields");
|
JSONObject fields = mapping.getJSONObject("fields");
|
||||||
|
LandTemplateLook landLook = snapshotLandTemplateLook(wb);
|
||||||
if (fields != null) {
|
if (fields != null) {
|
||||||
// 陆运拼车等模板常把「报价日期」与「有效期」误绑同一格(如 H5「报价日期:」)。
|
// 陆运拼车等模板常把「报价日期」与「有效期」误绑同一格(如 H5「报价日期:」)。
|
||||||
// 同格时只写出报价日期,避免 TMS valid_until 盖掉日期标签含义。
|
// 同格时只写出报价日期,避免 TMS valid_until 盖掉日期标签含义。
|
||||||
@@ -669,6 +673,10 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 映射常把业务员指到「客户名称」表头。销售没填客户时,不能把业务员名字写进那一列。
|
||||||
|
if (mappingPointsAtForeignBandLabel(wb, cellRef, code)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
cellRef = redirectLandHeaderBandMapping(wb, cellRef);
|
cellRef = redirectLandHeaderBandMapping(wb, cellRef);
|
||||||
writeCell(wb, cellRef, String.valueOf(val));
|
writeCell(wb, cellRef, String.valueOf(val));
|
||||||
}
|
}
|
||||||
@@ -711,6 +719,8 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
overwriteEstimateTotal(wb, facts);
|
overwriteEstimateTotal(wb, facts);
|
||||||
// 无费用明细时:清掉误写入费用名称列的起运港/目的港(WO202609280006)
|
// 无费用明细时:清掉误写入费用名称列的起运港/目的港(WO202609280006)
|
||||||
clearPortSpillInFeeNameColumn(wb, facts);
|
clearPortSpillInFeeNameColumn(wb, facts);
|
||||||
|
// 跨境集拼:列头必须和空白模板一样(蓝底白字)。POI 写回后币种/燃油会变成大红字。
|
||||||
|
restoreLandTemplateLook(wb, landLook);
|
||||||
// 转 PDF 前把纸张改成 A4 居中,避免顶部裁字、整页偏左。
|
// 转 PDF 前把纸张改成 A4 居中,避免顶部裁字、整页偏左。
|
||||||
applyQuotePrintLayout(wb);
|
applyQuotePrintLayout(wb);
|
||||||
Files.createDirectories(targetXlsx.getParent());
|
Files.createDirectories(targetXlsx.getParent());
|
||||||
@@ -2662,6 +2672,12 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
if (sheet == null) {
|
if (sheet == null) {
|
||||||
sheet = wb.getSheetAt(0);
|
sheet = wb.getSheetAt(0);
|
||||||
}
|
}
|
||||||
|
// 配送费用是印好的城市单价(200/450、当日达),不是运价线路。
|
||||||
|
// 写进去会把始发地、币种填进城市类别和燃油附加费率。
|
||||||
|
int deliveryHeader = region.getIntValue("headerRow") - 1;
|
||||||
|
if (deliveryHeader >= 0 && rowLooksLikeDeliveryPriceHeader(sheet, deliveryHeader)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// 中港等模板:dataStartRow 常误指「4.2米/3T」车型表头行,下移到真正数据行
|
// 中港等模板:dataStartRow 常误指「4.2米/3T」车型表头行,下移到真正数据行
|
||||||
rowIdx0 = resolveRouteDataStartRow(sheet, rowIdx0, region.getIntValue("headerRow"));
|
rowIdx0 = resolveRouteDataStartRow(sheet, rowIdx0, region.getIntValue("headerRow"));
|
||||||
int stopRow = findStopRow(sheet, region.getJSONArray("stopAtText"), rowIdx0);
|
int stopRow = findStopRow(sheet, region.getJSONArray("stopAtText"), rowIdx0);
|
||||||
@@ -2724,7 +2740,8 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
}
|
}
|
||||||
int cur = rowIdx0;
|
int cur = rowIdx0;
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
if (rowLooksLikeVehicleHeader(sheet, cur) || rowLooksLikeLandRouteMatrixHeader(sheet, cur)) {
|
if (rowLooksLikeVehicleHeader(sheet, cur) || rowLooksLikeLandRouteMatrixHeader(sheet, cur)
|
||||||
|
|| rowLooksLikeDeliveryPriceHeader(sheet, cur)) {
|
||||||
cur++;
|
cur++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2733,7 +2750,8 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
int header0 = headerRow1Based > 0 ? headerRow1Based - 1 : -1;
|
int header0 = headerRow1Based > 0 ? headerRow1Based - 1 : -1;
|
||||||
if (header0 >= 0 && cur == header0) {
|
if (header0 >= 0 && cur == header0) {
|
||||||
cur = header0 + 1;
|
cur = header0 + 1;
|
||||||
if (rowLooksLikeVehicleHeader(sheet, cur) || rowLooksLikeLandRouteMatrixHeader(sheet, cur)) {
|
if (rowLooksLikeVehicleHeader(sheet, cur) || rowLooksLikeLandRouteMatrixHeader(sheet, cur)
|
||||||
|
|| rowLooksLikeDeliveryPriceHeader(sheet, cur)) {
|
||||||
cur++;
|
cur++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2792,6 +2810,55 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
|| t.contains("提货费") || t.contains("口岸核放");
|
|| t.contains("提货费") || t.contains("口岸核放");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送费用表头:城市类别、覆盖区域、单价。时效、燃油附加费率已算运价列头。
|
||||||
|
* 这些字是列名,值在下一行,不能写到列名右侧的空白格。
|
||||||
|
*/
|
||||||
|
static boolean isDeliveryPriceHeaderCell(String text) {
|
||||||
|
if (StringUtils.isBlank(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = text.replaceAll("\\s+", "").trim();
|
||||||
|
return "城市类别".equals(t) || "覆盖区域".equals(t) || t.startsWith("单价");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送费用那一行:必须有城市类别、覆盖区域或「单价(元/票)」。
|
||||||
|
* 运价表自己也有燃油附加费率和时效,只凭这两列会把整段公斤运价跳过,始发地和目的地就空着。
|
||||||
|
* 已经认出是运价表头(始发地、目的地、元/KG、报关费)时,不当配送费。
|
||||||
|
*/
|
||||||
|
static boolean rowLooksLikeDeliveryPriceHeader(Sheet sheet, int rowIdx) {
|
||||||
|
if (sheet == null || rowIdx < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Row row = sheet.getRow(rowIdx);
|
||||||
|
if (row == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DataFormatter fmt = new DataFormatter();
|
||||||
|
boolean deliveryLabel = false;
|
||||||
|
boolean rateMatrix = false;
|
||||||
|
for (Cell cell : row) {
|
||||||
|
if (cell == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String text = StringUtils.trimToEmpty(fmt.formatCellValue(cell)).replaceAll("\\s+", "");
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isDeliveryPriceHeaderCell(text)) {
|
||||||
|
deliveryLabel = true;
|
||||||
|
}
|
||||||
|
if ("始发地".equals(text) || "始发站".equals(text) || "目的地".equals(text)
|
||||||
|
|| "币种".equals(text) || "最低收费".equals(text)
|
||||||
|
|| text.contains("元/KG") || text.contains("元/kg")
|
||||||
|
|| text.contains("报关费") || text.contains("提货费")) {
|
||||||
|
rateMatrix = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deliveryLabel && !rateMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
/** 行内出现「米/」「尺」「3T」「5T」等车型表头文案。 */
|
/** 行内出现「米/」「尺」「3T」「5T」等车型表头文案。 */
|
||||||
static boolean rowLooksLikeVehicleHeader(Sheet sheet, int rowIdx) {
|
static boolean rowLooksLikeVehicleHeader(Sheet sheet, int rowIdx) {
|
||||||
if (sheet == null || rowIdx < 0) {
|
if (sheet == null || rowIdx < 0) {
|
||||||
@@ -2857,6 +2924,10 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
rowObj == null ? null : rowObj.getString("pricePerCbm"),
|
rowObj == null ? null : rowObj.getString("pricePerCbm"),
|
||||||
facts == null ? null : facts.getString("price_per_cbm"));
|
facts == null ? null : facts.getString("price_per_cbm"));
|
||||||
}
|
}
|
||||||
|
// 跨境集拼是分档公斤价(2-500KG=12 …),不是把同一个 price_per_kg 灌进每一列。
|
||||||
|
if (looksLikeKgBracketHeader(key)) {
|
||||||
|
return matchWeightBracketPrice(rowObj, key);
|
||||||
|
}
|
||||||
if (key.toUpperCase(Locale.ROOT).contains("KG") || key.contains("公斤")) {
|
if (key.toUpperCase(Locale.ROOT).contains("KG") || key.contains("公斤")) {
|
||||||
return firstNonBlank(rowObj == null ? null : rowObj.getString("price_per_kg"),
|
return firstNonBlank(rowObj == null ? null : rowObj.getString("price_per_kg"),
|
||||||
rowObj == null ? null : rowObj.getString("pricePerKg"),
|
rowObj == null ? null : rowObj.getString("pricePerKg"),
|
||||||
@@ -2884,6 +2955,156 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表头是「2-500KG」「5001KG以上」这类分档,不是「元/KG」单价列。
|
||||||
|
*/
|
||||||
|
static boolean looksLikeKgBracketHeader(String headerKey) {
|
||||||
|
return parseKgBracket(headerKey) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用 landSegments.weightPrices 对上分档表头。对不上就空着,禁止把一档价抄进所有列。
|
||||||
|
*/
|
||||||
|
static String matchWeightBracketPrice(JSONObject rowObj, String headerKey) {
|
||||||
|
JSONArray tiers = weightPricesOf(rowObj);
|
||||||
|
int[] range = parseKgBracket(headerKey);
|
||||||
|
if (tiers == null || tiers.isEmpty() || range == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (int i = 0; i < tiers.size(); i++) {
|
||||||
|
JSONObject tier = tiers.getJSONObject(i);
|
||||||
|
if (tier == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int min = intOf(tier.get("min"));
|
||||||
|
boolean open = tier.get("max") == null || StringUtils.isBlank(String.valueOf(tier.get("max")));
|
||||||
|
int max = open ? -1 : intOf(tier.get("max"));
|
||||||
|
if (min == range[0] && max == range[1]) {
|
||||||
|
return formatTierPrice(tier.get("price"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化 route_rows 会丢掉 weightPrices。出单前从原始 landSegments 补回。
|
||||||
|
*/
|
||||||
|
public static void copyMissingWeightPrices(JSONArray rows, JSONArray rawLand) {
|
||||||
|
if (rows == null || rawLand == null || rawLand.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
JSONObject row = rows.getJSONObject(i);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONArray existing = weightPricesOf(row);
|
||||||
|
if (existing != null && !existing.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONObject src = i < rawLand.size() ? rawLand.getJSONObject(i) : null;
|
||||||
|
if (weightPricesOf(src) == null) {
|
||||||
|
src = findLandSegmentByOriginDest(rawLand, row);
|
||||||
|
}
|
||||||
|
JSONArray tiers = weightPricesOf(src);
|
||||||
|
if (tiers != null && !tiers.isEmpty()) {
|
||||||
|
row.put("weightPrices", tiers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject findLandSegmentByOriginDest(JSONArray rawLand, JSONObject row) {
|
||||||
|
if (rawLand == null || row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String origin = StringUtils.trimToEmpty(firstNonBlank(row.getString("origin"), row.getString("originCode")));
|
||||||
|
String dest = StringUtils.trimToEmpty(firstNonBlank(row.getString("destination"), row.getString("destinationCode")));
|
||||||
|
for (int i = 0; i < rawLand.size(); i++) {
|
||||||
|
JSONObject one = rawLand.getJSONObject(i);
|
||||||
|
if (one == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String o = StringUtils.trimToEmpty(firstNonBlank(one.getString("origin"), one.getString("originCode")));
|
||||||
|
String d = StringUtils.trimToEmpty(firstNonBlank(one.getString("destination"), one.getString("destinationCode")));
|
||||||
|
if (origin.equals(o) && dest.equals(d) && weightPricesOf(one) != null) {
|
||||||
|
return one;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSONArray weightPricesOf(JSONObject row) {
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object raw = row.get("weightPrices");
|
||||||
|
if (raw == null) {
|
||||||
|
raw = row.get("weight_prices");
|
||||||
|
}
|
||||||
|
if (raw instanceof JSONArray) {
|
||||||
|
return ((JSONArray) raw).isEmpty() ? null : (JSONArray) raw;
|
||||||
|
}
|
||||||
|
if (raw instanceof java.util.List) {
|
||||||
|
JSONArray parsed = JSON.parseArray(JSON.toJSONString(raw));
|
||||||
|
return parsed == null || parsed.isEmpty() ? null : parsed;
|
||||||
|
}
|
||||||
|
if (raw instanceof String && StringUtils.isNotBlank((String) raw)) {
|
||||||
|
JSONArray parsed = parseFeeRows(raw);
|
||||||
|
return parsed.isEmpty() ? null : parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return [min, max],无上限时 max 为 -1;不是分档表头则 null */
|
||||||
|
static int[] parseKgBracket(String headerKey) {
|
||||||
|
if (StringUtils.isBlank(headerKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String t = headerKey.replaceAll("\\s+", "").toUpperCase(Locale.ROOT);
|
||||||
|
java.util.regex.Matcher ranged = java.util.regex.Pattern.compile("(\\d+)[-~~至到](\\d+)KG").matcher(t);
|
||||||
|
if (ranged.find()) {
|
||||||
|
return new int[]{Integer.parseInt(ranged.group(1)), Integer.parseInt(ranged.group(2))};
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher open = java.util.regex.Pattern.compile("(\\d+)KG以上").matcher(t);
|
||||||
|
if (open.find()) {
|
||||||
|
return new int[]{Integer.parseInt(open.group(1)), -1};
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher openCn = java.util.regex.Pattern.compile("(\\d+)以上").matcher(t);
|
||||||
|
if (openCn.find() && t.contains("KG")) {
|
||||||
|
return new int[]{Integer.parseInt(openCn.group(1)), -1};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intOf(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new java.math.BigDecimal(String.valueOf(value).trim()).intValue();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatTierPrice(Object price) {
|
||||||
|
if (price == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String text = String.valueOf(price).trim();
|
||||||
|
if (text.isEmpty() || "-".equals(text) || "null".equalsIgnoreCase(text)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new java.math.BigDecimal(text).stripTrailingZeros().toPlainString();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean vehicleTypeEquals(String actual, String headerKey) {
|
private static boolean vehicleTypeEquals(String actual, String headerKey) {
|
||||||
if (StringUtils.isBlank(actual) || StringUtils.isBlank(headerKey)) {
|
if (StringUtils.isBlank(actual) || StringUtils.isBlank(headerKey)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -3114,6 +3335,10 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int col = CellReference.convertColStringToIndex(colLetter.trim());
|
int col = CellReference.convertColStringToIndex(colLetter.trim());
|
||||||
|
// 两行合并的列头(燃油附加费率、时效)值在左上角。写到合并区下沿会把列头样式撑乱。
|
||||||
|
int[] origin = resolveMergeOrigin(sheet, rowIdx, col);
|
||||||
|
rowIdx = origin[0];
|
||||||
|
col = origin[1];
|
||||||
String ref = sheetName + "!" + new CellReference(rowIdx, col).formatAsString();
|
String ref = sheetName + "!" + new CellReference(rowIdx, col).formatAsString();
|
||||||
// #region agent log
|
// #region agent log
|
||||||
if (StringUtils.isNotBlank(value) && (value.contains("广州") || "B".equalsIgnoreCase(colLetter.trim()))) {
|
if (StringUtils.isNotBlank(value) && (value.contains("广州") || "B".equalsIgnoreCase(colLetter.trim()))) {
|
||||||
@@ -3141,16 +3366,407 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String exist = readCellText(cell);
|
String exist = readCellText(cell);
|
||||||
|
String header = columnHeaderText(sheet, rowIdx, col);
|
||||||
// 蓝底/费用表头格禁止当数据写:跨境集拼「始发地」被写成「上海」会拆掉主题蓝条
|
// 蓝底/费用表头格禁止当数据写:跨境集拼「始发地」被写成「上海」会拆掉主题蓝条
|
||||||
if (isFeeTableHeader(exist) || isLandInquiryHeaderBandLabel(exist)
|
if (isFeeTableHeader(exist) || isLandInquiryHeaderBandLabel(exist)
|
||||||
|| isLandRouteMatrixHeaderCell(exist) || isSectionTitle(sheet, cell)) {
|
|| isLandRouteMatrixHeaderCell(exist) || isDeliveryPriceHeaderCell(exist)
|
||||||
|
|| isSectionTitle(sheet, cell)
|
||||||
|
|| rowLooksLikeLandRouteMatrixHeader(sheet, rowIdx)
|
||||||
|
|| rowLooksLikeDeliveryPriceHeader(sheet, rowIdx)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 报关费、提货费、口岸、重抛比、当日达是模板印好的,空值或运价不能清掉。
|
||||||
|
if (isTemplateFixedNote(exist)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 配送费「单价(元/票)」已印好 200/450 时,不要改成公斤运价。其它表的单价列仍可填。
|
||||||
|
if (header.contains("元/票") && StringUtils.isNotBlank(value) && StringUtils.isNotBlank(exist)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 币种只进「币种/币别」列。配送费的覆盖区域、燃油附加费率不是币种,CNY 写进去会盖掉城市。
|
||||||
|
if (looksLikeCurrencyCode(value) && columnRejectsCurrency(header)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeNumericOrText(cell, value);
|
writeNumericOrText(cell, value);
|
||||||
|
// 始发地/目的地是城市。模板空格常套用「币种/燃油」的红字+顶对齐,
|
||||||
|
// 长地名一换行就会画进蓝表头,看起来像列头被改了颜色。
|
||||||
|
if (isCityRouteColumn(sheet, rowIdx, col, header)) {
|
||||||
|
restyleCityDataCell(cell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 映射表写出的备注同样要换行,否则只看得见半截
|
// 映射表写出的备注同样要换行,否则只看得见半截
|
||||||
applyRemarkWrap(sheet, row, cell, value);
|
applyRemarkWrap(sheet, row, cell, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据格正上方的列头(向上最多 3 行,合并格看左上角)。
|
||||||
|
* 用来区分「币种」列和「燃油附加费率 / 覆盖区域」列,避免币种写错列。
|
||||||
|
*/
|
||||||
|
static String columnHeaderText(Sheet sheet, int rowIdx, int col) {
|
||||||
|
if (sheet == null || rowIdx <= 0 || col < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
DataFormatter fmt = new DataFormatter();
|
||||||
|
for (int r = rowIdx - 1; r >= Math.max(0, rowIdx - 3); r--) {
|
||||||
|
int[] origin = resolveMergeOrigin(sheet, r, col);
|
||||||
|
Row row = sheet.getRow(origin[0]);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Cell cell = row.getCell(origin[1]);
|
||||||
|
String text = StringUtils.trimToEmpty(fmt.formatCellValue(cell)).replaceAll("\\s+", "");
|
||||||
|
if (StringUtils.isNotBlank(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CNY / USD 这类币种代码。城市名、时效不算。 */
|
||||||
|
static boolean looksLikeCurrencyCode(String value) {
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = value.trim().toUpperCase(Locale.ROOT);
|
||||||
|
return "CNY".equals(t) || "USD".equals(t) || "RMB".equals(t) || "EUR".equals(t)
|
||||||
|
|| "HKD".equals(t) || "VND".equals(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这些列头不是币种:写 CNY 会把覆盖区域的城市、或燃油附加费率的空格盖掉。
|
||||||
|
* 表头是「币种 / 币别」,或上面没有列头时,仍允许写币种。
|
||||||
|
*/
|
||||||
|
static boolean columnRejectsCurrency(String header) {
|
||||||
|
if (StringUtils.isBlank(header)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (header.contains("币种") || header.contains("币别")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return header.contains("燃油") || header.contains("覆盖") || header.contains("城市")
|
||||||
|
|| header.contains("单价") || header.contains("时效") || header.contains("税率")
|
||||||
|
|| header.contains("始发") || header.contains("目的") || header.contains("备注")
|
||||||
|
|| header.contains("报关") || header.contains("提货") || header.contains("口岸");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运价表「始发地 / 目的地」列:列头文案,或向上几行合并格就是这两个字。
|
||||||
|
*/
|
||||||
|
static boolean isCityRouteColumn(Sheet sheet, int rowIdx, int col, String header) {
|
||||||
|
if (header != null && (header.contains("始发") || header.contains("目的"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sheet == null || rowIdx <= 0 || col < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DataFormatter fmt = new DataFormatter();
|
||||||
|
for (int r = rowIdx - 1; r >= Math.max(0, rowIdx - 4); r--) {
|
||||||
|
int[] origin = resolveMergeOrigin(sheet, r, col);
|
||||||
|
Row row = sheet.getRow(origin[0]);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String text = StringUtils.trimToEmpty(fmt.formatCellValue(row.getCell(origin[1]))).replaceAll("\\s+", "");
|
||||||
|
if ("始发地".equals(text) || "始发站".equals(text) || "目的地".equals(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 城市写在白格里:黑字、垂直居中。
|
||||||
|
* 不沿用币种列的红字,也不顶对齐,避免字画进上面的蓝表头。
|
||||||
|
* 不改填充和边框,蓝表头本身不动。
|
||||||
|
*/
|
||||||
|
private static void restyleCityDataCell(Cell cell) {
|
||||||
|
if (cell == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Workbook wb = cell.getSheet().getWorkbook();
|
||||||
|
Font old = wb.getFontAt(cell.getCellStyle().getFontIndex());
|
||||||
|
CellStyle copy = wb.createCellStyle();
|
||||||
|
copy.cloneStyleFrom(cell.getCellStyle());
|
||||||
|
Font black = wb.createFont();
|
||||||
|
if (old != null) {
|
||||||
|
if (StringUtils.isNotBlank(old.getFontName())) {
|
||||||
|
black.setFontName(old.getFontName());
|
||||||
|
}
|
||||||
|
if (old.getFontHeight() > 0) {
|
||||||
|
black.setFontHeight(old.getFontHeight());
|
||||||
|
}
|
||||||
|
black.setBold(false);
|
||||||
|
}
|
||||||
|
black.setColor(IndexedColors.BLACK.getIndex());
|
||||||
|
copy.setFont(black);
|
||||||
|
copy.setWrapText(false);
|
||||||
|
copy.setShrinkToFit(true);
|
||||||
|
copy.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
cell.setCellStyle(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列头底色/字色用 RGB 记下来。cloneStyleFrom 会丢掉主题色,运价表会变成「业务员」那种深蓝,
|
||||||
|
* 配送费「燃油附加费率」蓝底也会没。
|
||||||
|
*/
|
||||||
|
static final class LandTemplateLook {
|
||||||
|
private final List<HeaderPaint> paints = new ArrayList<>();
|
||||||
|
private final List<int[]> rowHeights = new ArrayList<>();
|
||||||
|
private final List<int[]> colWidths = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class HeaderPaint {
|
||||||
|
final int sheet;
|
||||||
|
final int row;
|
||||||
|
final int col;
|
||||||
|
final byte[] fillRgb;
|
||||||
|
final byte[] fontRgb;
|
||||||
|
final boolean bold;
|
||||||
|
final String fontName;
|
||||||
|
final short fontHeight;
|
||||||
|
final boolean wrap;
|
||||||
|
|
||||||
|
HeaderPaint(int sheet, int row, int col, byte[] fillRgb, byte[] fontRgb,
|
||||||
|
boolean bold, String fontName, short fontHeight, boolean wrap) {
|
||||||
|
this.sheet = sheet;
|
||||||
|
this.row = row;
|
||||||
|
this.col = col;
|
||||||
|
this.fillRgb = fillRgb;
|
||||||
|
this.fontRgb = fontRgb;
|
||||||
|
this.bold = bold;
|
||||||
|
this.fontName = fontName;
|
||||||
|
this.fontHeight = fontHeight;
|
||||||
|
this.wrap = wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LandTemplateLook snapshotLandTemplateLook(Workbook wb) {
|
||||||
|
LandTemplateLook look = new LandTemplateLook();
|
||||||
|
if (wb == null) {
|
||||||
|
return look;
|
||||||
|
}
|
||||||
|
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
|
||||||
|
Sheet sheet = wb.getSheetAt(s);
|
||||||
|
if (sheet == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean[] keep = markLandHeaderRows(sheet);
|
||||||
|
int lastRow = Math.min(sheet.getLastRowNum(), 40);
|
||||||
|
for (int r = 0; r <= lastRow; r++) {
|
||||||
|
if (!keep[r]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row row = sheet.getRow(r);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
look.rowHeights.add(new int[]{s, r, row.getHeight()});
|
||||||
|
int lastCol = Math.max(row.getLastCellNum(), 18);
|
||||||
|
for (int c = 0; c < lastCol; c++) {
|
||||||
|
Cell cell = row.getCell(c);
|
||||||
|
if (cell == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
look.paints.add(new HeaderPaint(s, r, c, readFillRgb(cell), readFontRgb(cell),
|
||||||
|
fontBold(cell), fontName(cell), fontHeight(cell),
|
||||||
|
cell.getCellStyle() != null && cell.getCellStyle().getWrapText()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int cols = 18;
|
||||||
|
for (int r = 0; r <= lastRow; r++) {
|
||||||
|
Row row = sheet.getRow(r);
|
||||||
|
if (row != null) {
|
||||||
|
cols = Math.max(cols, row.getLastCellNum());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int c = 0; c < cols; c++) {
|
||||||
|
look.colWidths.add(new int[]{s, c, sheet.getColumnWidth(c)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return look;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restoreLandTemplateLook(Workbook wb, LandTemplateLook look) {
|
||||||
|
if (wb == null || look == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (HeaderPaint paint : look.paints) {
|
||||||
|
Sheet sheet = wb.getSheetAt(paint.sheet);
|
||||||
|
if (sheet == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row row = sheet.getRow(paint.row);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Cell cell = row.getCell(paint.col);
|
||||||
|
if (cell == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
applyHeaderPaint(cell, paint);
|
||||||
|
}
|
||||||
|
for (int[] rh : look.rowHeights) {
|
||||||
|
Sheet sheet = wb.getSheetAt(rh[0]);
|
||||||
|
Row row = sheet == null ? null : sheet.getRow(rh[1]);
|
||||||
|
if (row != null) {
|
||||||
|
row.setHeight((short) rh[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int[] cw : look.colWidths) {
|
||||||
|
Sheet sheet = wb.getSheetAt(cw[0]);
|
||||||
|
if (sheet != null) {
|
||||||
|
sheet.setColumnWidth(cw[1], cw[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyHeaderPaint(Cell cell, HeaderPaint paint) {
|
||||||
|
Workbook wb = cell.getSheet().getWorkbook();
|
||||||
|
CellStyle copy = wb.createCellStyle();
|
||||||
|
if (cell.getCellStyle() != null) {
|
||||||
|
copy.cloneStyleFrom(cell.getCellStyle());
|
||||||
|
}
|
||||||
|
if (paint.fillRgb != null && copy instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
|
||||||
|
org.apache.poi.xssf.usermodel.XSSFCellStyle xs = (org.apache.poi.xssf.usermodel.XSSFCellStyle) copy;
|
||||||
|
xs.setFillForegroundColor(new XSSFColor(paint.fillRgb, null));
|
||||||
|
xs.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
} else if (paint.fillRgb != null) {
|
||||||
|
copy.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
}
|
||||||
|
XSSFFont font = (XSSFFont) wb.createFont();
|
||||||
|
if (StringUtils.isNotBlank(paint.fontName)) {
|
||||||
|
font.setFontName(paint.fontName);
|
||||||
|
}
|
||||||
|
if (paint.fontHeight > 0) {
|
||||||
|
font.setFontHeight(paint.fontHeight);
|
||||||
|
}
|
||||||
|
font.setBold(paint.bold);
|
||||||
|
if (paint.fontRgb != null) {
|
||||||
|
font.setColor(new XSSFColor(paint.fontRgb, null));
|
||||||
|
}
|
||||||
|
copy.setFont(font);
|
||||||
|
copy.setWrapText(paint.wrap);
|
||||||
|
cell.setCellStyle(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] readFillRgb(Cell cell) {
|
||||||
|
if (!(cell instanceof XSSFCell)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
org.apache.poi.xssf.usermodel.XSSFCellStyle style =
|
||||||
|
(org.apache.poi.xssf.usermodel.XSSFCellStyle) cell.getCellStyle();
|
||||||
|
if (style == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
XSSFColor color = style.getFillForegroundXSSFColor();
|
||||||
|
return xssfRgb(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] readFontRgb(Cell cell) {
|
||||||
|
if (!(cell instanceof XSSFCell)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
XSSFFont font = ((XSSFCell) cell).getCellStyle().getFont();
|
||||||
|
if (font == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return xssfRgb(font.getXSSFColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] xssfRgb(XSSFColor color) {
|
||||||
|
if (color == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] rgb = color.getRGBWithTint();
|
||||||
|
if (rgb == null || rgb.length < 3) {
|
||||||
|
rgb = color.getRGB();
|
||||||
|
}
|
||||||
|
if (rgb == null || rgb.length < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] out = new byte[3];
|
||||||
|
System.arraycopy(rgb, rgb.length - 3, out, 0, 3);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean fontBold(Cell cell) {
|
||||||
|
if (cell == null || cell.getCellStyle() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Font font = cell.getSheet().getWorkbook().getFontAt(cell.getCellStyle().getFontIndex());
|
||||||
|
return font != null && font.getBold();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fontName(Cell cell) {
|
||||||
|
if (cell == null || cell.getCellStyle() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Font font = cell.getSheet().getWorkbook().getFontAt(cell.getCellStyle().getFontIndex());
|
||||||
|
return font == null ? "" : font.getFontName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static short fontHeight(Cell cell) {
|
||||||
|
if (cell == null || cell.getCellStyle() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Font font = cell.getSheet().getWorkbook().getFontAt(cell.getCellStyle().getFontIndex());
|
||||||
|
return font == null ? 0 : font.getFontHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean[] markLandHeaderRows(Sheet sheet) {
|
||||||
|
int last = Math.min(sheet.getLastRowNum(), 40);
|
||||||
|
boolean[] keep = new boolean[last + 2];
|
||||||
|
for (int r = 0; r <= last; r++) {
|
||||||
|
if (rowLooksLikeLandRouteMatrixHeader(sheet, r)) {
|
||||||
|
keep[r] = true;
|
||||||
|
if (r + 1 < keep.length) {
|
||||||
|
keep[r + 1] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowLooksLikeDeliveryPriceHeader(sheet, r)) {
|
||||||
|
keep[r] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨境集拼模板里印好的说明和配送时效,不是待填数据。
|
||||||
|
* 报关费、提货费、口岸、重抛比、当日达被空备注或运价盖掉后,销售对不上原模板。
|
||||||
|
*/
|
||||||
|
static boolean isTemplateFixedNote(String text) {
|
||||||
|
if (StringUtils.isBlank(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = text.replaceAll("\\s+", "");
|
||||||
|
return t.contains("重抛比") || t.contains("实报实销") || t.contains("当日达")
|
||||||
|
|| t.contains("续票") || t.contains("续页") || t.contains("另议")
|
||||||
|
|| t.contains("客户交货");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模板里币种、燃油附加费率、税率的红字(显式 RGB 或索引红)。 */
|
||||||
|
static boolean fontIsRed(Cell cell) {
|
||||||
|
if (!(cell instanceof XSSFCell)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
XSSFFont font = ((XSSFCell) cell).getCellStyle().getFont();
|
||||||
|
if (font == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (font.getColor() == Font.COLOR_RED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
XSSFColor color = font.getXSSFColor();
|
||||||
|
if (color == null || color.getRGB() == null || color.getRGB().length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
byte[] rgb = color.getRGB();
|
||||||
|
int r = rgb[rgb.length - 3] & 0xFF;
|
||||||
|
int g = rgb[rgb.length - 2] & 0xFF;
|
||||||
|
int b = rgb[rgb.length - 1] & 0xFF;
|
||||||
|
return r >= 180 && g <= 40 && b <= 40;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI mapping 常只标 name/amount/currency;空运表头有「单位/单价/数量」时现场补列字母。
|
* AI mapping 常只标 name/amount/currency;空运表头有「单位/单价/数量」时现场补列字母。
|
||||||
* 不覆盖 mapping 已有列,避免改错已对齐的金额列。
|
* 不覆盖 mapping 已有列,避免改错已对齐的金额列。
|
||||||
@@ -4331,7 +4947,10 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
String existing = readCellText(cell);
|
String existing = readCellText(cell);
|
||||||
// 映射点常落在合并标签格 / 20GP 表头上,不能把原字盖掉
|
// 映射点常落在合并标签格 / 20GP 表头上,不能把原字盖掉
|
||||||
if (isFeeTableHeader(existing) || isLabelLike(existing) || isSectionTitle(sheet, cell)
|
if (isFeeTableHeader(existing) || isLabelLike(existing) || isSectionTitle(sheet, cell)
|
||||||
|| isLandInquiryHeaderBandLabel(existing)) {
|
|| isLandInquiryHeaderBandLabel(existing) || isLandRouteMatrixHeaderCell(existing)
|
||||||
|
|| isDeliveryPriceHeaderCell(existing)
|
||||||
|
|| rowLooksLikeLandRouteMatrixHeader(sheet, dest[0])
|
||||||
|
|| rowLooksLikeDeliveryPriceHeader(sheet, dest[0])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int colon = Math.max(existing.indexOf(':'), existing.indexOf(':'));
|
int colon = Math.max(existing.indexOf(':'), existing.indexOf(':'));
|
||||||
@@ -4454,6 +5073,12 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
int rowIdx = labelCell.getRowIndex();
|
int rowIdx = labelCell.getRowIndex();
|
||||||
|
// 运价表、配送费表的列头(时效、燃油附加费率、城市类别)不是「标签右侧填值」。
|
||||||
|
// 否则 N+3 会写到时效列右边的空白格,列头样式也被带乱。
|
||||||
|
if (rowLooksLikeLandRouteMatrixHeader(sheet, rowIdx)
|
||||||
|
|| rowLooksLikeDeliveryPriceHeader(sheet, rowIdx)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
int startCol = labelCell.getColumnIndex() + 1;
|
int startCol = labelCell.getColumnIndex() + 1;
|
||||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||||
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
||||||
@@ -4474,7 +5099,7 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
Cell cand = row.getCell(dest[1]);
|
Cell cand = row.getCell(dest[1]);
|
||||||
String exist = readCellText(cand);
|
String exist = readCellText(cand);
|
||||||
if (isFeeTableHeader(exist) || isLabelLike(exist) || isSectionTitle(sheet, cand)
|
if (isFeeTableHeader(exist) || isLabelLike(exist) || isSectionTitle(sheet, cand)
|
||||||
|| isUnsuitableValueCell(exist)) {
|
|| isUnsuitableValueCell(exist) || isLandRouteMatrixHeaderCell(exist)) {
|
||||||
// 左右双栏:已找到左侧候选后,遇到右侧标签必须停,禁止再跨到送货地址值区
|
// 左右双栏:已找到左侧候选后,遇到右侧标签必须停,禁止再跨到送货地址值区
|
||||||
if ((firstEmpty != null || wideEmpty != null)
|
if ((firstEmpty != null || wideEmpty != null)
|
||||||
&& (isLabelLike(exist) || isSectionTitle(sheet, cand))) {
|
&& (isLabelLike(exist) || isSectionTitle(sheet, cand))) {
|
||||||
@@ -4510,10 +5135,13 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
if (sheet == null || labelCell == null || StringUtils.isBlank(text)) {
|
if (sheet == null || labelCell == null || StringUtils.isBlank(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!isLandInquiryHeaderBandLabel(text)) {
|
int rowIdx = labelCell.getRowIndex();
|
||||||
|
// 「时效」不参与条带判定(运价矩阵也有这列),但条带行上的时效值仍在下一行。
|
||||||
|
boolean bandLabel = isLandInquiryHeaderBandLabel(text);
|
||||||
|
boolean transitOnBand = isInquiryBandTransitLabel(text) && rowLooksLikeLandInquiryHeaderBand(sheet, rowIdx);
|
||||||
|
if (!bandLabel && !transitOnBand) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int rowIdx = labelCell.getRowIndex();
|
|
||||||
if (!rowLooksLikeLandInquiryHeaderBand(sheet, rowIdx)) {
|
if (!rowLooksLikeLandInquiryHeaderBand(sheet, rowIdx)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -4556,6 +5184,45 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
|| "货物类型".equals(compact) || "条款".equals(compact) || "发货日期".equals(compact);
|
|| "货物类型".equals(compact) || "条款".equals(compact) || "发货日期".equals(compact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 询价条带上的时效。矩阵同名列不走这里,调用方必须先确认该行已是询价条带。 */
|
||||||
|
static boolean isInquiryBandTransitLabel(String text) {
|
||||||
|
if (StringUtils.isBlank(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String compact = text.replace(":", "").replace(":", "").replaceAll("\\s+", "").trim();
|
||||||
|
return "时效".equals(compact) || "时效(天)".equals(compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射格落在询价条带表头上,且这列表头对应的字段不是正在写的字段。
|
||||||
|
* 例如 sales_name 被指到「客户名称」:跳过,改由标签「业务员」去写。
|
||||||
|
*/
|
||||||
|
static boolean mappingPointsAtForeignBandLabel(Workbook wb, String cellRef, String fieldCode) {
|
||||||
|
ParsedRef pref = ParsedRef.parse(cellRef);
|
||||||
|
if (pref == null || wb == null || StringUtils.isBlank(fieldCode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Sheet sh = wb.getSheet(pref.sheet);
|
||||||
|
if (sh == null && wb.getNumberOfSheets() > 0) {
|
||||||
|
sh = wb.getSheetAt(0);
|
||||||
|
}
|
||||||
|
if (sh == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int[] origin = resolveMergeOrigin(sh, pref.row, pref.col);
|
||||||
|
Row row = sh.getRow(origin[0]);
|
||||||
|
Cell cell = row == null ? null : row.getCell(origin[1]);
|
||||||
|
String text = readCellText(cell);
|
||||||
|
if (!rowLooksLikeLandInquiryHeaderBand(sh, origin[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isLandInquiryHeaderBandLabel(text) && !isInquiryBandTransitLabel(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String labelCode = matchLabel(text.replace(":", ":").trim(), FILL_BY_LABEL);
|
||||||
|
return StringUtils.isNotBlank(labelCode) && !labelCode.equals(fieldCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一行里出现不少于 3 个询价条带标签,视为陆运蓝底表头行(截图正确版第 5 行)。
|
* 一行里出现不少于 3 个询价条带标签,视为陆运蓝底表头行(截图正确版第 5 行)。
|
||||||
*/
|
*/
|
||||||
@@ -4670,6 +5337,9 @@ public final class QuoteTemplateExcelSupport {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String val = firstFact(facts, code, text.replace(":", "").replace(":", "").trim());
|
String val = firstFact(facts, code, text.replace(":", "").replace(":", "").trim());
|
||||||
|
if ((StringUtils.isBlank(val) || "-".equals(val)) && "transit_days".equals(code)) {
|
||||||
|
val = firstFact(facts, "transit_time");
|
||||||
|
}
|
||||||
if (StringUtils.isBlank(val) || "-".equals(val)) {
|
if (StringUtils.isBlank(val) || "-".equals(val)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -463,6 +463,17 @@ public class InquiryTmsQueryService {
|
|||||||
putExtra(row, "transitDays", transit);
|
putExtra(row, "transitDays", transit);
|
||||||
putExtra(row, "transit_time", transit);
|
putExtra(row, "transit_time", transit);
|
||||||
putExtra(row, "remark", firstText(seg.get("remark"), seg.get("note"), seg.get("description")));
|
putExtra(row, "remark", firstText(seg.get("remark"), seg.get("note"), seg.get("description")));
|
||||||
|
Object tiers = seg.get("weightPrices");
|
||||||
|
if (tiers == null) {
|
||||||
|
tiers = seg.get("weight_prices");
|
||||||
|
}
|
||||||
|
if (tiers != null) {
|
||||||
|
String tierJson = tiers instanceof String ? String.valueOf(tiers) : JSON.toJSONString(tiers);
|
||||||
|
if (StringUtils.isNotBlank(tierJson) && !"null".equalsIgnoreCase(tierJson)
|
||||||
|
&& !"[]".equals(tierJson.trim())) {
|
||||||
|
row.put("weightPrices", tierJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 腿上若带分项费率扩展字段,原样带上供「提货费/元/CBM」列匹配
|
// 腿上若带分项费率扩展字段,原样带上供「提货费/元/CBM」列匹配
|
||||||
String[][] rateKeys = {
|
String[][] rateKeys = {
|
||||||
{"pickupFee", "pickup_fee"}, {"deliveryFee", "delivery_fee"},
|
{"pickupFee", "pickup_fee"}, {"deliveryFee", "delivery_fee"},
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
-- 机器人回复语:用现网整理表覆盖旧原型数据,并写入调用编码 reply_code。
|
||||||
|
-- 仅改 inquiry_robot.inq_bot_reply。可重复执行(先清空再插入)。
|
||||||
|
-- 智能体本期不读本表;编码先落库供后台展示与日后按码取文案。
|
||||||
|
-- 若尚无 reply_code 列会先加;列已存在时本句报 1060,可忽略后继续 DELETE/INSERT。
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `inq_bot_reply`
|
||||||
|
ADD COLUMN `reply_code` varchar(64) NULL COMMENT '智能体调用编码' AFTER `category_title`;
|
||||||
|
|
||||||
|
DELETE FROM `inq_bot_reply`;
|
||||||
|
|
||||||
|
INSERT INTO `inq_bot_reply`
|
||||||
|
(`id`,`category_code`,`category_title`,`reply_code`,`trigger_scene`,`reply_text`,`enabled`,`sort_no`,`create_by`,`create_time`)
|
||||||
|
VALUES
|
||||||
|
('brv10000000000000000000000000001','inquiring','询价中','inquiring.first_chat','销售首次单聊机器人','你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000002','inquiring','询价中','inquiring.ask_transport','未识别运输方式','请回复数字或文字,告知我询价的运输方式:
|
||||||
|
1.陆运
|
||||||
|
2.海运
|
||||||
|
3.空运',1,2,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000003','inquiring','询价中','inquiring.continue_need_wo','续办但未提供工单号','请告诉我要继续的工单号,例如 WO202609140034。',1,3,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000004','inquiring','询价中','inquiring.continue_not_found','续办找不到工单','找不到工单 {{ticket_no}},请核对工单号后再试。',1,4,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000005','inquiring','询价中','inquiring.image_recognizing','销售发送图片','正在识别图片',1,5,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000006','inquiring','询价中','inquiring.image_tech_fail','图片识别失败','图片暂时没认出来,请再发一张或直接打字。',1,6,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000007','inquiring','询价中','inquiring.attach_recognizing','销售发送附件','正在识别附件',1,7,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000008','inquiring','询价中','inquiring.attach_tech_fail','附件识别失败','附件暂时没认出来,请再传一次或直接打字。',1,8,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000009','inquiring','询价中','inquiring.attach_h5_expired','H5 上传链接过期','已过期,请回企微重新说附件询价。',1,9,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000010','inquiring','询价中','inquiring.attach_h5_ok','H5 上传成功','已收到,请回企微查看识别结果。',1,10,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000011','inquiring','询价中','inquiring.pull_group_manual','请手动拉人进群并发送工单号','请手动拉相关人员和智能体到原有群,并在群聊发送工单编号{{ticket_no}}',1,11,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000012','inquiring','询价中','inquiring.skip_collab_ack','销售跳过内部协同','好的,已跳过内部协同。正在按 TMS 标准报价生成报价单草稿…',1,12,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000013','inquiring','询价中','inquiring.group_created','协同群创建成功','协同群已创建,群名与工单号一致:{{ticket_no}}。已邀请:销售{{sales_name}}、产品{{product_names}}。请在群聊中继续确认报价。',1,13,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000014','inquiring','询价中','inquiring.group_exists','协同群已存在','协同群已在,群名与工单号一致:{{ticket_no}}。请到该群继续确认报价。',1,14,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000015','inquiring','询价中','inquiring.sea_no_staff','海运没有可拉的产品人员','没有可拉的产品人员。请先在后台「员工管理」维护本线路的海运产品岗后再试。',1,15,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000016','inquiring','询价中','inquiring.land_no_staff','陆运没有可拉的产品人员','没有可拉的产品人员。请先在后台「员工管理」维护本线路的陆运产品岗后再试。',1,16,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000017','inquiring','询价中','inquiring.group_create_fail','建群失败','建群失败,请稍后重试。',1,17,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000018','inquiring','询价中','inquiring.quote_generating','报价单生成中','报价单正在生成,请稍候。',1,18,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000019','inquiring','询价中','inquiring.air_ticket_not_found','空运群找不到工单','找不到工单{{ticket_no}},请核对工单号后再试。',1,19,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000020','inquiring','询价中','inquiring.air_ticket_not_air','工单不是空运单','工单{{ticket_no}}不是空运单,不能在空运群里跟。',1,20,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000021','inquiring','询价中','inquiring.air_ticket_occupied','空运工单已在另一个群','工单{{ticket_no}}已经在另一个群跟了,请到那个群继续。',1,21,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000022','inquiring','询价中','inquiring.group_only_product','非产品人员在群里报价','只有产品能报价格,请产品在群里发价格或 Excel/PDF',1,22,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000023','inquiring','询价中','inquiring.group_file_saved','群附件已保存','附件已保存后台',1,23,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000024','inquiring','询价中','inquiring.air_lock_need_wo','空运锁舱/释放未带工单号','锁舱或释放舱位请带上工单号,例如 WO202609160001锁舱。',1,24,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000025','inquiring','询价中','inquiring.air_lock_need_quote','没有报价不能锁舱','工单{{ticket_no}}还没有报价,不能锁舱。',1,25,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000026','inquiring','询价中','inquiring.air_airline_only_cabin','只有航线能锁舱或释放','只有岗位为「航线·空运」的人能锁舱或释放舱位。',1,26,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000027','inquiring','询价中','inquiring.air_lock_ok','空运锁舱成功','工单{{ticket_no}} 已通知TMS执行空运锁舱,执行成功。
|
||||||
|
操作人:{{operator}}
|
||||||
|
舱位选项:{{option_no}}
|
||||||
|
执行编号:{{exec_no}}
|
||||||
|
报价编号:{{quote_no}}',1,27,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000028','inquiring','询价中','inquiring.air_lock_fail','空运锁舱失败','工单{{ticket_no}} 空运锁舱失败。
|
||||||
|
操作人:{{operator}}
|
||||||
|
锁舱失败的原因:{{reason}}
|
||||||
|
已通知IT运维排查,请稍等。',1,28,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000029','inquiring','询价中','inquiring.air_release_ok','空运释放成功','工单{{ticket_no}} 已通知TMS执行空运释放舱位成功
|
||||||
|
操作人:{{operator}}
|
||||||
|
舱位选项:{{option_no}}
|
||||||
|
执行编号:{{exec_no}}
|
||||||
|
报价编号:{{quote_no}}',1,29,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000030','inquiring','询价中','inquiring.air_release_fail','空运释放失败','工单{{ticket_no}} 空运释放舱位失败。
|
||||||
|
操作人:{{operator}}
|
||||||
|
释放舱位失败的原因:{{reason}}
|
||||||
|
已通知IT运维排查,请稍等。',1,30,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000031','inquiring','询价中','inquiring.air_release_deal_blocked','已成交不能释放舱位','释放舱位失败,工单{{ticket_no}}已成交。',1,31,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000032','inquiring','询价中','inquiring.air_release_need_lock','没有有效锁舱不能释放','工单{{ticket_no}}还没有有效锁舱,不能释放。',1,32,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000033','quoted','已报价','quoted.reject_ack','销售选择「不采用」','请问如何调整报价单,以满足您的需求?请说明要调哪一项费用,例如「提货费加 10%」。',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000034','quoted','已报价','quoted.adopt_excel','销售确认采用 → 生成 Excel','已确认采用该方案,正在生成 Excel 最终报价单…',1,2,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000035','quoted','已报价','quoted.adopt_pdf','销售确认采用 → 生成 PDF','已确认采用该方案,正在生成PDF最终报价单…',1,3,'admin',NOW()),
|
||||||
|
('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()),
|
||||||
|
('brv10000000000000000000000000040','lost','未成交','lost.ask_reason','成交跟进中选择「未成交」且未写原因','已记录为「未成交」。请补充未成交原因。',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000041','lost','未成交','lost.reason_ack','销售回复未成交原因','已记下未成交原因:{{lost_reason}}。',1,2,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000042','closed','已关闭','closed.air_cannot_activate','空运工单已是终态仍要激活','工单{{ticket_no}}已是「{{status}}」,不能再激活。',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000043','sys-ex','系统异常','sys.llm_fail','大模型调用失败(销售侧)','大模型服务异常,IT排查中...',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000044','sys-ex','系统异常','sys.tms_fail','TMS 接口失败(销售侧)','TMS接口异常,IT运维排查中...',1,2,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000045','sys-ex','系统异常','sys.group_add_fail','企微添加成员失败','无法添加 {{staff_name}} 同事,请联系管理员检查企业微信配置。',1,3,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000046','sys-ex','系统异常','sys.tms_recovery','TMS 已恢复','TMS已恢复正常使用,感谢您的耐心等待。',1,4,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000047','sys-ex','系统异常','sys.llm_recovery','大模型已恢复','大模型已恢复正常使用,感谢您的耐心等待。',1,5,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000048','sys-ex','系统异常','sys.cabin_recovery','空运舱位接口已恢复','空运舱位接口已恢复正常使用,感谢您的耐心等待。',1,6,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000049','sys-ex','系统异常','sys.group_recovery','建群已恢复','建群已恢复正常,感谢您的耐心等待。',1,7,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000050','transfer','转人工','transfer.already_collab','该工单已拉过产品进群','该工单已拉产品进群协同。',1,1,'admin',NOW()),
|
||||||
|
('brv10000000000000000000000000051','transfer','转人工','transfer.air_hint','空运转人工(不自动建群)','触发转人工,请手动把航线和智能体拉进原有空运群,并发送工单号 {{ticket_no}}。',1,2,'admin',NOW());
|
||||||
|
|
||||||
|
ALTER TABLE `inq_bot_reply`
|
||||||
|
ADD UNIQUE INDEX `uk_inq_bot_reply_reply_code` (`reply_code`);
|
||||||
|
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
-- 为 inq_bot_reply 增加智能体调用编码 reply_code,并回填现网 51 条。
|
||||||
|
-- 仅改 inquiry_robot。列已存在时 ADD 会报 1060,可忽略后继续执行 UPDATE。
|
||||||
|
-- 本期智能体不读本表。
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `inq_bot_reply`
|
||||||
|
ADD COLUMN `reply_code` varchar(64) NULL COMMENT '智能体调用编码' AFTER `category_title`;
|
||||||
|
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.first_chat' WHERE `id` = 'brv10000000000000000000000000001' OR (`reply_code` IS NULL AND `trigger_scene` = '销售首次单聊机器人');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.ask_transport' WHERE `id` = 'brv10000000000000000000000000002' OR (`reply_code` IS NULL AND `trigger_scene` = '未识别运输方式');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.continue_need_wo' WHERE `id` = 'brv10000000000000000000000000003' OR (`reply_code` IS NULL AND `trigger_scene` = '续办但未提供工单号');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.continue_not_found' WHERE `id` = 'brv10000000000000000000000000004' OR (`reply_code` IS NULL AND `trigger_scene` = '续办找不到工单');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.image_recognizing' WHERE `id` = 'brv10000000000000000000000000005' OR (`reply_code` IS NULL AND `trigger_scene` = '销售发送图片');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.image_tech_fail' WHERE `id` = 'brv10000000000000000000000000006' OR (`reply_code` IS NULL AND `trigger_scene` = '图片识别失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.attach_recognizing' WHERE `id` = 'brv10000000000000000000000000007' OR (`reply_code` IS NULL AND `trigger_scene` = '销售发送附件');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.attach_tech_fail' WHERE `id` = 'brv10000000000000000000000000008' OR (`reply_code` IS NULL AND `trigger_scene` = '附件识别失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.attach_h5_expired' WHERE `id` = 'brv10000000000000000000000000009' OR (`reply_code` IS NULL AND `trigger_scene` = 'H5 上传链接过期');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.attach_h5_ok' WHERE `id` = 'brv10000000000000000000000000010' OR (`reply_code` IS NULL AND `trigger_scene` = 'H5 上传成功');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.pull_group_manual' WHERE `id` = 'brv10000000000000000000000000011' OR (`reply_code` IS NULL AND `trigger_scene` = '请手动拉人进群并发送工单号');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.skip_collab_ack' WHERE `id` = 'brv10000000000000000000000000012' OR (`reply_code` IS NULL AND `trigger_scene` = '销售跳过内部协同');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.group_created' WHERE `id` = 'brv10000000000000000000000000013' OR (`reply_code` IS NULL AND `trigger_scene` = '协同群创建成功');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.group_exists' WHERE `id` = 'brv10000000000000000000000000014' OR (`reply_code` IS NULL AND `trigger_scene` = '协同群已存在');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.sea_no_staff' WHERE `id` = 'brv10000000000000000000000000015' OR (`reply_code` IS NULL AND `trigger_scene` = '海运没有可拉的产品人员');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.land_no_staff' WHERE `id` = 'brv10000000000000000000000000016' OR (`reply_code` IS NULL AND `trigger_scene` = '陆运没有可拉的产品人员');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.group_create_fail' WHERE `id` = 'brv10000000000000000000000000017' OR (`reply_code` IS NULL AND `trigger_scene` = '建群失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.quote_generating' WHERE `id` = 'brv10000000000000000000000000018' OR (`reply_code` IS NULL AND `trigger_scene` = '报价单生成中');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_ticket_not_found' WHERE `id` = 'brv10000000000000000000000000019' OR (`reply_code` IS NULL AND `trigger_scene` = '空运群找不到工单');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_ticket_not_air' WHERE `id` = 'brv10000000000000000000000000020' OR (`reply_code` IS NULL AND `trigger_scene` = '工单不是空运单');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_ticket_occupied' WHERE `id` = 'brv10000000000000000000000000021' OR (`reply_code` IS NULL AND `trigger_scene` = '空运工单已在另一个群');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.group_only_product' WHERE `id` = 'brv10000000000000000000000000022' OR (`reply_code` IS NULL AND `trigger_scene` = '非产品人员在群里报价');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.group_file_saved' WHERE `id` = 'brv10000000000000000000000000023' OR (`reply_code` IS NULL AND `trigger_scene` = '群附件已保存');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_lock_need_wo' WHERE `id` = 'brv10000000000000000000000000024' OR (`reply_code` IS NULL AND `trigger_scene` = '空运锁舱/释放未带工单号');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_lock_need_quote' WHERE `id` = 'brv10000000000000000000000000025' OR (`reply_code` IS NULL AND `trigger_scene` = '没有报价不能锁舱');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_airline_only_cabin' WHERE `id` = 'brv10000000000000000000000000026' OR (`reply_code` IS NULL AND `trigger_scene` = '只有航线能锁舱或释放');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_lock_ok' WHERE `id` = 'brv10000000000000000000000000027' OR (`reply_code` IS NULL AND `trigger_scene` = '空运锁舱成功');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_lock_fail' WHERE `id` = 'brv10000000000000000000000000028' OR (`reply_code` IS NULL AND `trigger_scene` = '空运锁舱失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_release_ok' WHERE `id` = 'brv10000000000000000000000000029' OR (`reply_code` IS NULL AND `trigger_scene` = '空运释放成功');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_release_fail' WHERE `id` = 'brv10000000000000000000000000030' OR (`reply_code` IS NULL AND `trigger_scene` = '空运释放失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_release_deal_blocked' WHERE `id` = 'brv10000000000000000000000000031' OR (`reply_code` IS NULL AND `trigger_scene` = '已成交不能释放舱位');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'inquiring.air_release_need_lock' WHERE `id` = 'brv10000000000000000000000000032' OR (`reply_code` IS NULL AND `trigger_scene` = '没有有效锁舱不能释放');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'quoted.reject_ack' WHERE `id` = 'brv10000000000000000000000000033' OR (`reply_code` IS NULL AND `trigger_scene` = '销售选择「不采用」');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'quoted.adopt_excel' WHERE `id` = 'brv10000000000000000000000000034' OR (`reply_code` IS NULL AND `trigger_scene` = '销售确认采用 → 生成 Excel');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'quoted.adopt_pdf' WHERE `id` = 'brv10000000000000000000000000035' OR (`reply_code` IS NULL AND `trigger_scene` = '销售确认采用 → 生成 PDF');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'quoted.template_missing' WHERE `id` = 'brv10000000000000000000000000036' OR (`reply_code` IS NULL AND `trigger_scene` = '后台未维护报价模板');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'quoted.file_ready' WHERE `id` = 'brv10000000000000000000000000037' OR (`reply_code` IS NULL AND `trigger_scene` = '最终报价单生成完成');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'deal.recorded_won' WHERE `id` = 'brv10000000000000000000000000038' OR (`reply_code` IS NULL AND `trigger_scene` = '销售选择「成交」');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'negotiating.recorded' WHERE `id` = 'brv10000000000000000000000000039' OR (`reply_code` IS NULL AND `trigger_scene` = '成交跟进中选择「协商中」');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'lost.ask_reason' WHERE `id` = 'brv10000000000000000000000000040' OR (`reply_code` IS NULL AND `trigger_scene` = '成交跟进中选择「未成交」且未写原因');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'lost.reason_ack' WHERE `id` = 'brv10000000000000000000000000041' OR (`reply_code` IS NULL AND `trigger_scene` = '销售回复未成交原因');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'closed.air_cannot_activate' WHERE `id` = 'brv10000000000000000000000000042' OR (`reply_code` IS NULL AND `trigger_scene` = '空运工单已是终态仍要激活');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.llm_fail' WHERE `id` = 'brv10000000000000000000000000043' OR (`reply_code` IS NULL AND `trigger_scene` = '大模型调用失败(销售侧)');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.tms_fail' WHERE `id` = 'brv10000000000000000000000000044' OR (`reply_code` IS NULL AND `trigger_scene` = 'TMS 接口失败(销售侧)');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.group_add_fail' WHERE `id` = 'brv10000000000000000000000000045' OR (`reply_code` IS NULL AND `trigger_scene` = '企微添加成员失败');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.tms_recovery' WHERE `id` = 'brv10000000000000000000000000046' OR (`reply_code` IS NULL AND `trigger_scene` = 'TMS 已恢复');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.llm_recovery' WHERE `id` = 'brv10000000000000000000000000047' OR (`reply_code` IS NULL AND `trigger_scene` = '大模型已恢复');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.cabin_recovery' WHERE `id` = 'brv10000000000000000000000000048' OR (`reply_code` IS NULL AND `trigger_scene` = '空运舱位接口已恢复');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'sys.group_recovery' WHERE `id` = 'brv10000000000000000000000000049' OR (`reply_code` IS NULL AND `trigger_scene` = '建群已恢复');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'transfer.already_collab' WHERE `id` = 'brv10000000000000000000000000050' OR (`reply_code` IS NULL AND `trigger_scene` = '该工单已拉过产品进群');
|
||||||
|
UPDATE `inq_bot_reply` SET `reply_code` = 'transfer.air_hint' WHERE `id` = 'brv10000000000000000000000000051' OR (`reply_code` IS NULL AND `trigger_scene` = '空运转人工(不自动建群)');
|
||||||
|
|
||||||
|
ALTER TABLE `inq_bot_reply`
|
||||||
|
ADD UNIQUE INDEX `uk_inq_bot_reply_reply_code` (`reply_code`);
|
||||||
|
|
||||||
+400
@@ -1251,6 +1251,385 @@ public class QuoteTemplateFillTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 现网跨境集拼:第 5 行蓝字是表头(客户名称/始发地/货物类型被合并),
|
||||||
|
* 第 8–9 行是「始发地 | 目的地 | 最低收费 | 元/KG」。
|
||||||
|
* 城市和品名不得盖掉这些字。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void landGroupageKeepsTemplateHeaderWords() throws Exception {
|
||||||
|
Path src = Files.createTempFile("quote-land-groupage-real", ".xlsx");
|
||||||
|
Path out = Files.createTempFile("quote-land-groupage-real-out", ".xlsx");
|
||||||
|
try (Workbook wb = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = wb.createSheet("中越集拼报价");
|
||||||
|
org.apache.poi.ss.usermodel.Row band = row(sheet, 4);
|
||||||
|
band.createCell(1).setCellValue("业务员");
|
||||||
|
band.createCell(2).setCellValue("客户名称");
|
||||||
|
band.createCell(4).setCellValue("始发地");
|
||||||
|
band.createCell(6).setCellValue("条款");
|
||||||
|
band.createCell(10).setCellValue("发货日期");
|
||||||
|
band.createCell(11).setCellValue("时效");
|
||||||
|
band.createCell(13).setCellValue("货物类型");
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 2, 3));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 4, 5));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 13, 14));
|
||||||
|
row(sheet, 5);
|
||||||
|
org.apache.poi.ss.usermodel.Row matrix = row(sheet, 7);
|
||||||
|
matrix.createCell(1).setCellValue("始发地");
|
||||||
|
matrix.createCell(2).setCellValue("目的地");
|
||||||
|
matrix.createCell(4).setCellValue("2-500KG");
|
||||||
|
matrix.createCell(9).setCellValue("币种");
|
||||||
|
matrix.createCell(10).setCellValue("燃油附加费率");
|
||||||
|
org.apache.poi.ss.usermodel.Row sub = row(sheet, 8);
|
||||||
|
sub.createCell(3).setCellValue("最低收费");
|
||||||
|
sub.createCell(4).setCellValue("元/KG");
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 1, 1));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 2, 2));
|
||||||
|
row(sheet, 9);
|
||||||
|
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||||
|
wb.write(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSONObject mapping = new JSONObject(true);
|
||||||
|
mapping.put("sheet", "中越集拼报价");
|
||||||
|
JSONObject fields = new JSONObject(true);
|
||||||
|
fields.put("sales_name", "中越集拼报价!C5");
|
||||||
|
fields.put("origin", "中越集拼报价!F5");
|
||||||
|
fields.put("commodity", "中越集拼报价!O5");
|
||||||
|
fields.put("quote_date", "中越集拼报价!K5");
|
||||||
|
mapping.put("fields", fields);
|
||||||
|
JSONArray regions = new JSONArray();
|
||||||
|
JSONObject region = new JSONObject(true);
|
||||||
|
region.put("headerRow", 8);
|
||||||
|
region.put("dataStartRow", 9);
|
||||||
|
JSONObject columns = new JSONObject(true);
|
||||||
|
columns.put("origin", "B");
|
||||||
|
columns.put("destination", "C");
|
||||||
|
region.put("columns", columns);
|
||||||
|
regions.add(region);
|
||||||
|
mapping.put("routeRegions", regions);
|
||||||
|
JSONObject facts = new JSONObject(true);
|
||||||
|
facts.put("sales_name", "LinMianXun");
|
||||||
|
facts.put("origin", "上海");
|
||||||
|
facts.put("destination", "胡志明");
|
||||||
|
facts.put("commodity", "茶叶");
|
||||||
|
facts.put("quote_date", "2026-10-09");
|
||||||
|
facts.put("route_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||||
|
"[{\"origin\":\"上海\",\"destination\":\"胡志明\"}]"));
|
||||||
|
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||||
|
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||||
|
Sheet sheet = filled.getSheetAt(0);
|
||||||
|
assertEquals("客户名称", read(sheet, 4, 2));
|
||||||
|
assertEquals("始发地", read(sheet, 4, 4));
|
||||||
|
assertEquals("货物类型", read(sheet, 4, 13));
|
||||||
|
assertEquals("发货日期", read(sheet, 4, 10));
|
||||||
|
assertEquals("LinMianXun", read(sheet, 5, 1));
|
||||||
|
assertEquals("", read(sheet, 5, 2));
|
||||||
|
assertEquals("上海", read(sheet, 5, 4));
|
||||||
|
assertEquals("茶叶", read(sheet, 5, 13));
|
||||||
|
assertEquals("2026-10-09", read(sheet, 5, 10));
|
||||||
|
assertEquals("始发地", read(sheet, 7, 1));
|
||||||
|
assertEquals("目的地", read(sheet, 7, 2));
|
||||||
|
assertEquals("最低收费", read(sheet, 8, 3));
|
||||||
|
assertEquals("", read(sheet, 7, 3));
|
||||||
|
assertEquals("上海", read(sheet, 9, 1));
|
||||||
|
assertEquals("胡志明", read(sheet, 9, 2));
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(src);
|
||||||
|
Files.deleteIfExists(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销售没填客户名称时,客户名称格保持空白,不能写成业务员。
|
||||||
|
* 询价条带的时效写在表头下一行;公斤分档价按 weightPrices 落入各列。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void landGroupageLeavesCustomerBlankAndFillsTransitAndKgPrices() throws Exception {
|
||||||
|
Path src = Files.createTempFile("quote-land-groupage-price", ".xlsx");
|
||||||
|
Path out = Files.createTempFile("quote-land-groupage-price-out", ".xlsx");
|
||||||
|
try (Workbook wb = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = wb.createSheet("中越集拼报价");
|
||||||
|
org.apache.poi.ss.usermodel.Row band = row(sheet, 4);
|
||||||
|
band.createCell(1).setCellValue("业务员");
|
||||||
|
band.createCell(2).setCellValue("客户名称");
|
||||||
|
band.createCell(4).setCellValue("始发地");
|
||||||
|
band.createCell(10).setCellValue("发货日期");
|
||||||
|
band.createCell(11).setCellValue("时效");
|
||||||
|
band.createCell(13).setCellValue("货物类型");
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 2, 3));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 4, 5));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 11, 12));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 13, 14));
|
||||||
|
row(sheet, 5);
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(5, 5, 11, 12));
|
||||||
|
org.apache.poi.ss.usermodel.Row matrix = row(sheet, 7);
|
||||||
|
matrix.createCell(1).setCellValue("始发地");
|
||||||
|
matrix.createCell(2).setCellValue("目的地");
|
||||||
|
matrix.createCell(4).setCellValue("2-500KG");
|
||||||
|
matrix.createCell(5).setCellValue("501-1000KG");
|
||||||
|
matrix.createCell(6).setCellValue("1001-3000KG");
|
||||||
|
matrix.createCell(7).setCellValue("3001-5000KG");
|
||||||
|
matrix.createCell(8).setCellValue("5001KG以上");
|
||||||
|
matrix.createCell(15).setCellValue("时效");
|
||||||
|
org.apache.poi.ss.usermodel.Row sub = row(sheet, 8);
|
||||||
|
sub.createCell(3).setCellValue("最低收费");
|
||||||
|
sub.createCell(4).setCellValue("元/KG");
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 1, 1));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 2, 2));
|
||||||
|
row(sheet, 9);
|
||||||
|
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||||
|
wb.write(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSONArray routeRows = new JSONArray();
|
||||||
|
JSONObject route = new JSONObject(true);
|
||||||
|
route.put("origin", "广西壮族自治区");
|
||||||
|
route.put("destination", "胡志明");
|
||||||
|
route.put("currency", "CNY");
|
||||||
|
route.put("transit_time", "N+3");
|
||||||
|
routeRows.add(route);
|
||||||
|
JSONArray rawLand = new JSONArray();
|
||||||
|
JSONObject seg = new JSONObject(true);
|
||||||
|
seg.put("origin", "广西壮族自治区");
|
||||||
|
seg.put("destination", "胡志明");
|
||||||
|
seg.put("weightPrices", JSONArray.parseArray(
|
||||||
|
"[{\"min\":2,\"max\":500,\"price\":12.0},{\"min\":501,\"max\":1000,\"price\":11.5},"
|
||||||
|
+ "{\"min\":1001,\"max\":3000,\"price\":11.0},{\"min\":3001,\"max\":5000,\"price\":10.5},"
|
||||||
|
+ "{\"min\":5001,\"price\":10.0}]"));
|
||||||
|
rawLand.add(seg);
|
||||||
|
QuoteTemplateExcelSupport.copyMissingWeightPrices(routeRows, rawLand);
|
||||||
|
JSONObject mapping = new JSONObject(true);
|
||||||
|
mapping.put("sheet", "中越集拼报价");
|
||||||
|
JSONObject fields = new JSONObject(true);
|
||||||
|
fields.put("sales_name", "中越集拼报价!C5");
|
||||||
|
fields.put("customer_name", "中越集拼报价!D5");
|
||||||
|
fields.put("transit_time", "中越集拼报价!M5");
|
||||||
|
mapping.put("fields", fields);
|
||||||
|
JSONArray regions = new JSONArray();
|
||||||
|
JSONObject region = new JSONObject(true);
|
||||||
|
region.put("headerRow", 8);
|
||||||
|
region.put("dataStartRow", 9);
|
||||||
|
JSONObject columns = new JSONObject(true);
|
||||||
|
columns.put("origin", "B");
|
||||||
|
columns.put("destination", "C");
|
||||||
|
columns.put("transit_time", "P");
|
||||||
|
JSONObject vehicleRates = new JSONObject(true);
|
||||||
|
vehicleRates.put("2-500KG", "E");
|
||||||
|
vehicleRates.put("501-1000KG", "F");
|
||||||
|
vehicleRates.put("1001-3000KG", "G");
|
||||||
|
vehicleRates.put("3001-5000KG", "H");
|
||||||
|
vehicleRates.put("5001KG以上", "I");
|
||||||
|
columns.put("vehicleRates", vehicleRates);
|
||||||
|
region.put("columns", columns);
|
||||||
|
regions.add(region);
|
||||||
|
mapping.put("routeRegions", regions);
|
||||||
|
JSONObject facts = new JSONObject(true);
|
||||||
|
facts.put("sales_name", "LuoXiaoHua");
|
||||||
|
facts.put("transit_time", "N+3");
|
||||||
|
facts.put("transit_days", "N+3");
|
||||||
|
facts.put("route_rows", routeRows);
|
||||||
|
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||||
|
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||||
|
Sheet sheet = filled.getSheetAt(0);
|
||||||
|
assertEquals("客户名称", read(sheet, 4, 2));
|
||||||
|
assertEquals("时效", read(sheet, 4, 11));
|
||||||
|
assertEquals("LuoXiaoHua", read(sheet, 5, 1));
|
||||||
|
assertEquals("", read(sheet, 5, 2));
|
||||||
|
assertEquals("N+3", read(sheet, 5, 11));
|
||||||
|
assertEquals("2-500KG", read(sheet, 7, 4));
|
||||||
|
assertEquals("时效", read(sheet, 7, 15));
|
||||||
|
assertEquals("12.0", read(sheet, 9, 4));
|
||||||
|
assertEquals("11.5", read(sheet, 9, 5));
|
||||||
|
assertEquals("11.0", read(sheet, 9, 6));
|
||||||
|
assertEquals("10.5", read(sheet, 9, 7));
|
||||||
|
assertEquals("10.0", read(sheet, 9, 8));
|
||||||
|
assertEquals("N+3", read(sheet, 9, 15));
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(src);
|
||||||
|
Files.deleteIfExists(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送费用表头上的「时效」不能把 N+3 写到右边空白格,列头蓝底和文字都要留着。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void landGroupageDoesNotSpillTransitBesideDeliveryHeader() throws Exception {
|
||||||
|
Path src = Files.createTempFile("quote-land-delivery-src", ".xlsx");
|
||||||
|
Path out = Files.createTempFile("quote-land-delivery-out", ".xlsx");
|
||||||
|
try (Workbook wb = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = wb.createSheet("中越集拼报价");
|
||||||
|
org.apache.poi.ss.usermodel.CellStyle blue = wb.createCellStyle();
|
||||||
|
blue.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.ROYAL_BLUE.getIndex());
|
||||||
|
blue.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
||||||
|
org.apache.poi.ss.usermodel.Row band = row(sheet, 4);
|
||||||
|
band.createCell(1).setCellValue("业务员");
|
||||||
|
band.createCell(2).setCellValue("客户名称");
|
||||||
|
band.createCell(4).setCellValue("始发地");
|
||||||
|
band.createCell(11).setCellValue("时效");
|
||||||
|
band.getCell(11).setCellStyle(blue);
|
||||||
|
org.apache.poi.ss.usermodel.Row head = row(sheet, 18);
|
||||||
|
String[] labels = {"城市类别", "覆盖区域", "单价(元/票)", "燃油附加费率", "时效"};
|
||||||
|
int[] cols = {8, 10, 12, 13, 14};
|
||||||
|
for (int i = 0; i < labels.length; i++) {
|
||||||
|
org.apache.poi.ss.usermodel.Cell c = head.createCell(cols[i]);
|
||||||
|
c.setCellValue(labels[i]);
|
||||||
|
c.setCellStyle(blue);
|
||||||
|
}
|
||||||
|
row(sheet, 19);
|
||||||
|
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||||
|
wb.write(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSONObject facts = new JSONObject(true);
|
||||||
|
facts.put("transit_time", "N+3");
|
||||||
|
facts.put("transit_days", "N+3");
|
||||||
|
QuoteTemplateExcelSupport.fillWorkbook(src, out, new JSONObject(), facts);
|
||||||
|
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||||
|
Sheet sheet = filled.getSheetAt(0);
|
||||||
|
assertEquals("时效", read(sheet, 18, 14));
|
||||||
|
assertEquals("燃油附加费率", read(sheet, 18, 13));
|
||||||
|
assertEquals("", read(sheet, 18, 15));
|
||||||
|
assertEquals("", read(sheet, 19, 15));
|
||||||
|
assertEquals("N+3", read(sheet, 5, 11));
|
||||||
|
assertEquals(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND,
|
||||||
|
sheet.getRow(18).getCell(14).getCellStyle().getFillPattern());
|
||||||
|
assertEquals(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND,
|
||||||
|
sheet.getRow(18).getCell(13).getCellStyle().getFillPattern());
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(src);
|
||||||
|
Files.deleteIfExists(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目的地长地名不能按备注换行顶进蓝表头,红字要改回黑字。
|
||||||
|
* 配送费的覆盖区域、燃油附加费率不能写成 CNY,币种只留在运价表「币种」列。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void landGroupageKeepsCityOutOfHeaderAndCurrencyOutOfFuel() throws Exception {
|
||||||
|
Path src = Files.createTempFile("quote-land-city-src", ".xlsx");
|
||||||
|
Path out = Files.createTempFile("quote-land-city-out", ".xlsx");
|
||||||
|
try (Workbook wb = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = wb.createSheet("中越集拼报价");
|
||||||
|
org.apache.poi.ss.usermodel.Font white = wb.createFont();
|
||||||
|
white.setColor(org.apache.poi.ss.usermodel.IndexedColors.WHITE.getIndex());
|
||||||
|
white.setBold(true);
|
||||||
|
org.apache.poi.ss.usermodel.CellStyle band = wb.createCellStyle();
|
||||||
|
band.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.ROYAL_BLUE.getIndex());
|
||||||
|
band.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
||||||
|
band.setFont(white);
|
||||||
|
org.apache.poi.ss.usermodel.Row bandRow = row(sheet, 4);
|
||||||
|
org.apache.poi.ss.usermodel.Cell sales = bandRow.createCell(1);
|
||||||
|
sales.setCellValue("业务员");
|
||||||
|
sales.setCellStyle(band);
|
||||||
|
org.apache.poi.ss.usermodel.CellStyle matrixBlue = wb.createCellStyle();
|
||||||
|
matrixBlue.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.SKY_BLUE.getIndex());
|
||||||
|
matrixBlue.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
||||||
|
matrixBlue.setFont(white);
|
||||||
|
org.apache.poi.ss.usermodel.Row matrix = row(sheet, 7);
|
||||||
|
org.apache.poi.ss.usermodel.Cell originHead = matrix.createCell(1);
|
||||||
|
originHead.setCellValue("始发地");
|
||||||
|
originHead.setCellStyle(matrixBlue);
|
||||||
|
org.apache.poi.ss.usermodel.Cell destHead = matrix.createCell(2);
|
||||||
|
destHead.setCellValue("目的地");
|
||||||
|
destHead.setCellStyle(matrixBlue);
|
||||||
|
org.apache.poi.ss.usermodel.Cell currencyHead = matrix.createCell(9);
|
||||||
|
currencyHead.setCellValue("币种");
|
||||||
|
// 运价表头同样有燃油附加费率和时效。不能因此整段跳过,否则始发地、目的地都不写。
|
||||||
|
matrix.createCell(10).setCellValue("燃油附加费率");
|
||||||
|
matrix.createCell(15).setCellValue("时效");
|
||||||
|
matrix.getCell(15).setCellStyle(matrixBlue);
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 2, 2));
|
||||||
|
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 8, 9, 9));
|
||||||
|
org.apache.poi.ss.usermodel.Font red = wb.createFont();
|
||||||
|
red.setColor(org.apache.poi.ss.usermodel.IndexedColors.RED.getIndex());
|
||||||
|
org.apache.poi.ss.usermodel.CellStyle redOnSky = wb.createCellStyle();
|
||||||
|
redOnSky.cloneStyleFrom(matrixBlue);
|
||||||
|
redOnSky.setFont(red);
|
||||||
|
redOnSky.setWrapText(true);
|
||||||
|
redOnSky.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.TOP);
|
||||||
|
currencyHead.setCellStyle(redOnSky);
|
||||||
|
matrix.getCell(10).setCellStyle(redOnSky);
|
||||||
|
org.apache.poi.ss.usermodel.Row data = row(sheet, 9);
|
||||||
|
org.apache.poi.ss.usermodel.Cell dest = data.createCell(2);
|
||||||
|
dest.setCellStyle(redOnSky);
|
||||||
|
org.apache.poi.ss.usermodel.Row delivery = row(sheet, 18);
|
||||||
|
delivery.createCell(8).setCellValue("城市类别");
|
||||||
|
delivery.getCell(8).setCellStyle(matrixBlue);
|
||||||
|
delivery.createCell(10).setCellValue("覆盖区域");
|
||||||
|
delivery.getCell(10).setCellStyle(matrixBlue);
|
||||||
|
delivery.createCell(12).setCellValue("单价(元/票)");
|
||||||
|
delivery.getCell(12).setCellStyle(matrixBlue);
|
||||||
|
delivery.createCell(13).setCellValue("燃油附加费率");
|
||||||
|
delivery.getCell(13).setCellStyle(redOnSky);
|
||||||
|
delivery.createCell(14).setCellValue("时效");
|
||||||
|
delivery.getCell(14).setCellStyle(matrixBlue);
|
||||||
|
row(sheet, 19).createCell(12).setCellValue(200);
|
||||||
|
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||||
|
wb.write(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSONArray routeRows = new JSONArray();
|
||||||
|
JSONObject route = new JSONObject(true);
|
||||||
|
route.put("origin", "广西壮族自治区");
|
||||||
|
route.put("destination", "胡志明Hồ Chí Minh");
|
||||||
|
route.put("currency", "CNY");
|
||||||
|
routeRows.add(route);
|
||||||
|
JSONObject mapping = new JSONObject(true);
|
||||||
|
mapping.put("sheet", "中越集拼报价");
|
||||||
|
JSONArray regions = new JSONArray();
|
||||||
|
JSONObject rate = new JSONObject(true);
|
||||||
|
rate.put("headerRow", 8);
|
||||||
|
rate.put("dataStartRow", 10);
|
||||||
|
JSONObject rateCols = new JSONObject(true);
|
||||||
|
rateCols.put("origin", "B");
|
||||||
|
rateCols.put("destination", "C");
|
||||||
|
rateCols.put("currency", "J");
|
||||||
|
rate.put("columns", rateCols);
|
||||||
|
regions.add(rate);
|
||||||
|
JSONObject deliveryRegion = new JSONObject(true);
|
||||||
|
deliveryRegion.put("headerRow", 19);
|
||||||
|
deliveryRegion.put("dataStartRow", 20);
|
||||||
|
JSONObject deliveryCols = new JSONObject(true);
|
||||||
|
deliveryCols.put("origin", "I");
|
||||||
|
deliveryCols.put("destination", "K");
|
||||||
|
deliveryCols.put("currency", "N");
|
||||||
|
deliveryRegion.put("columns", deliveryCols);
|
||||||
|
regions.add(deliveryRegion);
|
||||||
|
mapping.put("routeRegions", regions);
|
||||||
|
JSONObject facts = new JSONObject(true);
|
||||||
|
facts.put("route_rows", routeRows);
|
||||||
|
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||||
|
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||||
|
Sheet sheet = filled.getSheetAt(0);
|
||||||
|
assertEquals("目的地", read(sheet, 7, 2));
|
||||||
|
assertEquals("胡志明Hồ Chí Minh", read(sheet, 9, 2));
|
||||||
|
assertEquals("CNY", read(sheet, 9, 9));
|
||||||
|
assertEquals(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER,
|
||||||
|
sheet.getRow(9).getCell(2).getCellStyle().getVerticalAlignment());
|
||||||
|
assertFalse(QuoteTemplateExcelSupport.fontIsRed(sheet.getRow(9).getCell(2)));
|
||||||
|
assertEquals("始发地", read(sheet, 7, 1));
|
||||||
|
assertEquals("币种", read(sheet, 7, 9));
|
||||||
|
byte[] originFill = QuoteTemplateExcelSupport.readFillRgb(sheet.getRow(7).getCell(1));
|
||||||
|
byte[] currencyFill = QuoteTemplateExcelSupport.readFillRgb(sheet.getRow(7).getCell(9));
|
||||||
|
byte[] salesFill = QuoteTemplateExcelSupport.readFillRgb(sheet.getRow(4).getCell(1));
|
||||||
|
byte[] deliveryFuelFill = QuoteTemplateExcelSupport.readFillRgb(sheet.getRow(18).getCell(13));
|
||||||
|
assertArrayEquals(originFill, currencyFill);
|
||||||
|
assertFalse(java.util.Arrays.equals(originFill, salesFill));
|
||||||
|
assertArrayEquals(originFill, deliveryFuelFill);
|
||||||
|
assertEquals("", read(sheet, 19, 8));
|
||||||
|
assertEquals("", read(sheet, 19, 10));
|
||||||
|
assertEquals("燃油附加费率", read(sheet, 18, 13));
|
||||||
|
assertEquals("", read(sheet, 19, 13));
|
||||||
|
assertEquals("200.0", read(sheet, 19, 12));
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(src);
|
||||||
|
Files.deleteIfExists(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 陆运国内拼车模板是标签栏,不是「费用名称+金额」表。
|
* 陆运国内拼车模板是标签栏,不是「费用名称+金额」表。
|
||||||
* TMS 的 元/CBM、送货费、时效必须写到标签右侧,不能落到 G 列或整张空着。
|
* TMS 的 元/CBM、送货费、时效必须写到标签右侧,不能落到 G 列或整张空着。
|
||||||
@@ -1329,6 +1708,27 @@ public class QuoteTemplateFillTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务员写中文名,不写企微账号。客户名称只留销售填的,账号不能占这一格。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void buildFillFactsUsesStaffNameAndKeepsCustomer() {
|
||||||
|
Map<String, Object> facts = new HashMap<>();
|
||||||
|
facts.put("sales_name", "林绵勋");
|
||||||
|
facts.put("客户名称", "南宁茶业");
|
||||||
|
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||||
|
.buildFillFacts("WO1", facts, new HashMap<>(), "LinMianXun");
|
||||||
|
assertEquals("林绵勋", out.getString("sales_name"));
|
||||||
|
assertEquals("南宁茶业", out.getString("customer_name"));
|
||||||
|
|
||||||
|
facts.put("customer_name", "LinMianXun");
|
||||||
|
facts.remove("客户名称");
|
||||||
|
JSONObject cleared = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||||
|
.buildFillFacts("WO1", facts, new HashMap<>(), "LinMianXun");
|
||||||
|
assertEquals("林绵勋", cleared.getString("sales_name"));
|
||||||
|
assertEquals("", cleared.getString("customer_name"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TMS 陆运 fee_lines 要投影成模板标签码,写出单时才能对上 元/CBM。
|
* TMS 陆运 fee_lines 要投影成模板标签码,写出单时才能对上 元/CBM。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BasicColumn, FormSchema } from '/@/components/Table';
|
|||||||
export const columns: BasicColumn[] = [
|
export const columns: BasicColumn[] = [
|
||||||
{ title: '分类', dataIndex: 'categoryTitle', width: 100 },
|
{ title: '分类', dataIndex: 'categoryTitle', width: 100 },
|
||||||
{ title: '分类码', dataIndex: 'categoryCode', width: 120 },
|
{ title: '分类码', dataIndex: 'categoryCode', width: 120 },
|
||||||
|
{ title: '调用编码', dataIndex: 'replyCode', width: 180 },
|
||||||
{ title: '触发场景', dataIndex: 'triggerScene', width: 220 },
|
{ title: '触发场景', dataIndex: 'triggerScene', width: 220 },
|
||||||
{ title: '回复正文', dataIndex: 'replyText', align: 'left' },
|
{ title: '回复正文', dataIndex: 'replyText', align: 'left' },
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,9 @@ export const searchFormSchema: FormSchema[] = [
|
|||||||
{ label: '协商中', value: 'negotiating' },
|
{ label: '协商中', value: 'negotiating' },
|
||||||
{ label: '未成交', value: 'lost' },
|
{ label: '未成交', value: 'lost' },
|
||||||
{ label: '已关闭', value: 'closed' },
|
{ label: '已关闭', value: 'closed' },
|
||||||
|
{ label: '事件异常', value: 'event-ex' },
|
||||||
|
{ label: '系统异常', value: 'sys-ex' },
|
||||||
|
{ label: '转人工', value: 'transfer' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -48,10 +52,19 @@ export const formSchema: FormSchema[] = [
|
|||||||
{ label: '协商中 negotiating', value: 'negotiating' },
|
{ label: '协商中 negotiating', value: 'negotiating' },
|
||||||
{ label: '未成交 lost', value: 'lost' },
|
{ label: '未成交 lost', value: 'lost' },
|
||||||
{ label: '已关闭 closed', value: 'closed' },
|
{ label: '已关闭 closed', value: 'closed' },
|
||||||
|
{ label: '事件异常 event-ex', value: 'event-ex' },
|
||||||
|
{ label: '系统异常 sys-ex', value: 'sys-ex' },
|
||||||
|
{ label: '转人工 transfer', value: 'transfer' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ field: 'categoryTitle', label: '分类标题', component: 'Input', required: true },
|
{ field: 'categoryTitle', label: '分类标题', component: 'Input', required: true },
|
||||||
|
{
|
||||||
|
field: 'replyCode',
|
||||||
|
label: '调用编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '如 lost.ask_reason,已有编码不可改' },
|
||||||
|
},
|
||||||
{ field: 'triggerScene', label: '触发场景', component: 'Input', required: true },
|
{ field: 'triggerScene', label: '触发场景', component: 'Input', required: true },
|
||||||
{
|
{
|
||||||
field: 'replyText',
|
field: 'replyText',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 机器人回复语:对接主账 /inquiry/botReply。
|
* 机器人回复语:对接主账 /inquiry/botReply。
|
||||||
* 分类壳用本地元数据;条目内容全部来自库。
|
* 分类壳用本地元数据;条目内容全部来自库。
|
||||||
|
* 调用编码仅展示/预置,本期不向智能体下发。
|
||||||
*/
|
*/
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
@@ -21,12 +22,14 @@ import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
|||||||
|
|
||||||
const categories = ref<ReplyCategory[]>(emptyCategories());
|
const categories = ref<ReplyCategory[]>(emptyCategories());
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const totalCount = ref(0);
|
||||||
const modalOpen = ref(false);
|
const modalOpen = ref(false);
|
||||||
const editing = ref<{ categoryId: string; item: ReplyItem | null }>({
|
const editing = ref<{ categoryId: string; item: ReplyItem | null }>({
|
||||||
categoryId: "",
|
categoryId: "",
|
||||||
item: null,
|
item: null,
|
||||||
});
|
});
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
|
replyCode: "",
|
||||||
trigger: "",
|
trigger: "",
|
||||||
text: "",
|
text: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -52,6 +55,7 @@ function groupByCategory(list: any[]) {
|
|||||||
if (row.categoryTitle) cat.title = row.categoryTitle;
|
if (row.categoryTitle) cat.title = row.categoryTitle;
|
||||||
cat.items.push({
|
cat.items.push({
|
||||||
id: String(row.id),
|
id: String(row.id),
|
||||||
|
replyCode: row.replyCode || "",
|
||||||
trigger: row.triggerScene || "",
|
trigger: row.triggerScene || "",
|
||||||
text: row.replyText || "",
|
text: row.replyText || "",
|
||||||
enabled: row.enabled === 1 || row.enabled === true,
|
enabled: row.enabled === 1 || row.enabled === true,
|
||||||
@@ -64,7 +68,14 @@ async function loadList() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res: any = await getReplyListAll();
|
const res: any = await getReplyListAll();
|
||||||
const list = Array.isArray(res) ? res : res?.result || res?.records || [];
|
const list = Array.isArray(res)
|
||||||
|
? res
|
||||||
|
: Array.isArray(res?.result)
|
||||||
|
? res.result
|
||||||
|
: Array.isArray(res?.records)
|
||||||
|
? res.records
|
||||||
|
: [];
|
||||||
|
totalCount.value = list.length;
|
||||||
categories.value = groupByCategory(list as any[]);
|
categories.value = groupByCategory(list as any[]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || "加载回复语失败");
|
message.error(e?.message || "加载回复语失败");
|
||||||
@@ -93,13 +104,14 @@ async function toggle(item: ReplyItem) {
|
|||||||
|
|
||||||
function openCreate(categoryId?: string) {
|
function openCreate(categoryId?: string) {
|
||||||
editing.value = { categoryId: categoryId || categories.value[0]?.id || "inquiring", item: null };
|
editing.value = { categoryId: categoryId || categories.value[0]?.id || "inquiring", item: null };
|
||||||
Object.assign(form, { trigger: "", text: "", enabled: true });
|
Object.assign(form, { replyCode: "", trigger: "", text: "", enabled: true });
|
||||||
modalOpen.value = true;
|
modalOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(categoryId: string, item: ReplyItem) {
|
function openEdit(categoryId: string, item: ReplyItem) {
|
||||||
editing.value = { categoryId, item };
|
editing.value = { categoryId, item };
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
|
replyCode: item.replyCode || "",
|
||||||
trigger: item.trigger,
|
trigger: item.trigger,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
@@ -114,7 +126,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
const cat = categories.value.find((c) => c.id === editing.value.categoryId);
|
const cat = categories.value.find((c) => c.id === editing.value.categoryId);
|
||||||
if (!cat) return;
|
if (!cat) return;
|
||||||
const body = {
|
const body: Record<string, any> = {
|
||||||
id: editing.value.item?.id,
|
id: editing.value.item?.id,
|
||||||
categoryCode: cat.id,
|
categoryCode: cat.id,
|
||||||
categoryTitle: cat.title,
|
categoryTitle: cat.title,
|
||||||
@@ -122,6 +134,11 @@ async function save() {
|
|||||||
replyText: form.text.trim(),
|
replyText: form.text.trim(),
|
||||||
enabled: form.enabled ? 1 : 0,
|
enabled: form.enabled ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
if (editing.value.item?.replyCode) {
|
||||||
|
body.replyCode = editing.value.item.replyCode;
|
||||||
|
} else if (form.replyCode.trim()) {
|
||||||
|
body.replyCode = form.replyCode.trim();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await saveReply(body, Boolean(editing.value.item));
|
await saveReply(body, Boolean(editing.value.item));
|
||||||
message.success(editing.value.item ? "话术已更新" : "话术已添加");
|
message.success(editing.value.item ? "话术已更新" : "话术已添加");
|
||||||
@@ -141,6 +158,7 @@ async function save() {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
|
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
|
||||||
|
<p v-else class="reply-total">共 {{ totalCount }} 条(按现网整理表应为 51 条;事件异常本期无条目)</p>
|
||||||
|
|
||||||
<article v-for="cat in categories" :key="cat.id" class="reply-category">
|
<article v-for="cat in categories" :key="cat.id" class="reply-category">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||||
@@ -154,6 +172,7 @@ async function save() {
|
|||||||
<div v-for="item in cat.items" :key="item.id" class="reply-item">
|
<div v-for="item in cat.items" :key="item.id" class="reply-item">
|
||||||
<div style="min-width: 0">
|
<div style="min-width: 0">
|
||||||
<div class="reply-meta">触发:{{ item.trigger }}</div>
|
<div class="reply-meta">触发:{{ item.trigger }}</div>
|
||||||
|
<div v-if="item.replyCode" class="reply-meta">调用编码:{{ item.replyCode }}</div>
|
||||||
<div class="reply-text">{{ item.text }}</div>
|
<div class="reply-text">{{ item.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0">
|
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0">
|
||||||
@@ -172,7 +191,7 @@ async function save() {
|
|||||||
<AdminModal
|
<AdminModal
|
||||||
v-model:open="modalOpen"
|
v-model:open="modalOpen"
|
||||||
:title="editing.item ? '编辑话术' : '添加话术'"
|
:title="editing.item ? '编辑话术' : '添加话术'"
|
||||||
size="md"
|
size="lg"
|
||||||
ok-text="保存"
|
ok-text="保存"
|
||||||
@ok="save"
|
@ok="save"
|
||||||
>
|
>
|
||||||
@@ -182,13 +201,28 @@ async function save() {
|
|||||||
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.title }}</option>
|
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.title }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="lbl">调用编码</label>
|
||||||
|
<input
|
||||||
|
v-model="form.replyCode"
|
||||||
|
class="field"
|
||||||
|
type="text"
|
||||||
|
:disabled="Boolean(editing.item?.replyCode)"
|
||||||
|
placeholder="如 lost.ask_reason;已有编码不可改"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label class="lbl">触发条件 <span class="req">*</span></label>
|
<label class="lbl">触发条件 <span class="req">*</span></label>
|
||||||
<input v-model="form.trigger" class="field" type="text" />
|
<input v-model="form.trigger" class="field" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row form-row-tall">
|
||||||
<label class="lbl">回复正文 <span class="req">*</span></label>
|
<label class="lbl">回复正文 <span class="req">*</span></label>
|
||||||
<textarea v-model="form.text" class="field" rows="5" placeholder="支持 {{ticket_no}} 等变量" />
|
<textarea
|
||||||
|
v-model="form.text"
|
||||||
|
class="field field-tall"
|
||||||
|
rows="12"
|
||||||
|
placeholder="支持 {{ticket_no}} 等变量"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label class="lbl">状态</label>
|
<label class="lbl">状态</label>
|
||||||
@@ -201,6 +235,22 @@ async function save() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.reply-total {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
.form-row-tall {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
textarea.field-tall {
|
||||||
|
height: 240px !important;
|
||||||
|
min-height: 240px !important;
|
||||||
|
padding: 8px 10px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
.check-line {
|
.check-line {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
export type ReplyItem = {
|
export type ReplyItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
/** 智能体日后按此码取文案;空表示未接入。 */
|
||||||
|
replyCode?: string;
|
||||||
trigger: string;
|
trigger: string;
|
||||||
text: string;
|
text: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 机器人回复语:完整照搬原型 admin-bot-replies.html(V1.0.24)。
|
* 机器人回复语 mock:与现网整理表、migration_inq_bot_reply_replace_v1.sql 同源。
|
||||||
* 共 9 类 37 条;与 deploy/sql/inquiry_bot_reply_proto_seed_v1.sql 同源。
|
* 正式页走主账 /inquiry/botReply;本文件仅对照,共 51 条。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ReplyItem = {
|
export type ReplyItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
replyCode?: string;
|
||||||
trigger: string;
|
trigger: string;
|
||||||
text: string;
|
text: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -26,47 +27,229 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-blue",
|
tagClass: "tag-blue",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r1",
|
id: "brv10000000000000000000000000001",
|
||||||
|
replyCode: "inquiring.first_chat",
|
||||||
trigger: "销售首次单聊机器人",
|
trigger: "销售首次单聊机器人",
|
||||||
text: "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。",
|
text: "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r2",
|
id: "brv10000000000000000000000000002",
|
||||||
trigger: "销售上传 Excel / 图片附件",
|
replyCode: "inquiring.ask_transport",
|
||||||
text: "已收到附件,正在识别路线、货量、品名和时效要求,原始文件将存为工单需求附件。工单 {{ticket_no}} 状态:询价中。",
|
trigger: "未识别运输方式",
|
||||||
|
text: "请回复数字或文字,告知我询价的运输方式:\n1.陆运\n2.海运\n3.空运",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r3",
|
id: "brv10000000000000000000000000003",
|
||||||
trigger: "AI 解析成功 → 创建工单",
|
replyCode: "inquiring.continue_need_wo",
|
||||||
text: "已识别:{{biz_type}} · {{origin}} → {{destination}} · {{cargo_desc}}。工单 {{ticket_no}} 已创建,正在查询 TMS 标准报价…",
|
trigger: "续办但未提供工单号",
|
||||||
|
text: "请告诉我要继续的工单号,例如 WO202609140034。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r4",
|
id: "brv10000000000000000000000000004",
|
||||||
trigger: "AI 解析失败 → 要求补充参数",
|
replyCode: "inquiring.continue_not_found",
|
||||||
text: "未能完全理解需求,请补充或确认:起运地?目的地?件数?毛重?体积?如有品名和时效要求,也请一并补充。",
|
trigger: "续办找不到工单",
|
||||||
|
text: "找不到工单 {{ticket_no}},请核对工单号后再试。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r5",
|
id: "brv10000000000000000000000000005",
|
||||||
trigger: "TMS 命中标准价 → 询问是否内部协同",
|
replyCode: "inquiring.image_recognizing",
|
||||||
text: "TMS 标准报价合计 {{system_quote}},预计时效 {{eta}}。价格明细:空运价、操作费、文件费、报关费、提货费、目的地杂费、其他附加费。{{history_hint}}是否需要咨询内部{{internal_role_label}}人员,或直接生成报价单?",
|
trigger: "销售发送图片",
|
||||||
|
text: "正在识别图片",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r6",
|
id: "brv10000000000000000000000000006",
|
||||||
trigger: "销售确认创建内部协同群",
|
replyCode: "inquiring.image_tech_fail",
|
||||||
text: "正在创建内部协同群「{{group_name}}」,并拉入销售 {{sales_name}}、{{internal_staff}} 及询价机器人。工单 {{ticket_no}} 保持「询价中」,等待内部反馈。",
|
trigger: "图片识别失败",
|
||||||
|
text: "图片暂时没认出来,请再发一张或直接打字。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r7",
|
id: "brv10000000000000000000000000007",
|
||||||
trigger: "内部人员回复手工报价",
|
replyCode: "inquiring.attach_recognizing",
|
||||||
text: "收到 {{staff_name}}({{staff_role}})的手工报价,已结构化处理。报价 {{manual_quote}},附件 {{quote_attachment}} 已存入工单,正在整理报价方案…",
|
trigger: "销售发送附件",
|
||||||
|
text: "正在识别附件",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000008",
|
||||||
|
replyCode: "inquiring.attach_tech_fail",
|
||||||
|
trigger: "附件识别失败",
|
||||||
|
text: "附件暂时没认出来,请再传一次或直接打字。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000009",
|
||||||
|
replyCode: "inquiring.attach_h5_expired",
|
||||||
|
trigger: "H5 上传链接过期",
|
||||||
|
text: "已过期,请回企微重新说附件询价。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000010",
|
||||||
|
replyCode: "inquiring.attach_h5_ok",
|
||||||
|
trigger: "H5 上传成功",
|
||||||
|
text: "已收到,请回企微查看识别结果。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000011",
|
||||||
|
replyCode: "inquiring.pull_group_manual",
|
||||||
|
trigger: "请手动拉人进群并发送工单号",
|
||||||
|
text: "请手动拉相关人员和智能体到原有群,并在群聊发送工单编号{{ticket_no}}",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000012",
|
||||||
|
replyCode: "inquiring.skip_collab_ack",
|
||||||
|
trigger: "销售跳过内部协同",
|
||||||
|
text: "好的,已跳过内部协同。正在按 TMS 标准报价生成报价单草稿…",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000013",
|
||||||
|
replyCode: "inquiring.group_created",
|
||||||
|
trigger: "协同群创建成功",
|
||||||
|
text: "协同群已创建,群名与工单号一致:{{ticket_no}}。已邀请:销售{{sales_name}}、产品{{product_names}}。请在群聊中继续确认报价。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000014",
|
||||||
|
replyCode: "inquiring.group_exists",
|
||||||
|
trigger: "协同群已存在",
|
||||||
|
text: "协同群已在,群名与工单号一致:{{ticket_no}}。请到该群继续确认报价。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000015",
|
||||||
|
replyCode: "inquiring.sea_no_staff",
|
||||||
|
trigger: "海运没有可拉的产品人员",
|
||||||
|
text: "没有可拉的产品人员。请先在后台「员工管理」维护本线路的海运产品岗后再试。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000016",
|
||||||
|
replyCode: "inquiring.land_no_staff",
|
||||||
|
trigger: "陆运没有可拉的产品人员",
|
||||||
|
text: "没有可拉的产品人员。请先在后台「员工管理」维护本线路的陆运产品岗后再试。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000017",
|
||||||
|
replyCode: "inquiring.group_create_fail",
|
||||||
|
trigger: "建群失败",
|
||||||
|
text: "建群失败,请稍后重试。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000018",
|
||||||
|
replyCode: "inquiring.quote_generating",
|
||||||
|
trigger: "报价单生成中",
|
||||||
|
text: "报价单正在生成,请稍候。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000019",
|
||||||
|
replyCode: "inquiring.air_ticket_not_found",
|
||||||
|
trigger: "空运群找不到工单",
|
||||||
|
text: "找不到工单{{ticket_no}},请核对工单号后再试。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000020",
|
||||||
|
replyCode: "inquiring.air_ticket_not_air",
|
||||||
|
trigger: "工单不是空运单",
|
||||||
|
text: "工单{{ticket_no}}不是空运单,不能在空运群里跟。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000021",
|
||||||
|
replyCode: "inquiring.air_ticket_occupied",
|
||||||
|
trigger: "空运工单已在另一个群",
|
||||||
|
text: "工单{{ticket_no}}已经在另一个群跟了,请到那个群继续。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000022",
|
||||||
|
replyCode: "inquiring.group_only_product",
|
||||||
|
trigger: "非产品人员在群里报价",
|
||||||
|
text: "只有产品能报价格,请产品在群里发价格或 Excel/PDF",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000023",
|
||||||
|
replyCode: "inquiring.group_file_saved",
|
||||||
|
trigger: "群附件已保存",
|
||||||
|
text: "附件已保存后台",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000024",
|
||||||
|
replyCode: "inquiring.air_lock_need_wo",
|
||||||
|
trigger: "空运锁舱/释放未带工单号",
|
||||||
|
text: "锁舱或释放舱位请带上工单号,例如 WO202609160001锁舱。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000025",
|
||||||
|
replyCode: "inquiring.air_lock_need_quote",
|
||||||
|
trigger: "没有报价不能锁舱",
|
||||||
|
text: "工单{{ticket_no}}还没有报价,不能锁舱。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000026",
|
||||||
|
replyCode: "inquiring.air_airline_only_cabin",
|
||||||
|
trigger: "只有航线能锁舱或释放",
|
||||||
|
text: "只有岗位为「航线·空运」的人能锁舱或释放舱位。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000027",
|
||||||
|
replyCode: "inquiring.air_lock_ok",
|
||||||
|
trigger: "空运锁舱成功",
|
||||||
|
text: "工单{{ticket_no}} 已通知TMS执行空运锁舱,执行成功。\n操作人:{{operator}}\n舱位选项:{{option_no}}\n执行编号:{{exec_no}}\n报价编号:{{quote_no}}",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000028",
|
||||||
|
replyCode: "inquiring.air_lock_fail",
|
||||||
|
trigger: "空运锁舱失败",
|
||||||
|
text: "工单{{ticket_no}} 空运锁舱失败。\n操作人:{{operator}}\n锁舱失败的原因:{{reason}}\n已通知IT运维排查,请稍等。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000029",
|
||||||
|
replyCode: "inquiring.air_release_ok",
|
||||||
|
trigger: "空运释放成功",
|
||||||
|
text: "工单{{ticket_no}} 已通知TMS执行空运释放舱位成功\n操作人:{{operator}}\n舱位选项:{{option_no}}\n执行编号:{{exec_no}}\n报价编号:{{quote_no}}",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000030",
|
||||||
|
replyCode: "inquiring.air_release_fail",
|
||||||
|
trigger: "空运释放失败",
|
||||||
|
text: "工单{{ticket_no}} 空运释放舱位失败。\n操作人:{{operator}}\n释放舱位失败的原因:{{reason}}\n已通知IT运维排查,请稍等。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000031",
|
||||||
|
replyCode: "inquiring.air_release_deal_blocked",
|
||||||
|
trigger: "已成交不能释放舱位",
|
||||||
|
text: "释放舱位失败,工单{{ticket_no}}已成交。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000032",
|
||||||
|
replyCode: "inquiring.air_release_need_lock",
|
||||||
|
trigger: "没有有效锁舱不能释放",
|
||||||
|
text: "工单{{ticket_no}}还没有有效锁舱,不能释放。",
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -76,41 +259,40 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-blue",
|
tagClass: "tag-blue",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r8",
|
id: "brv10000000000000000000000000033",
|
||||||
trigger: "报价方案整理完成 → 询问是否采用",
|
replyCode: "quoted.reject_ack",
|
||||||
text: "工单 {{ticket_no}} 状态已更新为「已报价」。整理后的报价方案:{{route_summary}} · {{quote_amount}} · {{eta}}。请确认是否采用:(采用,生成 Excel 报价单 / 采用,生成 PDF 报价单 / 不采用)",
|
trigger: "销售选择「不采用」",
|
||||||
|
text: "请问如何调整报价单,以满足您的需求?请说明要调哪一项费用,例如「提货费加 10%」。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r9",
|
id: "brv10000000000000000000000000034",
|
||||||
trigger: "销售确认采用 → 生成最终报价单",
|
replyCode: "quoted.adopt_excel",
|
||||||
text: "已确认采用该方案,正在生成 {{quote_format}} 最终报价单…工单保持「已报价」,完成后可直接转发给客户。",
|
trigger: "销售确认采用 → 生成 Excel",
|
||||||
|
text: "已确认采用该方案,正在生成 Excel 最终报价单…",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r10",
|
id: "brv10000000000000000000000000035",
|
||||||
|
replyCode: "quoted.adopt_pdf",
|
||||||
|
trigger: "销售确认采用 → 生成 PDF",
|
||||||
|
text: "已确认采用该方案,正在生成PDF最终报价单…",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000036",
|
||||||
|
replyCode: "quoted.template_missing",
|
||||||
|
trigger: "后台未维护报价模板",
|
||||||
|
text: "生成报价单失败,请联系IT运维维护后台报价模板。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000037",
|
||||||
|
replyCode: "quoted.file_ready",
|
||||||
trigger: "最终报价单生成完成",
|
trigger: "最终报价单生成完成",
|
||||||
text: "最终报价单已生成:{{quote_file}}({{quote_amount}} · {{route_summary}})。工单 {{ticket_no}} 保持「已报价」,报价单已归档。",
|
text: "已生成{{kind}}最终报价单:{{filename}}(工单 {{ticket_no}})",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r11",
|
|
||||||
trigger: "销售发送比例调价指令",
|
|
||||||
text: "已识别调价指令:在当前报价基础上{{adjust_direction}}{{adjust_percent}}%。原报价 {{original_quote}} → 新报价 {{new_quote}}。正在刷新报价并重新生成报价单,工单保持「已报价」。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r12",
|
|
||||||
trigger: "二次询价 · 关联历史工单",
|
|
||||||
text: "识别为二次询价,已关联历史工单 {{history_ticket_no}}。TMS 标准报价 {{system_quote}};上次报价 {{history_quote}},历史报价文档已引用至当前会话。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r13",
|
|
||||||
trigger: "报价单出具满*小时 → 发起成交跟进",
|
|
||||||
text: "工单 {{ticket_no}} 已于 {{quote_time}} 出具报价({{route_summary}} · {{quote_amount}})。请确认当前与客户的成交状态:(成交 / 未成交 / 协商中)",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,17 +302,12 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-green",
|
tagClass: "tag-green",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r14",
|
id: "brv10000000000000000000000000038",
|
||||||
trigger: "销售在成交跟进中选择「成交」",
|
replyCode: "deal.recorded_won",
|
||||||
text: "已记录为「已成交」。工单 {{ticket_no}} 状态已更新,成交确认时间 {{deal_time}} 已写入后台。已推送成交通知至 {{manager_name}}({{manager_role}})。",
|
trigger: "销售选择「成交」",
|
||||||
|
text: "已记录为「已成交」。工单 {{ticket_no}} 已关闭智能体跟进。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r15",
|
|
||||||
trigger: "成交确认 → 企微推送负责人",
|
|
||||||
text: "【成交通知】工单 {{ticket_no}} 已成交。销售:{{sales_name}} · 业务:{{biz_type}} · 路线:{{route_summary}}报价:{{quote_amount}} · 确认时间:{{deal_time}}推送对象:{{manager_name}}({{manager_role}})。请知悉并安排后续履约跟进。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -140,29 +317,12 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-amber",
|
tagClass: "tag-amber",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r16",
|
id: "brv10000000000000000000000000039",
|
||||||
trigger: "销售选择「不采用」当前报价方案",
|
replyCode: "negotiating.recorded",
|
||||||
text: "工单 {{ticket_no}} 状态已更新为「协商中」。请问希望如何调整?可说明调价幅度、换舱/时效要求,或是否需要重新拉内部{{internal_role_label}}协同。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r17",
|
|
||||||
trigger: "成交跟进中选择「协商中」",
|
trigger: "成交跟进中选择「协商中」",
|
||||||
text: "已记录为「协商中」。工单跟进周期已顺延 {{negotiate_extend_days}}个工作日,将于 {{next_followup_time}} 再次向您发起成交状态询问。",
|
text: "已记录为「协商中」。已顺延三个工作日,{{followup_when}}的9:00再次发起成交状态询问。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r18",
|
|
||||||
trigger: "协商跟进周期到期 → 再次询问",
|
|
||||||
text: "工单 {{ticket_no}} 协商跟进周期已到({{route_summary}} · {{quote_amount}})。请再次确认当前成交状态:(成交 / 未成交 / 协商中)",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r19",
|
|
||||||
trigger: "调价或重新协同完成 → 出具新方案",
|
|
||||||
text: "协商调整已完成,新报价方案:{{route_summary}} · {{quote_amount}} · {{eta}}。工单状态更新为「已报价」,请确认是否采用。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -172,23 +332,19 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-rose",
|
tagClass: "tag-rose",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r20",
|
id: "brv10000000000000000000000000040",
|
||||||
trigger: "成交跟进中选择「未成交」",
|
replyCode: "lost.ask_reason",
|
||||||
text: "请告知{{ticket_no}}「未成交」的原因(必填)。回复后系统将记录至工单 {{ticket_no}},并推送未成交通知至对应负责人。",
|
trigger: "成交跟进中选择「未成交」且未写原因",
|
||||||
|
text: "已记录为「未成交」。请补充未成交原因。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r21",
|
id: "brv10000000000000000000000000041",
|
||||||
trigger: "销售回复未成交原因 → 关单并通知负责人",
|
replyCode: "lost.reason_ack",
|
||||||
text: "已记录为「未成交」。原因:{{reject_reason}}。登记时间 {{reject_time}},已推送未成交通知至 {{manager_name}}({{manager_role}})。工单 {{ticket_no}} 已归档。",
|
trigger: "销售回复未成交原因",
|
||||||
|
text: "已记下未成交原因:{{lost_reason}}。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r22",
|
|
||||||
trigger: "未成交确认 → 企微推送负责人",
|
|
||||||
text: "【未成交通知】工单 {{ticket_no}} 未成交。销售:{{sales_name}} · 业务:{{biz_type}} · 路线:{{route_summary}}未成交原因:{{reject_reason}}登记时间:{{reject_time}} · 推送对象:{{manager_name}}({{manager_role}})。请知悉本单未成单情况。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -198,17 +354,12 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-rose",
|
tagClass: "tag-rose",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r23",
|
id: "brv10000000000000000000000000042",
|
||||||
trigger: "达到配置超时时间 → 系统自动关闭",
|
replyCode: "closed.air_cannot_activate",
|
||||||
text: "工单 {{ticket_no}} 因「{{close_reason}}」已达关闭条件,系统已自动关闭。关闭时间:{{close_time}}。本单不再发起成交跟进,亦不触发成交/未成交通知。",
|
trigger: "空运工单已是终态仍要激活",
|
||||||
|
text: "工单{{ticket_no}}已是「{{status}}」,不能再激活。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r24",
|
|
||||||
trigger: "销售/客服手动关闭工单",
|
|
||||||
text: "工单 {{ticket_no}} 已由 {{operator_name}} 手动关闭。关闭原因:{{close_reason}}。如需重新询价,请发起新的询价对话。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,24 +368,7 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
desc: "业务流程中参与方响应超时等业务侧异常,写入工单「事件异常」字段,需人工跟进。",
|
desc: "业务流程中参与方响应超时等业务侧异常,写入工单「事件异常」字段,需人工跟进。",
|
||||||
tagClass: "tag-amber",
|
tagClass: "tag-amber",
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
id: "r25",
|
|
||||||
trigger: "内部协同*小时无响应 → 群内提醒",
|
|
||||||
text: "@{{staff_name}} @{{sales_name}} 工单 {{ticket_no}} 已超过 {{timeout_hours}} 小时未收到内部协同回复,请尽快确认。事件异常:{{event_exception_type}}。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r26",
|
|
||||||
trigger: "内部协同超时达告警阈值 → 生成告警工单",
|
|
||||||
text: "工单 {{ticket_no}} 内部协同已超过 {{alert_hours}} 小时未响应,已生成「{{event_exception_type}}」告警,请管理员跟进。未响应人员:{{staff_name}}({{staff_role}})。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r27",
|
|
||||||
trigger: "成交跟进多次无响应 → 标记销售响应超时",
|
|
||||||
text: "工单 {{ticket_no}} 成交跟进已发起 {{followup_count}} 次,销售仍未回复。事件异常:销售响应超时。如继续无响应,系统将按配置自动关闭工单。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -244,29 +378,54 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-rose",
|
tagClass: "tag-rose",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r28",
|
id: "brv10000000000000000000000000043",
|
||||||
trigger: "大模型调用失败",
|
replyCode: "sys.llm_fail",
|
||||||
text: "AI 解析暂时不可用({{system_error_detail}})。工单 {{ticket_no}} 已标记系统异常:大模型异常。请稍后重试,或回复「人工报价」转人工处理。",
|
trigger: "大模型调用失败(销售侧)",
|
||||||
|
text: "大模型服务异常,IT排查中...",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r29",
|
id: "brv10000000000000000000000000044",
|
||||||
trigger: "TMS 接口调用失败",
|
replyCode: "sys.tms_fail",
|
||||||
text: "TMS 标准价库查询失败({{system_error_detail}})。工单已标记系统异常:TMS异常。可稍后重试,或选择转人工由内部人员报价。",
|
trigger: "TMS 接口失败(销售侧)",
|
||||||
|
text: "TMS接口异常,IT运维排查中...",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r30",
|
id: "brv10000000000000000000000000045",
|
||||||
trigger: "企微建群 / 添加成员失败",
|
replyCode: "sys.group_add_fail",
|
||||||
text: "创建协同群失败,无法添加 {{staff_name}}({{staff_wechat_id}})。系统异常:建群异常 · {{system_error_detail}}。已在后台生成异常工单,请联系管理员检查人员配置。",
|
trigger: "企微添加成员失败",
|
||||||
|
text: "无法添加 {{staff_name}} 同事,请联系管理员检查企业微信配置。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r31",
|
id: "brv10000000000000000000000000046",
|
||||||
trigger: "系统异常处理完成",
|
replyCode: "sys.tms_recovery",
|
||||||
text: "工单 {{ticket_no}} 系统异常「{{system_exception_type}}」已处理完毕,工单状态恢复为「{{restored_status}}」。处理记录:{{handle_note}}",
|
trigger: "TMS 已恢复",
|
||||||
|
text: "TMS已恢复正常使用,感谢您的耐心等待。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000047",
|
||||||
|
replyCode: "sys.llm_recovery",
|
||||||
|
trigger: "大模型已恢复",
|
||||||
|
text: "大模型已恢复正常使用,感谢您的耐心等待。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000048",
|
||||||
|
replyCode: "sys.cabin_recovery",
|
||||||
|
trigger: "空运舱位接口已恢复",
|
||||||
|
text: "空运舱位接口已恢复正常使用,感谢您的耐心等待。",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "brv10000000000000000000000000049",
|
||||||
|
replyCode: "sys.group_recovery",
|
||||||
|
trigger: "建群已恢复",
|
||||||
|
text: "建群已恢复正常,感谢您的耐心等待。",
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,41 +435,19 @@ export const mockReplyCategories: ReplyCategory[] = [
|
|||||||
tagClass: "tag-violet",
|
tagClass: "tag-violet",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "r32",
|
id: "brv10000000000000000000000000050",
|
||||||
trigger: "TMS 暂无报价",
|
replyCode: "transfer.already_collab",
|
||||||
text: "暂无 TMS 标准报价。触发转人工:{{transfer_reason}}。是否拉{{internal_role_label}}进群协同?(是 / 否)",
|
trigger: "该工单已拉过产品进群",
|
||||||
|
text: "该工单已拉产品进群协同。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "r33",
|
id: "brv10000000000000000000000000051",
|
||||||
trigger: "意图识别失败",
|
replyCode: "transfer.air_hint",
|
||||||
text: "已尝试 {{intent_retry}} 次澄清,仍无法识别询价意图。触发转人工:意图识别失败。是否拉{{internal_role_label}}进群协同?(是 / 否)",
|
trigger: "空运转人工(不自动建群)",
|
||||||
|
text: "触发转人工,请手动把航线和智能体拉进原有空运群,并发送工单号 {{ticket_no}}。",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "r34",
|
|
||||||
trigger: "参数提取不全",
|
|
||||||
text: "已连续 {{param_retry}} 次反问补充询价参数仍未完整。触发转人工:参数提取不全。是否拉{{internal_role_label}}进群协同?(是 / 否)",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r35",
|
|
||||||
trigger: "报价解析失败",
|
|
||||||
text: "触发转人工:{{transfer_reason}}(已连续失败 {{fail_count}} 次)。是否拉{{internal_role_label}}进群协同?(是 / 否)",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r36",
|
|
||||||
trigger: "空运锁舱/释放舱位失败",
|
|
||||||
text: "触发转人工:{{transfer_reason}}(已连续失败 {{fail_count}} 次)。请航线人员在 TMS 侧手工处理,或手动拉相关人员协助。请务必在群聊发送工单编号 {{ticket_no}}。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "r37",
|
|
||||||
trigger: "销售发送「人工报价」",
|
|
||||||
text: "已识别转人工指令。{{confirm_pull_group}}正在创建内部协同群,拉入销售 {{sales_name}}、{{internal_staff}} 及询价机器人…工单 {{ticket_no}} 进入「询价中」。",
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1399,3 +1399,19 @@ html {
|
|||||||
font-size: 11px !important;
|
font-size: 11px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗多行正文:必须压过上面 .field 的 height:34px。
|
||||||
|
* AdminModal 会 Teleport 到 body,页面 scoped 样式经常盖不住这条全局规则。
|
||||||
|
*/
|
||||||
|
textarea.field.field-tall {
|
||||||
|
height: 240px !important;
|
||||||
|
min-height: 240px !important;
|
||||||
|
padding: 8px 10px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
resize: vertical !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row.form-row-tall {
|
||||||
|
align-items: start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1680,6 +1680,11 @@ textarea:focus-visible {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-modal-body textarea.field.field-tall {
|
||||||
|
height: 240px;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-modal-body .perm-tree-box {
|
.admin-modal-body .perm-tree-box {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
Reference in New Issue
Block a user