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.MonthlyStatisticsMapper;
|
||||||
import me.iiv.agent.mapper.m0.PositionsMapper;
|
import me.iiv.agent.mapper.m0.PositionsMapper;
|
||||||
import me.iiv.agent.mapper.m0.ToiletMapper;
|
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.ToolObject;
|
||||||
import me.iiv.agent.mcp.XqTool;
|
import me.iiv.agent.mcp.XqTool;
|
||||||
import me.iiv.agent.permission.PermissionService;
|
import me.iiv.agent.permission.PermissionService;
|
||||||
@@ -93,7 +95,7 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
|||||||
@ToolParam(required = false, description = "厕所 id,正整数,精确匹配") Long toiletId) {
|
@ToolParam(required = false, description = "厕所 id,正整数,精确匹配") Long toiletId) {
|
||||||
|
|
||||||
if (districtId == null) {
|
if (districtId == null) {
|
||||||
throw new IllegalArgumentException("districtId 必填");
|
throw invalid("districtId 必填");
|
||||||
}
|
}
|
||||||
validatePositive(districtId, "districtId");
|
validatePositive(districtId, "districtId");
|
||||||
validatePositive(buildingId, "buildingId");
|
validatePositive(buildingId, "buildingId");
|
||||||
@@ -102,16 +104,16 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
|||||||
LocalDate end = parseDay(endDate, "endDate");
|
LocalDate end = parseDay(endDate, "endDate");
|
||||||
LocalDate today = LocalDate.now(SHANGHAI);
|
LocalDate today = LocalDate.now(SHANGHAI);
|
||||||
if (start.isAfter(end)) {
|
if (start.isAfter(end)) {
|
||||||
throw new IllegalArgumentException("startDate 不能晚于 endDate");
|
throw invalid("startDate 不能晚于 endDate");
|
||||||
}
|
}
|
||||||
if (end.isAfter(today)) {
|
if (end.isAfter(today)) {
|
||||||
throw new IllegalArgumentException("endDate 不得晚于今天");
|
throw invalid("endDate 不得晚于今天");
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(positionMode) && !"position".equals(positionMode) && !"wall".equals(positionMode)) {
|
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) {
|
if (deviceType != null && deviceType != 0 && deviceType != 1) {
|
||||||
throw new IllegalArgumentException("deviceType 仅支持 0、1");
|
throw invalid("deviceType 仅支持 0、1");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = AuthContextHolder.get().getUser();
|
User user = AuthContextHolder.get().getUser();
|
||||||
@@ -121,17 +123,16 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
|||||||
permissionService.applyDistrictRowFilter(districtWrapper, user);
|
permissionService.applyDistrictRowFilter(districtWrapper, user);
|
||||||
DistrictDistrictStrategy district = districtMapper.selectOne(districtWrapper);
|
DistrictDistrictStrategy district = districtMapper.selectOne(districtWrapper);
|
||||||
if (district == null) {
|
if (district == null) {
|
||||||
throw new IllegalStateException("站点不存在或无权访问");
|
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, "站点不存在或无权访问");
|
||||||
}
|
}
|
||||||
|
|
||||||
int queryDays = (int) ChronoUnit.DAYS.between(start, end) + 1;
|
int queryDays = (int) ChronoUnit.DAYS.between(start, end) + 1;
|
||||||
if (queryDays > MAX_QUERY_DAYS) {
|
if (queryDays > MAX_QUERY_DAYS) {
|
||||||
throw new IllegalArgumentException(
|
throw invalid("查询时间段最长 " + MAX_QUERY_DAYS + " 天(一个月),请修改日期范围");
|
||||||
"查询时间段最长 " + MAX_QUERY_DAYS + " 天(一个月),请修改日期范围");
|
|
||||||
}
|
}
|
||||||
int maxDays = statisticsDays();
|
int maxDays = statisticsDays();
|
||||||
if (queryDays > maxDays) {
|
if (queryDays > maxDays) {
|
||||||
throw new IllegalArgumentException("查询时间段最大" + maxDays + "天");
|
throw invalid("查询时间段最大" + maxDays + "天");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MonthlyStatistics> sourceRows = queryRows(districtId, start, end,
|
List<MonthlyStatistics> sourceRows = queryRows(districtId, start, end,
|
||||||
@@ -313,21 +314,26 @@ public class DistrictPaperPositionDetailTools implements ToolObject {
|
|||||||
|
|
||||||
private static LocalDate parseDay(String value, String name) {
|
private static LocalDate parseDay(String value, String name) {
|
||||||
if (!StringUtils.hasText(value)) {
|
if (!StringUtils.hasText(value)) {
|
||||||
throw new IllegalArgumentException(name + " 必填,格式 yyyy-MM-dd");
|
throw invalid(name + " 必填,格式 yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(value.trim());
|
return LocalDate.parse(value.trim());
|
||||||
} catch (java.time.format.DateTimeParseException e) {
|
} 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) {
|
private static void validatePositive(Long value, String name) {
|
||||||
if (value != null && value < 1) {
|
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) {
|
private static double round2(double value) {
|
||||||
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
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.MonthlyStatisticsMapper;
|
||||||
import me.iiv.agent.mapper.m0.PositionsMapper;
|
import me.iiv.agent.mapper.m0.PositionsMapper;
|
||||||
import me.iiv.agent.mapper.m0.ToiletMapper;
|
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.permission.PermissionService;
|
||||||
import me.iiv.agent.security.AuthContext;
|
import me.iiv.agent.security.AuthContext;
|
||||||
import me.iiv.agent.security.AuthContextHolder;
|
import me.iiv.agent.security.AuthContextHolder;
|
||||||
@@ -144,24 +146,25 @@ class DistrictPaperPositionDetailToolsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsInvalidArgumentsBeforeAnyQuery() {
|
void rejectsInvalidArgumentsBeforeAnyQuery() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
assertThrows(ToolCallException.class, () -> tools.districtPaperPositionDetailQuery(
|
||||||
null, start, end, null, null, null, null, null));
|
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));
|
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));
|
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));
|
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));
|
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));
|
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));
|
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));
|
9L, start, end, null, null, -1L, null, null));
|
||||||
assertThrows(IllegalArgumentException.class, () -> tools.districtPaperPositionDetailQuery(
|
ToolCallException exception = assertThrows(ToolCallException.class,
|
||||||
9L, start, end, null, null, null, null, 0L));
|
() -> tools.districtPaperPositionDetailQuery(9L, start, end, null, null, null, null, 0L));
|
||||||
|
assertEquals(McpErrorCode.INVALID_ARGUMENT, exception.getCode());
|
||||||
verifyNoInteractions(districtMapper, monthlyStatisticsMapper, positionsMapper, toiletMapper,
|
verifyNoInteractions(districtMapper, monthlyStatisticsMapper, positionsMapper, toiletMapper,
|
||||||
redisTemplate);
|
redisTemplate);
|
||||||
}
|
}
|
||||||
@@ -170,9 +173,10 @@ class DistrictPaperPositionDetailToolsTest {
|
|||||||
void rejectsInvisibleDistrict() {
|
void rejectsInvisibleDistrict() {
|
||||||
when(districtMapper.selectOne(any())).thenReturn(null);
|
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));
|
() -> 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());
|
verify(permissionService).applyDistrictRowFilter(any(), any());
|
||||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||||
}
|
}
|
||||||
@@ -195,9 +199,9 @@ class DistrictPaperPositionDetailToolsTest {
|
|||||||
mockDistrictAndDictionary();
|
mockDistrictAndDictionary();
|
||||||
String wideStart = today.minusDays(40).toString();
|
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));
|
() -> tools.districtPaperPositionDetailQuery(9L, wideStart, end, null, null, null, null, null));
|
||||||
assertTrue(exception.getMessage().contains("最长 31 天"));
|
assertTrue(exception.getRawMessage().contains("最长 31 天"));
|
||||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,9 +210,9 @@ class DistrictPaperPositionDetailToolsTest {
|
|||||||
mockDistrictAndDictionary();
|
mockDistrictAndDictionary();
|
||||||
when(hashOperations.get("Dictionary", "StatisticsDays")).thenReturn("5");
|
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));
|
() -> tools.districtPaperPositionDetailQuery(9L, start, end, null, null, null, null, null));
|
||||||
assertEquals("查询时间段最大5天", exception.getMessage());
|
assertEquals("查询时间段最大5天", exception.getRawMessage());
|
||||||
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
verifyNoInteractions(monthlyStatisticsMapper, positionsMapper, toiletMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user