diff --git a/xiaoqu-agent-service/README.md b/xiaoqu-agent-service/README.md index da22bd6..ee65fb1 100644 --- a/xiaoqu-agent-service/README.md +++ b/xiaoqu-agent-service/README.md @@ -92,6 +92,7 @@ xiaoqu-agent-service/ | `pickup.order.list.query` | workorder | read | 取货工单分页查询;申请/取货/完成日期独立过滤,物流按页批量富化 | | `pickup.order.detail.query` | workorder | read | 取货工单详情;返回物流、取货物品、SN 调整、附件、流程及关联单据 | | `district.paper.statistics.query` | hardware | read | 客户用纸统计(复刻老 /MonthlyStatistics/getMonthlyStatisticsMap)。入参 districtId/startDate/endDate/positionMode/deviceType/buildingId/floor/toiletId;出参 toiletGenders、summaryByGender(paperLengthM 米/useCount/perUseLengthM/deviceDayCount/rfidCount)、daily 按日明细。DB 聚合 m0 monthly_statistics;rfidCount 查 ES paperuselog(90 天热/冷索引,LocalEsDate 云/本地集群分界,rfid_use_flag 分口径)。详细设计见 doc/district-paper-statistics-design.md | +| `battery.fault.list.query` | hardware | read | 电池故障分页查询。按站点/物业/代理商/SN/设备类型/故障类型/恢复状态/故障日期筛选;沿用设备行级权限,按故障时间和 ID 倒序。详细设计见 doc/battery-fault-mcp-tool-design.md | 分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。 diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md index 8733420..62ce5d1 100644 --- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md +++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md @@ -782,6 +782,21 @@ --- +## 31. battery.fault.list.query + +分页查询电池故障记录,对应旧系统 `POST /batteryFault/batteryFaultLogList`。 + +- 分类:hardware | 类型:读 +- 数据来源:m0 `battery_fault_log`,固定 `soft_delete=0`。 +- 入参:`districtId/propertyId/agentId`(正整数)、`snCode`(模糊匹配)、`deviceType`(0 厕纸机/出纸机、1 擦手纸机)、`faultType`(0 温度、1 电压、2 电流、3 充电次数)、`faultState`(0 未恢复、1 已恢复)、`faultDateFrom/faultDateTo`(yyyy-MM-dd,结束日期含当天)、`page/rows`。 +- 分页默认 1/20、上限 100,固定按 `fault_start_time DESC, id DESC` 稳定排序。 +- 行级权限与 `hardware.list.query` 一致:总部查看全部;代理查看自己、直属下级、代管代理及代管物业;销售非负责人限自己创建及授权范围内物业。服务身份仍由 MCP scope 控制。 +- 返回 `id/snCode/deviceType/deviceTypeText/faultType/faultTypeText/faultState/faultStateText/peakValue/standardValue/faultStartTime/faultAddTime/faultRecoveryTime/installPosition/agentId/agentName/propertyId/propertyName/districtId/districtName`。时间按 Asia/Shanghai 输出;峰值和标准值保留数据库原始整数,不推断单位。 + +详细设计见 `battery-fault-mcp-tool-design.md`。 + +--- + ## 附录:在线查看工具清单 ```bash diff --git a/xiaoqu-agent-service/doc/battery-fault-mcp-tool-design.md b/xiaoqu-agent-service/doc/battery-fault-mcp-tool-design.md new file mode 100644 index 0000000..9dc8a55 --- /dev/null +++ b/xiaoqu-agent-service/doc/battery-fault-mcp-tool-design.md @@ -0,0 +1,32 @@ +# 电池故障列表 MCP 工具设计 + +需求:REQ-20260921-0006 + +## 目标 + +把旧系统 `/batteryFault/batteryFaultLogList` 封装为只读 MCP 工具 `battery.fault.list.query`,供 Agent 查询授权范围内的电池故障记录。旧接口注释掉了权限过滤,本工具补齐与出纸机列表一致的行级权限。 + +## 查询契约 + +- 数据源:m0 `battery_fault_log`,固定 `soft_delete=0`。 +- 筛选:站点、物业、代理商、SN 模糊匹配、设备类型、故障类型、恢复状态、故障日期范围。 +- 日期:`yyyy-MM-dd`;结束日期转为下一天 00:00 的排他上界,保证包含当天且不依赖毫秒精度。 +- 分页:默认 1/20,rows 上限 100,超限截断并返回 notice。 +- 排序:`fault_start_time DESC, id DESC`,避免同一故障时间下翻页漂移。 +- 查询次数:一次 count、一次分页 select;组织名称直接读取日志快照字段,无 N+1。 + +## 权限 + +`PermissionService.applyBatteryFaultLogRowFilter` 复用 `applyHardwareRowFilter` 的范围计算:总部不加过滤;代理范围为自己、直属下级、授权代管代理,并与授权代管物业取并集;销售非负责人仅按其可见物业过滤。显式传入的 agent/property/district 条件与权限条件叠加,越权时自然返回空列表。 + +tenant_service 服务身份按总部数据视角不加行过滤,但仍须通过 MCP 工具 scope 校验;缺失身份上下文继续 fail closed。 + +## 返回字段 + +返回设备、故障类型及状态(同时含 code/text)、原始峰值/标准值、故障发生/入列/恢复时间、安装位置,以及代理商/物业/站点 ID 和名称。时间统一格式化为 Asia/Shanghai `yyyy-MM-dd HH:mm:ss`,数值单位因旧代码未定义而不做推断或换算。 + +## 验证 + +- `BatteryFaultToolsTest`:参数校验、筛选 SQL、日期边界、分页截断、稳定排序、字段与文案映射、时区、Spring AI schema。 +- `PermissionServiceBatteryFaultRowFilterTest`:总部和代理+代管物业权限分支。 +- 全量 Maven 测试用于确认工具注册、权限和既有功能无回归。 diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m0/BatteryFaultLog.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m0/BatteryFaultLog.java new file mode 100644 index 0000000..ad2ffa8 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m0/BatteryFaultLog.java @@ -0,0 +1,45 @@ +package me.iiv.agent.entity.m0; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Date; + +/** 电池故障日志(m0.battery_fault_log)。 */ +@Getter +@Setter +@ToString +@TableName("battery_fault_log") +public class BatteryFaultLog implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Long createId; + private Date createTime; + private Long modifyId; + private Date modifyTime; + private Integer softDelete; + private String snCode; + private Integer deviceType; + private Integer faultType; + private Integer faultState; + private Date faultStartTime; + private Date faultAddTime; + private Date faultRecoveryTime; + private String installPosition; + private Long districtId; + private String districtName; + private Long propertyId; + private String propertyName; + private Long agentId; + private String agentName; + private Integer peakValue; + private Integer standardValue; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m0/BatteryFaultLogMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m0/BatteryFaultLogMapper.java new file mode 100644 index 0000000..30150e1 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m0/BatteryFaultLogMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m0; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m0.BatteryFaultLog; + +public interface BatteryFaultLogMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/BatteryFaultTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/BatteryFaultTools.java new file mode 100644 index 0000000..93a6325 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/BatteryFaultTools.java @@ -0,0 +1,172 @@ +package me.iiv.agent.mcp.tool; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import me.iiv.agent.entity.m0.BatteryFaultLog; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.mapper.m0.BatteryFaultLogMapper; +import me.iiv.agent.mcp.ToolObject; +import me.iiv.agent.mcp.XqTool; +import me.iiv.agent.permission.PermissionService; +import me.iiv.agent.security.AuthContextHolder; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** 电池故障列表查询工具,对应老系统 /batteryFault/batteryFaultLogList。 */ +@Component +public class BatteryFaultTools implements ToolObject { + + private static final int DEFAULT_ROWS = 20; + private static final int MAX_ROWS = 100; + private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE); + + final BatteryFaultLogMapper mapper; + final PermissionService permissionService; + + public BatteryFaultTools(BatteryFaultLogMapper mapper, PermissionService permissionService) { + this.mapper = mapper; + this.permissionService = permissionService; + } + + @XqTool(category = "hardware", kind = "read") + @Tool(name = "battery.fault.list.query", description = "分页查询当前身份可见的电池故障记录,对应老系统" + + " /batteryFault/batteryFaultLogList。支持站点、物业、代理商、设备 SN、设备类型、故障类型、" + + "恢复状态及故障日期范围筛选;page 默认 1,rows 默认 20、上限 100;" + + "固定按 fault_start_time DESC、id DESC 排序。") + public Map batteryFaultListQuery( + @ToolParam(required = false, description = "站点 id,正整数") Long districtId, + @ToolParam(required = false, description = "物业 id,正整数") Long propertyId, + @ToolParam(required = false, description = "代理商 id,正整数") Long agentId, + @ToolParam(required = false, description = "设备 SN,模糊匹配") String snCode, + @ToolParam(required = false, description = "设备类型:0=厕纸机/出纸机,1=擦手纸机") Integer deviceType, + @ToolParam(required = false, description = "故障类型:0=温度异常,1=电压异常,2=电流异常,3=充电次数异常") Integer faultType, + @ToolParam(required = false, description = "故障状态:0=未恢复,1=已恢复") Integer faultState, + @ToolParam(required = false, description = "故障日期起始,yyyy-MM-dd,包含当天") String faultDateFrom, + @ToolParam(required = false, description = "故障日期结束,yyyy-MM-dd,包含当天") String faultDateTo, + @ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page, + @ToolParam(required = false, description = "每页条数,默认 20,上限 100") Integer rows) { + validatePositive(districtId, "districtId"); + validatePositive(propertyId, "propertyId"); + validatePositive(agentId, "agentId"); + validateEnum(deviceType, "deviceType", 0, 1); + validateEnum(faultType, "faultType", 0, 1, 2, 3); + validateEnum(faultState, "faultState", 0, 1); + LocalDate from = parseDate(faultDateFrom, "faultDateFrom"); + LocalDate to = parseDate(faultDateTo, "faultDateTo"); + if (from != null && to != null && from.isAfter(to)) { + throw new IllegalArgumentException("faultDateFrom 不能晚于 faultDateTo"); + } + + int pageNo = page == null || page < 1 ? 1 : page; + boolean truncated = rows != null && rows > MAX_ROWS; + int pageSize = rows == null || rows < 1 ? DEFAULT_ROWS : Math.min(rows, MAX_ROWS); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(BatteryFaultLog::getSoftDelete, 0) + .eq(districtId != null, BatteryFaultLog::getDistrictId, districtId) + .eq(propertyId != null, BatteryFaultLog::getPropertyId, propertyId) + .eq(agentId != null, BatteryFaultLog::getAgentId, agentId) + .eq(deviceType != null, BatteryFaultLog::getDeviceType, deviceType) + .eq(faultType != null, BatteryFaultLog::getFaultType, faultType) + .eq(faultState != null, BatteryFaultLog::getFaultState, faultState); + if (StringUtils.hasText(snCode)) { + wrapper.like(BatteryFaultLog::getSnCode, snCode.trim()); + } + if (from != null) { + wrapper.ge(BatteryFaultLog::getFaultStartTime, + Date.from(from.atStartOfDay(BUSINESS_ZONE).toInstant())); + } + if (to != null) { + wrapper.lt(BatteryFaultLog::getFaultStartTime, + Date.from(to.plusDays(1).atStartOfDay(BUSINESS_ZONE).toInstant())); + } + + User user = AuthContextHolder.get().getUser(); + permissionService.applyBatteryFaultLogRowFilter(wrapper, user); + Long total = mapper.selectCount(wrapper); + wrapper.orderByDesc(BatteryFaultLog::getFaultStartTime) + .orderByDesc(BatteryFaultLog::getId) + .last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize); + List> records = mapper.selectList(wrapper).stream() + .map(this::record).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("total", total); + result.put("page", pageNo); + result.put("rows", pageSize); + if (truncated) { + result.put("notice", "rows 超过上限 100,已截断为 100"); + } + result.put("records", records); + return result; + } + + private Map record(BatteryFaultLog log) { + Map row = new LinkedHashMap<>(); + row.put("id", log.getId()); + row.put("snCode", log.getSnCode()); + row.put("deviceType", log.getDeviceType()); + row.put("deviceTypeText", text(log.getDeviceType(), "厕纸机", "擦手纸机")); + row.put("faultType", log.getFaultType()); + row.put("faultTypeText", text(log.getFaultType(), "温度异常", "电压异常", "电流异常", "充电次数异常")); + row.put("faultState", log.getFaultState()); + row.put("faultStateText", text(log.getFaultState(), "未恢复", "已恢复")); + row.put("peakValue", log.getPeakValue()); + row.put("standardValue", log.getStandardValue()); + row.put("faultStartTime", format(log.getFaultStartTime())); + row.put("faultAddTime", format(log.getFaultAddTime())); + row.put("faultRecoveryTime", format(log.getFaultRecoveryTime())); + row.put("installPosition", log.getInstallPosition()); + row.put("agentId", log.getAgentId()); + row.put("agentName", log.getAgentName()); + row.put("propertyId", log.getPropertyId()); + row.put("propertyName", log.getPropertyName()); + row.put("districtId", log.getDistrictId()); + row.put("districtName", log.getDistrictName()); + return row; + } + + private static LocalDate parseDate(String value, String name) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (DateTimeParseException ex) { + throw new IllegalArgumentException(name + " 必须为 yyyy-MM-dd 格式"); + } + } + + private static String format(Date value) { + return value == null ? null : TIME_FORMATTER.format(value.toInstant()); + } + + private static String text(Integer value, String... labels) { + return value != null && value >= 0 && value < labels.length ? labels[value] : null; + } + + private static void validatePositive(Long value, String name) { + if (value != null && value <= 0) { + throw new IllegalArgumentException(name + " 必须为正整数"); + } + } + + private static void validateEnum(Integer value, String name, Integer... allowed) { + if (value != null && !Arrays.asList(allowed).contains(value)) { + throw new IllegalArgumentException(name + " 不在允许范围内"); + } + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java index af8c039..6401103 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import me.iiv.agent.entity.m0.Agent; import me.iiv.agent.entity.m0.AgentManageDistrictAuth; import me.iiv.agent.entity.m0.Authority; +import me.iiv.agent.entity.m0.BatteryFaultLog; import me.iiv.agent.entity.m0.DistrictDistrictStrategy; import me.iiv.agent.entity.m0.HardwareView; import me.iiv.agent.entity.m0.Properyt; @@ -833,6 +834,24 @@ public class PermissionService { } } + /** 电池故障日志行权限:与出纸机列表复用同一代理/物业可见范围。 */ + public void applyBatteryFaultLogRowFilter(LambdaQueryWrapper wrapper, User user) { + RowScope scope = resolveRowScope(user); + if (scope.headquarters) { + return; + } + if (scope.salesPropertyIds != null) { + wrapper.in(BatteryFaultLog::getPropertyId, scope.salesPropertyIds); + return; + } + if (!scope.managedPropertyIds.isEmpty()) { + wrapper.and(w -> w.in(BatteryFaultLog::getAgentId, scope.ids) + .or().in(BatteryFaultLog::getPropertyId, scope.managedPropertyIds)); + } else { + wrapper.in(BatteryFaultLog::getAgentId, scope.ids); + } + } + /** * 合同(m2 contract_new)行权限:与站点/出纸机同一 getByIsAgent 变体 * (propertyFlag=true),agent 维度落到 contract_agent_id(合同所属公司, diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/BatteryFaultToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/BatteryFaultToolsTest.java new file mode 100644 index 0000000..7ea9f4d --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/BatteryFaultToolsTest.java @@ -0,0 +1,137 @@ +package me.iiv.agent.mcp.tool; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import me.iiv.agent.entity.m0.BatteryFaultLog; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.mapper.m0.BatteryFaultLogMapper; +import me.iiv.agent.permission.PermissionService; +import me.iiv.agent.security.AuthContext; +import me.iiv.agent.security.AuthContextHolder; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BatteryFaultToolsTest { + + @Mock + BatteryFaultLogMapper mapper; + @Mock + PermissionService permissionService; + + BatteryFaultTools tools; + + @BeforeEach + void setUp() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), + BatteryFaultLog.class); + tools = new BatteryFaultTools(mapper, permissionService); + User user = new User(); + user.setId(7L); + AuthContextHolder.set(new AuthContext(null, user)); + } + + @AfterEach + void tearDown() { + AuthContextHolder.clear(); + } + + @Test + @SuppressWarnings("unchecked") + void listBuildsFiltersMapsFieldsAndTruncatesRows() { + BatteryFaultLog log = new BatteryFaultLog(); + log.setId(9L); + log.setSnCode("BAT001"); + log.setDeviceType(0); + log.setFaultType(3); + log.setFaultState(1); + log.setPeakValue(3500); + log.setStandardValue(3700); + log.setFaultStartTime(Date.from(Instant.parse("2026-09-21T02:15:00Z"))); + log.setFaultAddTime(Date.from(Instant.parse("2026-09-21T02:20:00Z"))); + log.setAgentId(1L); + log.setAgentName("代理商"); + log.setPropertyId(2L); + log.setPropertyName("物业"); + log.setDistrictId(3L); + log.setDistrictName("站点"); + when(mapper.selectCount(any())).thenReturn(1L); + when(mapper.selectList(any())).thenReturn(List.of(log)); + + Map result = tools.batteryFaultListQuery( + 3L, 2L, 1L, "BAT", 0, 3, 1, + "2026-09-20", "2026-09-21", 1, 500); + + assertEquals(100, result.get("rows")); + assertEquals("rows 超过上限 100,已截断为 100", result.get("notice")); + Map record = ((List>) result.get("records")).get(0); + assertEquals("厕纸机", record.get("deviceTypeText")); + assertEquals("充电次数异常", record.get("faultTypeText")); + assertEquals("已恢复", record.get("faultStateText")); + assertEquals("2026-09-21 10:15:00", record.get("faultStartTime")); + assertEquals(3500, record.get("peakValue")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(mapper).selectList(captor.capture()); + String sql = captor.getValue().getSqlSegment(); + assertTrue(sql.contains("soft_delete =")); + assertTrue(sql.contains("district_id =")); + assertTrue(sql.contains("property_id =")); + assertTrue(sql.contains("agent_id =")); + assertTrue(sql.contains("sn_code LIKE")); + assertTrue(sql.contains("fault_start_time >=")); + assertTrue(sql.contains("fault_start_time <")); + assertTrue(sql.contains("ORDER BY fault_start_time DESC,id DESC")); + verify(permissionService).applyBatteryFaultLogRowFilter(any(), any()); + } + + @Test + void rejectsInvalidIdsEnumsAndDatesBeforeQuerying() { + assertThrows(IllegalArgumentException.class, () -> tools.batteryFaultListQuery( + 0L, null, null, null, null, null, null, null, null, 1, 20)); + assertThrows(IllegalArgumentException.class, () -> tools.batteryFaultListQuery( + null, null, null, null, 2, null, null, null, null, 1, 20)); + assertThrows(IllegalArgumentException.class, () -> tools.batteryFaultListQuery( + null, null, null, null, null, 4, null, null, null, 1, 20)); + assertThrows(IllegalArgumentException.class, () -> tools.batteryFaultListQuery( + null, null, null, null, null, null, null, "2026-09-22", "2026-09-21", 1, 20)); + assertThrows(IllegalArgumentException.class, () -> tools.batteryFaultListQuery( + null, null, null, null, null, null, null, "2026/09/21", null, 1, 20)); + verifyNoInteractions(mapper); + } + + @Test + void springAiPublishesReviewedSchema() { + var callbacks = org.springframework.ai.tool.method.MethodToolCallbackProvider.builder() + .toolObjects(tools).build().getToolCallbacks(); + Map schemas = Arrays.stream(callbacks).collect(Collectors.toMap( + callback -> callback.getToolDefinition().name(), + callback -> callback.getToolDefinition().inputSchema())); + + assertEquals(1, schemas.size()); + assertTrue(schemas.containsKey("battery.fault.list.query")); + assertTrue(schemas.get("battery.fault.list.query").contains("faultDateFrom")); + } +} diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceBatteryFaultRowFilterTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceBatteryFaultRowFilterTest.java new file mode 100644 index 0000000..68404af --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceBatteryFaultRowFilterTest.java @@ -0,0 +1,99 @@ +package me.iiv.agent.permission; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import me.iiv.agent.entity.m0.Agent; +import me.iiv.agent.entity.m0.AgentManageDistrictAuth; +import me.iiv.agent.entity.m0.Authority; +import me.iiv.agent.entity.m0.BatteryFaultLog; +import me.iiv.agent.entity.m0.Properyt; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.WarehouseAgentAuth; +import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper; +import me.iiv.agent.mapper.m0.AgentMapper; +import me.iiv.agent.mapper.m0.AuthorityMapper; +import me.iiv.agent.mapper.m0.ProperytMapper; +import me.iiv.agent.mapper.m2.WarehouseAgentAuthMapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PermissionServiceBatteryFaultRowFilterTest { + + @Mock AuthorityMapper authorityMapper; + @Mock AgentMapper agentMapper; + @Mock WarehouseAgentAuthMapper warehouseAgentAuthMapper; + @Mock AgentManageDistrictAuthMapper agentManageDistrictAuthMapper; + @Mock ProperytMapper properytMapper; + @Mock StringRedisTemplate redisTemplate; + + PermissionService service; + + @BeforeEach + void setUp() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), + BatteryFaultLog.class); + service = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper, + agentManageDistrictAuthMapper, properytMapper, redisTemplate); + } + + @Test + void headquartersAddsNoBoundary() { + when(authorityMapper.selectById(1L)).thenReturn(authority(0)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyBatteryFaultLogRowFilter(wrapper, user(1L)); + + assertTrue(wrapper.getSqlSegment().isBlank()); + } + + @Test + void agentUsesAgentAndManagedPropertyScope() { + when(authorityMapper.selectById(1L)).thenReturn(authority(1)); + Agent subordinate = new Agent(); + subordinate.setId(2L); + when(agentMapper.selectList(any())).thenReturn(List.of(subordinate)); + when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of()); + AgentManageDistrictAuth auth = new AgentManageDistrictAuth(); + auth.setPropertyId(44L); + when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(auth)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyBatteryFaultLogRowFilter(wrapper, user(1L)); + + String sql = wrapper.getSqlSegment(); + assertTrue(sql.contains("agent_id IN")); + assertTrue(sql.contains("property_id IN")); + assertTrue(sql.contains("OR")); + assertFalse(wrapper.getParamNameValuePairs().isEmpty()); + } + + private static Authority authority(int isAgent) { + Authority authority = new Authority(); + authority.setId(1L); + authority.setIsagent(isAgent); + authority.setSoftDelete(0); + return authority; + } + + private static User user(Long agentId) { + User user = new User(); + user.setId(7L); + user.setAuthorityId(1L); + user.setAgentId(agentId); + return user; + } +} diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceServicePrincipalTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceServicePrincipalTest.java index 3c6fdc4..7068250 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceServicePrincipalTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceServicePrincipalTest.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import me.iiv.agent.entity.m0.Agent; +import me.iiv.agent.entity.m0.BatteryFaultLog; import me.iiv.agent.entity.m0.HardwareView; import me.iiv.agent.entity.m0.Properyt; import me.iiv.agent.entity.svc.AgentApiKey; @@ -59,6 +60,8 @@ class PermissionServiceServicePrincipalTest { Properyt.class); TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Agent.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), + BatteryFaultLog.class); permissionService = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper, agentManageDistrictAuthMapper, properytMapper, redisTemplate); } @@ -86,6 +89,17 @@ class PermissionServiceServicePrincipalTest { verifyNoInteractions(authorityMapper); } + @Test + void servicePrincipalBatteryFaultFilterAddsNoBoundary() { + setServiceContext(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + permissionService.applyBatteryFaultLogRowFilter(wrapper, null); + + assertTrue(wrapper.getSqlSegment().isBlank(), "tenant_service 按总部视角全量可见"); + verifyNoInteractions(authorityMapper); + } + @Test void servicePrincipalVisibleAgentIdsReturnsNull() { setServiceContext();