[REQ-20260921-0006] 新增电池故障列表查询 MCP 工具 #27
@@ -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` 字段提示。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 测试用于确认工具注册、权限和既有功能无回归。
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<BatteryFaultLog> {
|
||||
}
|
||||
@@ -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<String, Object> 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<BatteryFaultLog> wrapper = new LambdaQueryWrapper<BatteryFaultLog>()
|
||||
.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<Map<String, Object>> records = mapper.selectList(wrapper).stream()
|
||||
.map(this::record).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> 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<String, Object> record(BatteryFaultLog log) {
|
||||
Map<String, Object> 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 + " 不在允许范围内");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BatteryFaultLog> 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(合同所属公司,
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> record = ((List<Map<String, Object>>) 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<LambdaQueryWrapper<BatteryFaultLog>> 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<String, String> 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"));
|
||||
}
|
||||
}
|
||||
+99
@@ -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<BatteryFaultLog> 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<BatteryFaultLog> 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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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<BatteryFaultLog> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyBatteryFaultLogRowFilter(wrapper, null);
|
||||
|
||||
assertTrue(wrapper.getSqlSegment().isBlank(), "tenant_service 按总部视角全量可见");
|
||||
verifyNoInteractions(authorityMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servicePrincipalVisibleAgentIdsReturnsNull() {
|
||||
setServiceContext();
|
||||
|
||||
Reference in New Issue
Block a user