智能体按 reply_code 读后台回复语;陆运整车报价单顶栏色与填表更稳,工单页展示同步收紧。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-07 11:41:09 +08:00
co-authored by Cursor
parent 177dd0b25f
commit 419b4a1bf7
35 changed files with 2439 additions and 268 deletions
@@ -44,12 +44,28 @@ public class InquiryAgentController {
@Autowired
private org.jeecg.modules.inquiry.tms.InquiryTmsCabinService cabinService;
@Autowired
private org.jeecg.modules.inquiry.service.IInquiryBotReplyService botReplyService;
@Value("${inquiry.agent.callback-token:${AGENT_CALLBACK_TOKEN:}}")
private String agentToken;
@Value("${inquiry.agent.biz-token:${BIZ_API_TOKEN:}}")
private String bizToken;
/**
* 智能体只读回复语。停用或不在列表里的编码由智能体回退默认句。
* 鉴权同其它 /inquiry/agent/** ,禁止后台菜单权限码。
*/
@ApiOperation("智能体-已启用回复语")
@GetMapping("/botReply/listEnabled")
public Result<java.util.List<java.util.Map<String, Object>>> botReplyListEnabled(HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(botReplyService.listEnabledForAgent());
}
@ApiOperation("智能体-回写模板 AI 映射结果")
@PutMapping("/quoteTemplate/parseResult")
public Result<String> quoteTemplateParseResult(@RequestBody Map<String, Object> body,
@@ -21,7 +21,7 @@ import java.util.List;
/**
* 机器人回复语 API。
* 支持按分类拉取全部条目(后台九分类页)与分页查询。
* 调用编码 replyCode 给智能体日后按码取文案;本期只维护、不向智能体下发。
* 调用编码 replyCode 供智能体 GET /inquiry/agent/botReply/listEnabled 按码取文案。
*/
@Slf4j
@Api(tags = "询价-机器人回复语")
@@ -123,7 +123,7 @@ public class InquiryTicket implements Serializable {
private List<InquiryPriceLine> saleLines;
/**
* 利润点逐行。不入库。没调过价为空。最后一行是整单利润点。
* 利润点逐行。不入库。没调过价每项都是 0%。最后一行是整单利润点。
*/
@TableField(exist = false)
@ApiModelProperty("利润点逐行(展示)")
@@ -22,4 +22,10 @@ public interface IInquiryBotReplyService extends IService<InquiryBotReply> {
* 编辑正文/启停。已有调用编码不可被请求覆盖。
*/
void updateReply(InquiryBotReply body);
/**
* 智能体只读:已启用且已有调用编码的正文。
* 停用项不返回,智能体回退默认句。
*/
java.util.List<java.util.Map<String, Object>> listEnabledForAgent();
}
@@ -9,7 +9,13 @@ import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
@@ -90,6 +96,35 @@ public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMappe
body.setReplyCode(code);
}
@Override
public List<Map<String, Object>> listEnabledForAgent() {
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
qw.eq("enabled", 1);
qw.isNotNull("reply_code");
qw.ne("reply_code", "");
qw.orderByAsc("sort_no").orderByAsc("id");
Page<InquiryBotReply> page = new Page<>(1, 2000);
page.setSearchCount(false);
page.setMaxLimit(2000L);
List<InquiryBotReply> rows = page(page, qw).getRecords();
List<Map<String, Object>> out = new ArrayList<>();
if (rows == null) {
return out;
}
for (InquiryBotReply row : rows) {
if (row == null || StringUtils.isBlank(row.getReplyCode())) {
continue;
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("replyCode", row.getReplyCode());
item.put("replyText", row.getReplyText());
item.put("triggerScene", row.getTriggerScene());
item.put("categoryCode", row.getCategoryCode());
out.add(item);
}
return out;
}
private void assertCodeUnique(String replyCode, String excludeId) {
if (StringUtils.isBlank(replyCode)) {
return;
@@ -686,7 +686,7 @@ public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
return;
}
String username = StringUtils.trimToEmpty(login.getUsername());
String wecom = StringUtils.trimToEmpty(login.getWorkNo());
String wecom = dashboardWecomId(login);
qw.and(w -> {
boolean any = false;
if (StringUtils.isNotBlank(wecom)) {
@@ -734,4 +734,20 @@ public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
return false;
}
}
/**
* 看板「只看自己」用的企微工号。
* 不用直接调 getWorkNo:测服打包时基座 LoginUser 可能还没有这个方法。
*/
private static String dashboardWecomId(LoginUser login) {
if (login == null) {
return "";
}
try {
Object v = login.getClass().getMethod("getWorkNo").invoke(login);
return v == null ? "" : StringUtils.trimToEmpty(String.valueOf(v));
} catch (ReflectiveOperationException ignored) {
return "";
}
}
}
@@ -19,7 +19,9 @@ import java.util.List;
* <p>
* 职责:从已保存的报价明细铺开展示。不改调价算法,不把各行金额加总冒充费用合计。
* 海运/陆运:成本是调价前生效价(手工同名盖 TMS,多出来的补上);销售是最新调价。
* 空运:成本整列空;销售是当前生效价;利润点只在调过价之后,对调价前销售价。
* 空运:成本整列空;销售是当前生效价。
* 未调价:各项利润点与费用合计都是 0%,禁止拿金额去填利润点列。
* 已调价:利润点对调价前同名行。
* 副作用:改 ticket 的三列逐行;手工盖价算出新的官方总价时,同时改成本价和利润点字符串。不写库。
* 调用:{@link InquiryTicketPriceSupport#apply} 末尾。列表 enrich 与保存报价后的 update 都走那里。
*/
@@ -126,7 +128,7 @@ public final class InquiryQuoteLineSupport {
profitTotal = StringUtils.defaultString(InquiryTicketPriceSupport.profitText(saleTotal, baseTotal));
ticket.setProfitRate(profitTotal);
} else if (!adjusted) {
ticket.setProfitRate("");
ticket.setProfitRate("0%");
}
if (air) {
@@ -134,13 +136,13 @@ public final class InquiryQuoteLineSupport {
ticket.setSaleLines(moneyLines(saleFees, saleTotal));
ticket.setProfitLines(adjusted && StringUtils.isNotBlank(profitTotal)
? profitAgainst(baselineFees, saleFees, profitTotal)
: Collections.<InquiryPriceLine>emptyList());
: zeroPercentLines(saleFees));
return;
}
if (!adjusted) {
ticket.setCostLines(moneyLines(costFees, costTotal));
ticket.setSaleLines(Collections.<InquiryPriceLine>emptyList());
ticket.setProfitLines(Collections.<InquiryPriceLine>emptyList());
ticket.setProfitLines(zeroPercentLines(costFees));
return;
}
List<Aligned> rows = align(costFees, saleFees);
@@ -216,6 +218,21 @@ public final class InquiryQuoteLineSupport {
return formatMoney(base.add(delta), totalCcy);
}
/**
* 未调价:每一项费用和费用合计都写 0%。
* 列表/详情都走这里,避免利润点列空白或误把金额填进去。
*/
private static List<InquiryPriceLine> zeroPercentLines(List<FeeRow> fees) {
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
if (fees != null) {
for (FeeRow fee : fees) {
lines.add(line(fee.name, "0%"));
}
}
lines.add(line(TOTAL_NAME, "0%"));
return lines;
}
private static List<InquiryPriceLine> moneyLines(List<FeeRow> fees, String totalText) {
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
if (fees != null) {
@@ -14,7 +14,8 @@ import java.util.regex.Pattern;
* 后台成本价 / 销售价 / 利润点:按业务线从报价记录重算。
* <p>
* 海运/陆运:TMS、产品报价是成本;销售调价后才有销售价,利润点=(销售-成本)/成本。
* 空运:成本价始终空;TMS/航线/调价都是销售侧;利润点只在销售调过价后算。
* 空运:成本价始终空;TMS/航线/调价都是销售侧。
* 未调价时利润点固定 0%(含各项),调过价才按差额算。
* 副作用:只改传入 ticket 的展示字段(三个总价字符串和三列逐行),不写库。调用方决定是否 updateById。
*/
public final class InquiryTicketPriceSupport {
@@ -63,7 +64,7 @@ public final class InquiryTicketPriceSupport {
ticket.setProfitRate(profitText(adjustPrice, baseline));
} else {
ticket.setSalePrice(StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice);
ticket.setProfitRate("");
ticket.setProfitRate("0%");
}
} else {
String cost = StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice;
@@ -73,7 +74,7 @@ public final class InquiryTicketPriceSupport {
ticket.setProfitRate(profitText(adjustPrice, cost));
} else {
ticket.setSalePrice("");
ticket.setProfitRate("");
ticket.setProfitRate("0%");
}
}
// 三个总价定完后,按同一批报价拆成逐行。有手工明细盖 TMS 时,这里会改写成本价和利润点。
@@ -689,7 +689,7 @@ public class InquiryTmsQueryService {
/**
* 把本次 TMS 报价写回工单主表,供后台列表/详情展示。
* 海运/陆运进成本价,空运进销售价;利润点必须等销售调价后才算。
* 海运/陆运进成本价,空运进销售价;未调价利润点为 0%,调过价才按差额算。
*/
static void applyQuoteToTicket(InquiryTicket ticket, Map<String, String> quote) {
if (ticket == null || quote == null) {