fix(district): position.detail 报错透出对齐 ToolCallException (REQ-20261012-0003)
入参校验/超 31 天/超 StatisticsDays 抛 INVALID_ARGUMENT,站点无权抛 NOT_FOUND_OR_FORBIDDEN,具体文案透出给调用方(对齐 traffic/device_average), 不再被统一掩码成 DATA_INTEGRITY_ERROR。Redis StatisticsDays 缺失维持 IllegalStateException 兜底掩码(服务端配置问题)。
This commit is contained in:
+18
-12
@@ -10,6 +10,8 @@ import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
|
||||
import me.iiv.agent.mapper.m0.MonthlyStatisticsMapper;
|
||||
import me.iiv.agent.mapper.m0.PositionsMapper;
|
||||
import me.iiv.agent.mapper.m0.ToiletMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.mcp.ToolObject;
|
||||
import me.iiv.agent.mcp.XqTool;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
@@ -93,7 +95,7 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
||||
@ToolParam(required = false, description = "厕所 id,正整数,精确匹配") Long toiletId) {
|
||||
|
||||
if (districtId == null) {
|
||||
throw new IllegalArgumentException("districtId 必填");
|
||||
throw invalid("districtId 必填");
|
||||
}
|
||||
validatePositive(districtId, "districtId");
|
||||
validatePositive(buildingId, "buildingId");
|
||||
@@ -102,16 +104,16 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
||||
LocalDate end = parseDay(endDate, "endDate");
|
||||
LocalDate today = LocalDate.now(SHANGHAI);
|
||||
if (start.isAfter(end)) {
|
||||
throw new IllegalArgumentException("startDate 不能晚于 endDate");
|
||||
throw invalid("startDate 不能晚于 endDate");
|
||||
}
|
||||
if (end.isAfter(today)) {
|
||||
throw new IllegalArgumentException("endDate 不得晚于今天");
|
||||
throw invalid("endDate 不得晚于今天");
|
||||
}
|
||||
if (StringUtils.hasText(positionMode) && !"position".equals(positionMode) && !"wall".equals(positionMode)) {
|
||||
throw new IllegalArgumentException("positionMode 仅支持 position、wall");
|
||||
throw invalid("positionMode 仅支持 position、wall");
|
||||
}
|
||||
if (deviceType != null && deviceType != 0 && deviceType != 1) {
|
||||
throw new IllegalArgumentException("deviceType 仅支持 0、1");
|
||||
throw invalid("deviceType 仅支持 0、1");
|
||||
}
|
||||
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
@@ -121,17 +123,16 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
||||
permissionService.applyDistrictRowFilter(districtWrapper, user);
|
||||
DistrictDistrictStrategy district = districtMapper.selectOne(districtWrapper);
|
||||
if (district == null) {
|
||||
throw new IllegalStateException("站点不存在或无权访问");
|
||||
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, "站点不存在或无权访问");
|
||||
}
|
||||
|
||||
int queryDays = (int) ChronoUnit.DAYS.between(start, end) + 1;
|
||||
if (queryDays > MAX_QUERY_DAYS) {
|
||||
throw new IllegalArgumentException(
|
||||
"查询时间段最长 " + MAX_QUERY_DAYS + " 天(一个月),请修改日期范围");
|
||||
throw invalid("查询时间段最长 " + MAX_QUERY_DAYS + " 天(一个月),请修改日期范围");
|
||||
}
|
||||
int maxDays = statisticsDays();
|
||||
if (queryDays > maxDays) {
|
||||
throw new IllegalArgumentException("查询时间段最大" + maxDays + "天");
|
||||
throw invalid("查询时间段最大" + maxDays + "天");
|
||||
}
|
||||
|
||||
List<MonthlyStatistics> sourceRows = queryRows(districtId, start, end,
|
||||
@@ -313,21 +314,26 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
||||
|
||||
private static LocalDate parseDay(String value, String name) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
throw new IllegalArgumentException(name + " 必填,格式 yyyy-MM-dd");
|
||||
throw invalid(name + " 必填,格式 yyyy-MM-dd");
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim());
|
||||
} catch (java.time.format.DateTimeParseException e) {
|
||||
throw new IllegalArgumentException(name + " 格式非法,要求 yyyy-MM-dd: " + value);
|
||||
throw invalid(name + " 格式非法,要求 yyyy-MM-dd: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePositive(Long value, String name) {
|
||||
if (value != null && value < 1) {
|
||||
throw new IllegalArgumentException(name + " 必须为正整数");
|
||||
throw invalid(name + " 必须为正整数");
|
||||
}
|
||||
}
|
||||
|
||||
/** 入参类错误透出具体文案给调用方(对齐 traffic/device_average 风格),便于 LLM 自愈 */
|
||||
private static ToolCallException invalid(String message) {
|
||||
return new ToolCallException(McpErrorCode.INVALID_ARGUMENT, message);
|
||||
}
|
||||
|
||||
private static double round2(double value) {
|
||||
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
+20
-16
@@ -11,6 +11,8 @@ import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
|
||||
import me.iiv.agent.mapper.m0.MonthlyStatisticsMapper;
|
||||
import me.iiv.agent.mapper.m0.PositionsMapper;
|
||||
import me.iiv.agent.mapper.m0.ToiletMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContext;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
@@ -144,24 +146,25 @@ class DistrictPaperPositionDetailToolsTest {
|
||||
|
||||
@Test
|
||||
void rejectsInvalidArgumentsBeforeAnyQuery() {
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
null, start, end, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
0L, start, end, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, "2026/09/01", end, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, start, end, "bad", null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, start, end, null, 2, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, end, start, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, start, today.plusDays(1).toString(), null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, start, end, null, null, -1L, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||
9L, start, end, null, null, null, null, 0L));
|
||||
ToolCallException exception = assertThrows(ToolCallException.class,
|
||||
() -> tools.districtPaperPositionDetailQuery(9L, start, end, null, null, null, null, 0L));
|
||||
assertEquals(McpErrorCode.INVALID_ARGUMENT, exception.getCode());
|
||||
verifyNoInteractions(districtMapper, monthlyStatisticsMapper, positionsMapper, toiletMapper,
|
||||
redisTemplate);
|
||||
}
|
||||
@@ -170,9 +173,10 @@ class DistrictPaperPositionDetailToolsTest {
|
||||
void rejectsInvisibleDistrict() {
|
||||
when(districtMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
ToolCallException exception = assertThrows(ToolCallException.class,
|
||||
() -> tools.districtPaperPositionDetailQuery(9L, start, end, null, null, null, null, null));
|
||||
assertEquals("站点不存在或无权访问", exception.getMessage());
|
||||
assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, exception.getCode());
|
||||
assertEquals("站点不存在或无权访问", exception.getRawMessage());
|
||||
verify(permissionService).applyDistrictRowFilter(any(), any());
|
||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||
}
|
||||
@@ -195,9 +199,9 @@ class DistrictPaperPositionDetailToolsTest {
|
||||
mockDistrictAndDictionary();
|
||||
String wideStart = today.minusDays(40).toString();
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
ToolCallException exception = assertThrows(ToolCallException.class,
|
||||
() -> tools.districtPaperPositionDetailQuery(9L, wideStart, end, null, null, null, null, null));
|
||||
assertTrue(exception.getMessage().contains("最长 31 天"));
|
||||
assertTrue(exception.getRawMessage().contains("最长 31 天"));
|
||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||
}
|
||||
|
||||
@@ -206,9 +210,9 @@ class DistrictPaperPositionDetailToolsTest {
|
||||
mockDistrictAndDictionary();
|
||||
when(hashOperations.get("Dictionary", "StatisticsDays")).thenReturn("5");
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
ToolCallException exception = assertThrows(ToolCallException.class,
|
||||
() -> tools.districtPaperPositionDetailQuery(9L, start, end, null, null, null, null, null));
|
||||
assertEquals("查询时间段最大5天", exception.getMessage());
|
||||
assertEquals("查询时间段最大5天", exception.getRawMessage());
|
||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user