feat(mcp): 新增出纸机查询工具 hardware.list/detail.query (REQ-20260910-0012)

This commit is contained in:
xiaoqu
2026-09-10 15:27:14 +08:00
parent 812c8e87f4
commit ff452d89d8
10 changed files with 1454 additions and 12 deletions
@@ -279,6 +279,62 @@
---
## 10. hardware.list.query
分页查询出纸机列表,对应老系统 `POST /hardware/listByPage`(HardwareViewController)。
- 分类:hardware | 类型:读
- 数据来源:m0 库 `hardware_view` 视图,固定 `soft_delete = 0`、`id DESC`
- 行级权限(`PermissionService.applyHardwareRowFilter`,与站点同一 `getByIsAgent(propertyFlag=true, className="property_id")` 变体):
- 总部(isagent=0):全部;
- 代理商(isagent=1):`agent_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
- 富化(修正老接口 N+1,按页批量):`agentName`/`propertyName`/`districtName` 走 Redis key `name:Agent:{id}` / `name:Properyt:{id}` / `name:District:{id}` multiGet;断纸状态 `piStatus` 批量读 `Assignment:{snCode}:29`(无 key 返回 0);本页非空 `imsi` 一次性 IN 查 `networkcard` 表。返回 `hardware_view` 全字段,不做字段级权限裁剪。
**入参**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| snCode | string | 否 | sn码,模糊匹配 |
| deviceType | number | 否 | 设备类型:0=厕纸机 1=擦手纸机 |
| status | number | 否 | 设备状态:1=在线 2=故障 3=离线 |
| online | number | 否 | 上下线状态:1=上线 2=下线 |
| lockMachineState | number | 否 | 锁机状态:0=未锁机 1=已锁机 |
| agentId | number | 否 | 代理商 id,正整数,精确匹配 |
| propertyId | number | 否 | 物业 id,正整数,精确匹配 |
| districtId | number | 否 | 站点 id,正整数,精确匹配 |
| buildingId | number | 否 | 楼 id,正整数,精确匹配 |
| floor | string | 否 | 楼层,精确匹配 |
| imsi | string | 否 | imsi码,精确匹配 |
| page | number | 否 | 页码,从 1 开始,默认 1 |
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
**出参 records 字段**:`hardware_view` 全字段(snCode、deviceType、status/online/lockMachineState 状态四元组、sysversion/firmwareVersion/voiceVersion/advertiseVersion、imsi、电量/纸量/电池族、各功能开关、memo 等,时间格式 `yyyy-MM-dd HH:mm:ss`)+ 富化字段 `agentName`/`propertyName`/`districtName`/`piStatus` + 物联网卡字段 `factoryType`/`carrier`/`dataBalance`/`dataUsage`/`iccid`/`msisdn`(按 imsi 匹配,无卡为 null)。
---
## 11. hardware.detail.query
按 snCode 或 id 查询出纸机详情(二选一必填),行级权限与 `hardware.list.query` 一致。
- 分类:hardware | 类型:读
- 数据来源:m0 库 `hardware_view` + `district_district_strategy`(站点信息)+ `networkcard`(物联网卡)
- 设备不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露设备是否存在)。
**入参**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| snCode | string | 条件必填 | sn码,精确匹配;与 id 二选一 |
| id | number | 条件必填 | 设备 id,正整数,精确匹配;与 snCode 二选一 |
**出参**:
- `record`:hardware_view 全字段 + 与列表同一套富化(agentName/propertyName/districtName、piStatus、物联网卡 factoryType/carrier/dataBalance/dataUsage/iccid/msisdn)。
- `record.site`(关联站点,district 表现有字段):`districtId`、`districtName`、`province`、`city`、`countyseat`、`address`、`contact`、`phone`、`buildingId`、`floor`。注意 district 表无经纬度和楼名字段,楼座/楼层取 hardware_view。
---
## 附录:在线查看工具清单
```bash
+11
View File
@@ -21,6 +21,17 @@ SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query', 'mall.order.list.query', 'mall.order.detail.query', 'mall.order.logistics.query')
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
-- 作用域:hardware(出纸机查询)
INSERT INTO agent_scope (name, code, memo, status, create_id)
SELECT '出纸机', 'hardware', 'dev 种子:出纸机查询作用域', 1, 0
WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'hardware');
-- 作用域-工具:hardware.list.query / hardware.detail.query
INSERT INTO scope_tool (scope_id, tool_id, create_id)
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
WHERE s.code = 'hardware' AND t.name IN ('hardware.list.query', 'hardware.detail.query')
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
COMMIT;
-- 校验
@@ -0,0 +1,295 @@
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;
/**
* hardware_view 视图(m0 库),字段 1:1 照搬老项目
* xiaoqu-publicbeanandmap beans/b0/HardwareView.java。
*/
@Getter
@Setter
@ToString
@TableName("hardware_view")
public class HardwareView implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 创建id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态 */
private Integer softDelete;
/** 设备编号 */
private String no;
/** 位置信息 */
private String address;
/** 位置id */
private Long positionId;
/** 位置信息 */
private String position;
/** sim卡号 */
private String simNo;
/** sn码 */
private String snCode;
/** 状态1:在线;2:故障;3:离线 */
private Integer status;
/** 设备随机码 */
private String checkCode;
/** 固件版本 */
private String firmwareVersion;
/** 厕所id */
private Long toiletId;
/** 楼id */
private Long buildingId;
/** 楼层 */
private String floor;
/** 区域id */
private Long districtId;
/** 物业id */
private Long propertyId;
/** 代理id */
private Long agentId;
/** 设备型号 */
private String version;
/** 备注 */
private String memo;
/** 保洁id */
private Long userId;
/** 电力支持 */
private Integer powerSupply;
/** 感应抽纸长度 */
private Integer senseLength;
/** 扫码抽纸长度 */
private Integer scanLength;
/** 出纸模式 */
private Integer paperType;
/** 纸规格 */
private String paperNorms;
/** 感应出纸次数 */
private Integer senseCount;
/** 扫码出纸次数 */
private Integer scanCount;
/** 剩余次数 */
private Integer paperSurplus;
/** 出纸计数 */
private Long paperCount;
/** sn码 */
private String temSnCode;
/** rfid */
private String rfid;
/** 硬件名称 */
private String name;
/** 安装场景0:厕位内:1:厕位外:2其它 */
private Integer installScenes;
/** 安装方式0:免打孔 1:打孔 */
private Integer installMode;
/** 供电方式 0:电源 1:电池 */
private Integer poweredType;
/** 设备上下线状态 */
private Integer online;
/** 蓝牙名称 */
private String bluetoothName;
/** imsi码 */
private String imsi;
/** 蓝牙地址 */
private String bluetoothMac;
/** 上线时间 */
private Date onlineTime;
/** 芯片号 */
private String chip;
/** 主板号 */
private String motherboard;
private String sysversion;
/** 感应出纸次数 */
private Integer paperState;
/** 扫码出纸次数 */
private Integer powerState;
private Integer positionState;
/** 剩余次数 */
private Integer useState;
/** 质保期 */
private Integer warrantyPeriod;
/** 运行模式 */
private Integer runlMode;
/** 锁状态 */
private Integer lockState;
/** 设备类型 */
private Integer deviceType;
/** 清理用纸卷计数 */
private Integer cleanRfidCount;
/** 工程模式0:关;1:开 */
private Integer projectModel;
/** 出纸模式0:关闭;1:开启 */
private Integer paperOutState;
/** 机芯更换时间 */
private Date machineChipTime;
/** 机芯编号 */
private String machineChipCode;
/** 当前机芯编号 */
private String currentChip;
/** 锁定机器状态0:未锁定;1:已锁定 */
private Integer lockMachineState;
/** 语音版本 */
private String voiceVersion;
/** 校准标识0:否;1:是 */
private Integer calibratFlag;
/** 满卷标识0:否;1:是 */
private Integer fullRollFlag;
/** 中校准锁定开关 */
private Integer middleFlag;
/** 低校准锁定开关 */
private Integer lowerFlag;
/** 纸校准锁定开关 */
private Integer paperLockFlag;
/** 低故障开关(0:故障;1:正常) */
private Integer lowerFaultFlag;
/** 中故障开关(0:故障;1:正常) */
private Integer middleFaultFlag;
/** 当前锁定状态出纸次数 */
private Integer paperLockCount;
/** 保修截止日期 */
private Date warrantyExpirationTime;
/** 首次上线时间 */
private Date firstOnTime;
/** 广告机版本 */
private String advertiseVersion;
/** 非法纸禁出状态(0:禁出;1:未禁出) */
private Integer illegalPaperDisableState;
/** 是否绑定NFC 0:否;1:是 */
private Integer nfcFlag;
/** nfcId */
private String nfcId;
/** 定时语音播放0:关;1:开 */
private Integer timerVoiceFlag;
/** 设定时长(分) */
private Integer timerVoiceTiming;
/** 定时出纸0:关;1:开 */
private Integer timerPaperoutFlag;
/** 设定时长(分) */
private Integer timerPaperoutTiming;
/** 定时出纸长度 */
private Integer timerPaperoutLength;
/** 电池电量异常标记0:否;1:是 */
private Integer batteryFaultFlag;
/** 电池温度 */
private Integer batteryTemp;
/** 网络信号 */
private Integer networkSignal;
/** 充电循环次数 */
private Integer batteryCircle;
/** 生产备注 */
private String prdMemo;
/** 备注 */
private String runMemo;
/** 电流 */
private Integer current;
/** 电压 */
private Integer voltage;
/** 计租标记(0否1是) */
private Integer rentFlag;
}
@@ -0,0 +1,118 @@
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.math.BigDecimal;
import java.util.Date;
/**
* networkcard 联网卡列表(m0 库),字段照搬老项目
* xiaoqu-publicbeanandmap beans/b0/Networkcard.java。
*/
@Getter
@Setter
@ToString
@TableName("networkcard")
public class Networkcard implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 创建id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态 */
private Integer softDelete;
/** sim卡号 */
private String simNo;
/** 设备编号 */
private String sncode;
/** 绑定状态 */
private Integer state;
/** msisdn */
private String msisdn;
/** iccid */
private String iccid;
/** imsi码 */
private String imsi;
/** 本月流量 */
private BigDecimal dataUsage;
/** 运营商 */
private String carrier;
/** 套餐大小 */
private Integer dataPlan;
/** 卡状态 */
private Integer accountStatus;
/** 计费结束日期 */
private Integer expiryDate;
/** 激活状态 0,1 */
private Integer active;
/** 激活日期 */
private Integer activeDate;
/** 剩余流量,单位M */
private BigDecimal dataBalance;
/** 出库日期 */
private Integer outboundDate;
/** 区域/站id */
private Integer districtId;
/** 所属物业 */
private Long propertyId;
/** 代理商id */
private Long agentId;
/** 楼id */
private Long buildingId;
/** 位置id */
private Long positionId;
/** 厕所id */
private Long toiletId;
/** 厂商类型0:厂商1;1:厂商2.... */
private Integer factoryType;
/** 卡状态 */
private String status;
/** 所属机构名称 */
private String orgName;
/** 卡当前套餐名称 */
private String iratePlanName;
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m0;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m0.HardwareView;
public interface HardwareViewMapper extends BaseMapper<HardwareView> {
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m0;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m0.Networkcard;
public interface NetworkcardMapper extends BaseMapper<Networkcard> {
}
@@ -0,0 +1,413 @@
package me.iiv.agent.mcp.tool;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
import me.iiv.agent.entity.m0.HardwareView;
import me.iiv.agent.entity.m0.Networkcard;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
import me.iiv.agent.mapper.m0.HardwareViewMapper;
import me.iiv.agent.mapper.m0.NetworkcardMapper;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 出纸机查询工具,仿老系统 HardwareViewController /listByPage。
* 富化修正老接口 N+1:名称/断纸状态走 Redis multiGet,物联网卡按页 IN 批量查。
*/
@Component
public class HardwareTools implements ToolObject {
private static final int DEFAULT_ROWS = 20;
private static final int MAX_ROWS = 100;
/** 老接口 Redis 字典 key(HardwareViewController /listByPage) */
private static final String NAME_AGENT_PREFIX = "name:Agent:";
private static final String NAME_PROPERTY_PREFIX = "name:Properyt:";
private static final String NAME_DISTRICT_PREFIX = "name:District:";
/** 断纸(缺纸工单)状态 key,老接口 :29 为纸状态业务类型;无 key 视为 0 */
private static final String ASSIGNMENT_KEY_TEMPLATE = "Assignment:%s:29";
final HardwareViewMapper hardwareViewMapper;
final NetworkcardMapper networkcardMapper;
final DistrictDistrictStrategyMapper districtMapper;
final PermissionService permissionService;
final StringRedisTemplate redisTemplate;
public HardwareTools(HardwareViewMapper hardwareViewMapper, NetworkcardMapper networkcardMapper,
DistrictDistrictStrategyMapper districtMapper,
PermissionService permissionService, StringRedisTemplate redisTemplate) {
this.hardwareViewMapper = hardwareViewMapper;
this.networkcardMapper = networkcardMapper;
this.districtMapper = districtMapper;
this.permissionService = permissionService;
this.redisTemplate = redisTemplate;
}
@XqTool(category = "hardware", kind = "read")
@Tool(name = "hardware.list.query", description = "分页查询当前用户可见的出纸机列表,对应老系统 /hardware/listByPage。"
+ "支持 snCode 模糊匹配,deviceType、status、online、lockMachineState、agentId、propertyId、districtId、buildingId、floor、imsi 精确匹配。"
+ "page 默认 1,rows 默认 20、上限 100,固定按 id 倒序。"
+ "agentName/propertyName/districtName 与断纸状态 piStatus 使用 Redis 批量富化,物联网卡信息按页批量查询。"
+ "返回 hardware_view 全字段,不做字段级权限裁剪。")
public Map<String, Object> hardwareListQuery(
@ToolParam(required = false, description = "sn码,模糊匹配") String snCode,
@ToolParam(required = false, description = "设备类型:0=厕纸机 1=擦手纸机") Integer deviceType,
@ToolParam(required = false, description = "设备状态:1=在线 2=故障 3=离线") Integer status,
@ToolParam(required = false, description = "上下线状态:1=上线 2=下线") Integer online,
@ToolParam(required = false, description = "锁机状态:0=未锁机 1=已锁机") Integer lockMachineState,
@ToolParam(required = false, description = "代理商 id,正整数,精确匹配") Long agentId,
@ToolParam(required = false, description = "物业 id,正整数,精确匹配") Long propertyId,
@ToolParam(required = false, description = "站点 id,正整数,精确匹配") Long districtId,
@ToolParam(required = false, description = "楼 id,正整数,精确匹配") Long buildingId,
@ToolParam(required = false, description = "楼层,精确匹配") String floor,
@ToolParam(required = false, description = "imsi码,精确匹配") String imsi,
@ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page,
@ToolParam(required = false, description = "每页条数,默认 20,上限 100") Integer rows) {
validateEnum(deviceType, "deviceType", 0, 1);
validateEnum(status, "status", 1, 2, 3);
validateEnum(online, "online", 1, 2);
validateEnum(lockMachineState, "lockMachineState", 0, 1);
validatePositive(agentId, "agentId");
validatePositive(propertyId, "propertyId");
validatePositive(districtId, "districtId");
validatePositive(buildingId, "buildingId");
User user = AuthContextHolder.get().getUser();
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<HardwareView> wrapper = new LambdaQueryWrapper<HardwareView>()
.eq(HardwareView::getSoftDelete, 0);
if (StringUtils.hasText(snCode)) {
wrapper.like(HardwareView::getSnCode, snCode);
}
if (deviceType != null) {
wrapper.eq(HardwareView::getDeviceType, deviceType);
}
if (status != null) {
wrapper.eq(HardwareView::getStatus, status);
}
if (online != null) {
wrapper.eq(HardwareView::getOnline, online);
}
if (lockMachineState != null) {
wrapper.eq(HardwareView::getLockMachineState, lockMachineState);
}
if (agentId != null) {
wrapper.eq(HardwareView::getAgentId, agentId);
}
if (propertyId != null) {
wrapper.eq(HardwareView::getPropertyId, propertyId);
}
if (districtId != null) {
wrapper.eq(HardwareView::getDistrictId, districtId);
}
if (buildingId != null) {
wrapper.eq(HardwareView::getBuildingId, buildingId);
}
if (StringUtils.hasText(floor)) {
wrapper.eq(HardwareView::getFloor, floor);
}
if (StringUtils.hasText(imsi)) {
wrapper.eq(HardwareView::getImsi, imsi);
}
permissionService.applyHardwareRowFilter(wrapper, user);
Long total = hardwareViewMapper.selectCount(wrapper);
wrapper.orderByDesc(HardwareView::getId)
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
List<HardwareView> list = hardwareViewMapper.selectList(wrapper);
List<Map<String, Object>> records = new ArrayList<>();
Enrichment enrichment = enrich(list);
for (HardwareView hardware : list) {
records.add(toRecord(hardware, enrichment));
}
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;
}
@XqTool(category = "hardware", kind = "read")
@Tool(name = "hardware.detail.query", description = "按 snCode 或 id 查询当前用户可见的出纸机详情,二选一必填。"
+ "返回 hardware_view 全字段 + agentName/propertyName/districtName、断纸状态 piStatus、"
+ "物联网卡信息(factoryType/carrier/dataBalance/dataUsage/iccid/msisdn)+ 关联站点信息(districtName、省市区县、详细地址、联系人、楼座/楼层)。"
+ "设备不存在或无权访问时返回 record=null 并在 notice 说明。")
public Map<String, Object> hardwareDetailQuery(
@ToolParam(required = false, description = "sn码,精确匹配;与 id 二选一必填") String snCode,
@ToolParam(required = false, description = "设备 id,正整数,精确匹配;与 snCode 二选一必填") Long id) {
if (!StringUtils.hasText(snCode) && id == null) {
throw new IllegalArgumentException("snCode 与 id 至少填一个");
}
validatePositive(id, "id");
User user = AuthContextHolder.get().getUser();
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<HardwareView>()
.eq(HardwareView::getSoftDelete, 0);
if (StringUtils.hasText(snCode)) {
wrapper.eq(HardwareView::getSnCode, snCode);
} else {
wrapper.eq(HardwareView::getId, id);
}
permissionService.applyHardwareRowFilter(wrapper, user);
HardwareView hardware = hardwareViewMapper.selectOne(wrapper);
Map<String, Object> result = new LinkedHashMap<>();
if (hardware == null) {
result.put("record", null);
result.put("notice", "设备不存在或无权访问");
return result;
}
Map<String, Object> record = toRecord(hardware, enrich(List.of(hardware)));
record.put("site", siteInfo(hardware));
result.put("record", record);
return result;
}
/** 站点信息:district 表现有字段(无经纬度/楼名字段);楼座/楼层取 hardware_view。 */
private Map<String, Object> siteInfo(HardwareView hardware) {
Map<String, Object> site = new LinkedHashMap<>();
DistrictDistrictStrategy district = hardware.getDistrictId() == null
? null : districtMapper.selectById(hardware.getDistrictId());
site.put("districtId", hardware.getDistrictId());
site.put("districtName", district == null ? null : district.getDistrictName());
site.put("province", district == null ? null : district.getProvince());
site.put("city", district == null ? null : district.getCity());
site.put("countyseat", district == null ? null : district.getCountyseat());
site.put("address", district == null ? null : district.getAddress());
site.put("contact", district == null ? null : district.getContact());
site.put("phone", district == null ? null : district.getPhone());
site.put("buildingId", hardware.getBuildingId());
site.put("floor", hardware.getFloor());
return site;
}
/** 批量富化上下文:Redis 名称/断纸状态 + 本页物联网卡,一次查询替代老接口 N+1。 */
private Enrichment enrich(List<HardwareView> list) {
Enrichment enrichment = new Enrichment();
if (list.isEmpty()) {
return enrichment;
}
enrichment.agentNames = redisNames(list, HardwareView::getAgentId, NAME_AGENT_PREFIX);
enrichment.propertyNames = redisNames(list, HardwareView::getPropertyId, NAME_PROPERTY_PREFIX);
enrichment.districtNames = redisNames(list, HardwareView::getDistrictId, NAME_DISTRICT_PREFIX);
enrichment.piStatus = piStatus(list);
enrichment.networkcards = networkcards(list);
return enrichment;
}
private Map<Long, String> redisNames(List<HardwareView> list, Function<HardwareView, Long> idGetter,
String prefix) {
List<Long> ids = list.stream().map(idGetter)
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (ids.isEmpty()) {
return Map.of();
}
List<String> keys = ids.stream().map(id -> prefix + id).collect(Collectors.toList());
List<String> values = redisTemplate.opsForValue().multiGet(keys);
Map<Long, String> names = new LinkedHashMap<>();
for (int i = 0; i < ids.size(); i++) {
names.put(ids.get(i), values == null || i >= values.size() ? null : values.get(i));
}
return names;
}
private Map<String, Integer> piStatus(List<HardwareView> list) {
List<String> snCodes = list.stream().map(HardwareView::getSnCode)
.filter(StringUtils::hasText).distinct().collect(Collectors.toList());
if (snCodes.isEmpty()) {
return Map.of();
}
List<String> keys = snCodes.stream()
.map(sn -> String.format(ASSIGNMENT_KEY_TEMPLATE, sn)).collect(Collectors.toList());
List<String> values = redisTemplate.opsForValue().multiGet(keys);
Map<String, Integer> status = new LinkedHashMap<>();
for (int i = 0; i < snCodes.size(); i++) {
String value = values == null || i >= values.size() ? null : values.get(i);
status.put(snCodes.get(i), value == null ? 0 : Integer.parseInt(value));
}
return status;
}
/** 本页非空 imsi 一次性 IN 查 networkcard(老接口按条 limit 1,取首条语义保留)。 */
private Map<String, Networkcard> networkcards(List<HardwareView> list) {
List<String> imsIs = list.stream().map(HardwareView::getImsi)
.filter(StringUtils::hasText).distinct().collect(Collectors.toList());
if (imsIs.isEmpty()) {
return Map.of();
}
return networkcardMapper.selectList(new LambdaQueryWrapper<Networkcard>()
.eq(Networkcard::getSoftDelete, 0)
.in(Networkcard::getImsi, imsIs))
.stream().collect(Collectors.toMap(Networkcard::getImsi, card -> card, (a, b) -> a));
}
private Map<String, Object> toRecord(HardwareView h, Enrichment enrichment) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", h.getId());
record.put("no", h.getNo());
record.put("name", h.getName());
record.put("snCode", h.getSnCode());
record.put("temSnCode", h.getTemSnCode());
record.put("checkCode", h.getCheckCode());
record.put("deviceType", h.getDeviceType());
record.put("status", h.getStatus());
record.put("online", h.getOnline());
record.put("lockMachineState", h.getLockMachineState());
record.put("agentId", h.getAgentId());
record.put("agentName", mapGet(enrichment.agentNames, h.getAgentId()));
record.put("propertyId", h.getPropertyId());
record.put("propertyName", mapGet(enrichment.propertyNames, h.getPropertyId()));
record.put("districtId", h.getDistrictId());
record.put("districtName", mapGet(enrichment.districtNames, h.getDistrictId()));
record.put("buildingId", h.getBuildingId());
record.put("floor", h.getFloor());
record.put("toiletId", h.getToiletId());
record.put("positionId", h.getPositionId());
record.put("position", h.getPosition());
record.put("address", h.getAddress());
record.put("version", h.getVersion());
record.put("sysversion", h.getSysversion());
record.put("firmwareVersion", h.getFirmwareVersion());
record.put("voiceVersion", h.getVoiceVersion());
record.put("advertiseVersion", h.getAdvertiseVersion());
record.put("imsi", h.getImsi());
record.put("simNo", h.getSimNo());
Networkcard card = mapGet(enrichment.networkcards, h.getImsi());
record.put("factoryType", card == null ? null : card.getFactoryType());
record.put("carrier", card == null ? null : card.getCarrier());
record.put("dataBalance", card == null ? null : card.getDataBalance());
record.put("dataUsage", card == null ? null : card.getDataUsage());
record.put("iccid", card == null ? null : card.getIccid());
record.put("msisdn", card == null ? null : card.getMsisdn());
record.put("piStatus", h.getSnCode() == null ? 0
: enrichment.piStatus.getOrDefault(h.getSnCode(), 0));
record.put("userId", h.getUserId());
record.put("powerSupply", h.getPowerSupply());
record.put("batteryTemp", h.getBatteryTemp());
record.put("batteryCircle", h.getBatteryCircle());
record.put("batteryFaultFlag", h.getBatteryFaultFlag());
record.put("networkSignal", h.getNetworkSignal());
record.put("current", h.getCurrent());
record.put("voltage", h.getVoltage());
record.put("paperSurplus", h.getPaperSurplus());
record.put("paperCount", h.getPaperCount());
record.put("paperState", h.getPaperState());
record.put("powerState", h.getPowerState());
record.put("positionState", h.getPositionState());
record.put("useState", h.getUseState());
record.put("paperType", h.getPaperType());
record.put("paperNorms", h.getPaperNorms());
record.put("senseLength", h.getSenseLength());
record.put("scanLength", h.getScanLength());
record.put("senseCount", h.getSenseCount());
record.put("scanCount", h.getScanCount());
record.put("rfid", h.getRfid());
record.put("cleanRfidCount", h.getCleanRfidCount());
record.put("installScenes", h.getInstallScenes());
record.put("installMode", h.getInstallMode());
record.put("poweredType", h.getPoweredType());
record.put("bluetoothName", h.getBluetoothName());
record.put("bluetoothMac", h.getBluetoothMac());
record.put("chip", h.getChip());
record.put("motherboard", h.getMotherboard());
record.put("onlineTime", format(h.getOnlineTime()));
record.put("warrantyPeriod", h.getWarrantyPeriod());
record.put("warrantyExpirationTime", format(h.getWarrantyExpirationTime()));
record.put("firstOnTime", format(h.getFirstOnTime()));
record.put("runlMode", h.getRunlMode());
record.put("lockState", h.getLockState());
record.put("projectModel", h.getProjectModel());
record.put("paperOutState", h.getPaperOutState());
record.put("machineChipTime", format(h.getMachineChipTime()));
record.put("machineChipCode", h.getMachineChipCode());
record.put("currentChip", h.getCurrentChip());
record.put("calibratFlag", h.getCalibratFlag());
record.put("fullRollFlag", h.getFullRollFlag());
record.put("middleFlag", h.getMiddleFlag());
record.put("lowerFlag", h.getLowerFlag());
record.put("paperLockFlag", h.getPaperLockFlag());
record.put("paperLockCount", h.getPaperLockCount());
record.put("lowerFaultFlag", h.getLowerFaultFlag());
record.put("middleFaultFlag", h.getMiddleFaultFlag());
record.put("illegalPaperDisableState", h.getIllegalPaperDisableState());
record.put("nfcFlag", h.getNfcFlag());
record.put("nfcId", h.getNfcId());
record.put("timerVoiceFlag", h.getTimerVoiceFlag());
record.put("timerVoiceTiming", h.getTimerVoiceTiming());
record.put("timerPaperoutFlag", h.getTimerPaperoutFlag());
record.put("timerPaperoutTiming", h.getTimerPaperoutTiming());
record.put("timerPaperoutLength", h.getTimerPaperoutLength());
record.put("prdMemo", h.getPrdMemo());
record.put("runMemo", h.getRunMemo());
record.put("rentFlag", h.getRentFlag());
record.put("createId", h.getCreateId());
record.put("createTime", format(h.getCreateTime()));
record.put("modifyTime", format(h.getModifyTime()));
record.put("memo", h.getMemo());
return record;
}
private static final class Enrichment {
Map<Long, String> agentNames = Map.of();
Map<Long, String> propertyNames = Map.of();
Map<Long, String> districtNames = Map.of();
Map<String, Integer> piStatus = Map.of();
Map<String, Networkcard> networkcards = Map.of();
}
private static <K, V> V mapGet(Map<K, V> map, K key) {
return key == null ? null : map.get(key);
}
private static String format(Date value) {
return value == null ? null : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);
}
private static void validatePositive(Long value, String name) {
if (value != null && value < 1) {
throw new IllegalArgumentException(name + " 必须为正整数");
}
}
private static void validateEnum(Integer value, String name, int... allowed) {
if (value == null) {
return;
}
for (int candidate : allowed) {
if (value == candidate) {
return;
}
}
String options = Arrays.stream(allowed).mapToObj(String::valueOf)
.collect(Collectors.joining("、"));
throw new IllegalArgumentException(name + " 仅支持 " + options);
}
}
@@ -5,6 +5,7 @@ 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.DistrictDistrictStrategy;
import me.iiv.agent.entity.m0.HardwareView;
import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
@@ -301,12 +302,28 @@ public class PermissionService {
}
/**
* 站点行权限:1:1 对齐老接口 districtlistByPage 使用的
* getByIsAgent(..., propertyFlag=true, className="property_id")。
* 行权限范围解析结果(老 getByIsAgent propertyFlag=true 变体的公共中间态)。
* headquarters=true 表示 isagent!=1(总部),调用方不加任何过滤。
* salesPropertyIds 非 null 表示销售部门非负责人互斥分支:只按物业 id 集合过滤
* (空集合已归一为 [0],保证查不到数据)。
*/
public void applyDistrictRowFilter(LambdaQueryWrapper<DistrictDistrictStrategy> wrapper, User user) {
private static final class RowScope {
boolean headquarters;
List<Long> ids;
List<Long> salesPropertyIds;
List<Long> managedPropertyIds;
}
/**
* 解析当前用户的行权限范围,district / hardware 等 propertyFlag=true、
* className="property_id" 变体共用(老 WrapperUtils.java:679-764)。
* 返回 headquarters 标记的 scope;调用方各自把结果落到自己的 wrapper 字段上。
*/
private RowScope resolveRowScope(User user) {
RowScope scope = new RowScope();
if (!isAgentAuthority(user.getAuthorityId())) {
return;
scope.headquarters = true;
return scope;
}
if (user.getAgentId() == null) {
throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据");
@@ -315,6 +332,7 @@ public class PermissionService {
List<Long> ids = new ArrayList<>(subordinateAndSelfIds(user));
List<WarehouseAgentAuth> agentlist = managedAuths(user.getId());
ids.addAll(agentlist.stream().map(WarehouseAgentAuth::getManagerAgentId).collect(Collectors.toList()));
scope.ids = ids;
if (isSalesNonLeader(user)) {
List<Long> propertyIds = properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
@@ -332,17 +350,54 @@ public class PermissionService {
.stream().map(Properyt::getId).collect(Collectors.toList()));
}
}
wrapper.in(DistrictDistrictStrategy::getPropertyId,
propertyIds.isEmpty() ? List.of(0L) : propertyIds);
return;
scope.salesPropertyIds = propertyIds.isEmpty() ? List.of(0L) : propertyIds;
return scope;
}
List<Long> managedPropertyIds = managedPropertyIds(ids);
if (!managedPropertyIds.isEmpty()) {
wrapper.and(w -> w.in(DistrictDistrictStrategy::getAgentId, ids)
.or().in(DistrictDistrictStrategy::getPropertyId, managedPropertyIds));
scope.managedPropertyIds = managedPropertyIds(ids);
return scope;
}
/**
* 站点行权限:1:1 对齐老接口 districtlistByPage 使用的
* getByIsAgent(..., propertyFlag=true, className="property_id")。
*/
public void applyDistrictRowFilter(LambdaQueryWrapper<DistrictDistrictStrategy> wrapper, User user) {
RowScope scope = resolveRowScope(user);
if (scope.headquarters) {
return;
}
if (scope.salesPropertyIds != null) {
wrapper.in(DistrictDistrictStrategy::getPropertyId, scope.salesPropertyIds);
return;
}
if (!scope.managedPropertyIds.isEmpty()) {
wrapper.and(w -> w.in(DistrictDistrictStrategy::getAgentId, scope.ids)
.or().in(DistrictDistrictStrategy::getPropertyId, scope.managedPropertyIds));
} else {
wrapper.in(DistrictDistrictStrategy::getAgentId, ids);
wrapper.in(DistrictDistrictStrategy::getAgentId, scope.ids);
}
}
/**
* 出纸机(hardware_view)行权限:与站点同一 getByIsAgent 变体
* (propertyFlag=true, className="property_id",老 HardwareViewController /listByPage),
* 仅落到 HardwareView 的 agent_id / property_id 字段上。
*/
public void applyHardwareRowFilter(LambdaQueryWrapper<HardwareView> wrapper, User user) {
RowScope scope = resolveRowScope(user);
if (scope.headquarters) {
return;
}
if (scope.salesPropertyIds != null) {
wrapper.in(HardwareView::getPropertyId, scope.salesPropertyIds);
return;
}
if (!scope.managedPropertyIds.isEmpty()) {
wrapper.and(w -> w.in(HardwareView::getAgentId, scope.ids)
.or().in(HardwareView::getPropertyId, scope.managedPropertyIds));
} else {
wrapper.in(HardwareView::getAgentId, scope.ids);
}
}
}
@@ -0,0 +1,283 @@
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.DistrictDistrictStrategy;
import me.iiv.agent.entity.m0.HardwareView;
import me.iiv.agent.entity.m0.Networkcard;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
import me.iiv.agent.mapper.m0.HardwareViewMapper;
import me.iiv.agent.mapper.m0.NetworkcardMapper;
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 org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.math.BigDecimal;
import java.util.Arrays;
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.assertNull;
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.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HardwareToolsTest {
@Mock
HardwareViewMapper hardwareViewMapper;
@Mock
NetworkcardMapper networkcardMapper;
@Mock
DistrictDistrictStrategyMapper districtMapper;
@Mock
PermissionService permissionService;
@Mock
StringRedisTemplate redisTemplate;
HardwareTools tools;
User user;
@BeforeEach
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
HardwareView.class);
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
Networkcard.class);
tools = new HardwareTools(hardwareViewMapper, networkcardMapper, districtMapper,
permissionService, redisTemplate);
user = new User();
user.setId(7L);
AuthContextHolder.set(new AuthContext(null, user));
}
@AfterEach
void tearDown() {
AuthContextHolder.clear();
}
@Test
@SuppressWarnings("unchecked")
void listBuildsLegacyFiltersAndTruncatesRows() {
HardwareView hardware = new HardwareView();
hardware.setId(9L);
hardware.setSnCode("D2AB001");
hardware.setAgentId(12L);
hardware.setPropertyId(11L);
hardware.setDistrictId(5L);
hardware.setImsi("460001234567890");
when(hardwareViewMapper.selectCount(any())).thenReturn(1L);
when(hardwareViewMapper.selectList(any())).thenReturn(List.of(hardware));
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
// name:Agent:12 / name:Properyt:11 / name:District:5 / Assignment:D2AB001:29
when(valueOperations.multiGet(anyList())).thenAnswer(invocation -> {
List<String> keys = invocation.getArgument(0);
return keys.stream().map(key -> switch (key) {
case "name:Agent:12" -> "测试代理";
case "name:Properyt:11" -> "测试物业";
case "name:District:5" -> "测试站点";
case "Assignment:D2AB001:29" -> "1";
default -> null;
}).collect(Collectors.toList());
});
Networkcard card = new Networkcard();
card.setImsi("460001234567890");
card.setFactoryType(1);
card.setCarrier("移动");
card.setDataBalance(new BigDecimal("512.5"));
card.setDataUsage(new BigDecimal("128.0"));
card.setIccid("8986001234");
card.setMsisdn("14412345678");
when(networkcardMapper.selectList(any())).thenReturn(List.of(card));
Map<String, Object> result = tools.hardwareListQuery(
"D2AB", 0, 1, 1, 0, 12L, 11L, 5L, 3L, "3F", "460001234567890", 1, 500);
assertEquals(1, result.get("page"));
assertEquals(100, result.get("rows"));
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
// 过滤拼装与老接口一一对应
ArgumentCaptor<LambdaQueryWrapper<HardwareView>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(hardwareViewMapper).selectList(captor.capture());
String sql = captor.getValue().getSqlSegment();
assertTrue(sql.contains("sn_code LIKE"));
assertTrue(sql.contains("device_type ="));
assertTrue(sql.contains("status ="));
assertTrue(sql.contains("online ="));
assertTrue(sql.contains("lock_machine_state ="));
assertTrue(sql.contains("agent_id ="));
assertTrue(sql.contains("property_id ="));
assertTrue(sql.contains("district_id ="));
assertTrue(sql.contains("building_id ="));
assertTrue(sql.contains("floor ="));
assertTrue(sql.contains("imsi ="));
assertTrue(sql.contains("soft_delete ="));
assertTrue(captor.getValue().getSqlSegment().contains("ORDER BY id DESC"));
verify(permissionService).applyHardwareRowFilter(any(), any());
// 富化:名称、断纸状态、物联网卡
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
assertEquals("测试代理", record.get("agentName"));
assertEquals("测试物业", record.get("propertyName"));
assertEquals("测试站点", record.get("districtName"));
assertEquals(1, record.get("piStatus"));
assertEquals(1, record.get("factoryType"));
assertEquals("移动", record.get("carrier"));
assertEquals(new BigDecimal("512.5"), record.get("dataBalance"));
assertEquals(new BigDecimal("128.0"), record.get("dataUsage"));
assertEquals("8986001234", record.get("iccid"));
assertEquals("14412345678", record.get("msisdn"));
}
@Test
@SuppressWarnings("unchecked")
void listDefaultsPiStatusToZeroWhenAssignmentKeyMissing() {
HardwareView hardware = new HardwareView();
hardware.setId(9L);
hardware.setSnCode("D2AB002");
when(hardwareViewMapper.selectCount(any())).thenReturn(1L);
when(hardwareViewMapper.selectList(any())).thenReturn(List.of(hardware));
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.multiGet(anyList())).thenReturn(List.of());
Map<String, Object> result = tools.hardwareListQuery(
null, null, null, null, null, null, null, null, null, null, null, 1, 20);
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
assertEquals(0, record.get("piStatus"));
assertNull(record.get("agentName"));
// 无 imsi 不查物联网卡
verify(networkcardMapper, never()).selectList(any());
}
@Test
void detailRequiresSnCodeOrId() {
assertThrows(IllegalArgumentException.class, () -> tools.hardwareDetailQuery(null, null));
assertThrows(IllegalArgumentException.class, () -> tools.hardwareDetailQuery(" ", 0L));
verifyNoInteractions(hardwareViewMapper);
}
@Test
void detailReturnsNullRecordWithNoticeWhenMissingOrUnauthorized() {
when(hardwareViewMapper.selectOne(any())).thenReturn(null);
Map<String, Object> result = tools.hardwareDetailQuery("D2AB404", null);
assertNull(result.get("record"));
assertEquals("设备不存在或无权访问", result.get("notice"));
verify(permissionService).applyHardwareRowFilter(any(), any());
}
@Test
@SuppressWarnings("unchecked")
void detailBySnCodeReturnsFullRecordWithSiteAndEnrichment() {
HardwareView hardware = new HardwareView();
hardware.setId(9L);
hardware.setSnCode("D2AB001");
hardware.setDeviceType(0);
hardware.setStatus(1);
hardware.setOnline(1);
hardware.setLockMachineState(0);
hardware.setSysversion("v1.2.3");
hardware.setFirmwareVersion("hw-9");
hardware.setImsi("460001234567890");
hardware.setDistrictId(5L);
hardware.setBuildingId(3L);
hardware.setFloor("3F");
when(hardwareViewMapper.selectOne(any())).thenReturn(hardware);
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.multiGet(anyList())).thenReturn(List.of());
when(networkcardMapper.selectList(any())).thenReturn(List.of());
DistrictDistrictStrategy district = new DistrictDistrictStrategy();
district.setId(5L);
district.setDistrictName("测试站点");
district.setProvince("广东省");
district.setCity("深圳市");
district.setCountyseat("南山区");
district.setAddress("科技园");
district.setContact("张三");
district.setPhone("13800000000");
when(districtMapper.selectById(5L)).thenReturn(district);
Map<String, Object> result = tools.hardwareDetailQuery("D2AB001", null);
Map<String, Object> record = (Map<String, Object>) result.get("record");
assertEquals("D2AB001", record.get("snCode"));
assertEquals(0, record.get("deviceType"));
assertEquals(1, record.get("status"));
assertEquals(1, record.get("online"));
assertEquals(0, record.get("lockMachineState"));
assertEquals("v1.2.3", record.get("sysversion"));
assertEquals("hw-9", record.get("firmwareVersion"));
assertEquals(0, record.get("piStatus"));
Map<String, Object> site = (Map<String, Object>) record.get("site");
assertEquals(5L, site.get("districtId"));
assertEquals("测试站点", site.get("districtName"));
assertEquals("广东省", site.get("province"));
assertEquals("深圳市", site.get("city"));
assertEquals("南山区", site.get("countyseat"));
assertEquals("科技园", site.get("address"));
assertEquals("张三", site.get("contact"));
assertEquals("13800000000", site.get("phone"));
assertEquals(3L, site.get("buildingId"));
assertEquals("3F", site.get("floor"));
}
@Test
void listRejectsUnknownEnumsBeforeQuerying() {
assertThrows(IllegalArgumentException.class, () -> tools.hardwareListQuery(
null, 2, null, null, null, null, null, null, null, null, null, 1, 20));
assertThrows(IllegalArgumentException.class, () -> tools.hardwareListQuery(
null, null, 0, null, null, null, null, null, null, null, null, 1, 20));
assertThrows(IllegalArgumentException.class, () -> tools.hardwareListQuery(
null, null, null, 3, null, null, null, null, null, null, null, 1, 20));
assertThrows(IllegalArgumentException.class, () -> tools.hardwareListQuery(
null, null, null, null, 2, null, null, null, null, null, null, 1, 20));
verifyNoInteractions(hardwareViewMapper);
}
@Test
void springAiPublishesBothReviewedToolSchemas() {
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(2, schemas.size());
assertTrue(schemas.containsKey("hardware.list.query"));
assertTrue(schemas.containsKey("hardware.detail.query"));
assertTrue(schemas.get("hardware.list.query").contains("lockMachineState"));
assertTrue(schemas.get("hardware.detail.query").contains("snCode"));
}
}
@@ -0,0 +1,197 @@
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.HardwareView;
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.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.verifyNoInteractions;
import static org.mockito.Mockito.when;
/** PermissionService.applyHardwareRowFilter 直接单测:hardware_view 行级权限各分支(与站点同一 getByIsAgent 变体)。 */
@ExtendWith(MockitoExtension.class)
class PermissionServiceHardwareRowFilterTest {
@Mock
AuthorityMapper authorityMapper;
@Mock
AgentMapper agentMapper;
@Mock
WarehouseAgentAuthMapper warehouseAgentAuthMapper;
@Mock
AgentManageDistrictAuthMapper agentManageDistrictAuthMapper;
@Mock
ProperytMapper properytMapper;
@Mock
StringRedisTemplate redisTemplate;
@Mock
HashOperations<String, Object, Object> hashOperations;
PermissionService permissionService;
@BeforeEach
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
HardwareView.class);
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
Properyt.class);
permissionService = new PermissionService(authorityMapper, agentMapper,
warehouseAgentAuthMapper, agentManageDistrictAuthMapper, properytMapper, redisTemplate);
}
@Test
void headquartersHardwareFilterAddsNoBoundary() {
User user = user(46L, 1L, null);
when(authorityMapper.selectById(1L)).thenReturn(authority(1L, 0));
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<>();
permissionService.applyHardwareRowFilter(wrapper, user);
assertTrue(wrapper.getSqlSegment().isBlank());
}
@Test
void agentHardwareFilterUsesSelfSubordinateAndManagedAgents() {
User user = user(1000L, 108L, 1L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
Agent subordinate = new Agent();
subordinate.setId(2L);
when(agentMapper.selectList(any())).thenReturn(List.of(subordinate));
WarehouseAgentAuth managedAuth = new WarehouseAgentAuth();
managedAuth.setManagerAgentId(3L);
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managedAuth));
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of());
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<>();
permissionService.applyHardwareRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
Collection<Object> values = wrapper.getParamNameValuePairs().values();
assertTrue(sql.contains("agent_id IN"));
assertFalse(sql.contains("property_id"));
assertTrue(values.contains(1L));
assertTrue(values.contains(2L));
assertTrue(values.contains(3L));
}
@Test
void agentHardwareFilterAddsManagedPropertyOrBranch() {
User user = user(1000L, 108L, 1L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
AgentManageDistrictAuth districtAuth = new AgentManageDistrictAuth();
districtAuth.setPropertyId(44L);
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(districtAuth));
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<>();
permissionService.applyHardwareRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
Collection<Object> values = wrapper.getParamNameValuePairs().values();
assertTrue(sql.contains("agent_id IN"));
assertTrue(sql.contains("property_id IN"));
assertTrue(sql.contains("OR"));
assertTrue(values.contains(1L));
assertTrue(values.contains(44L));
}
@Test
void salesNonLeaderHardwareFilterUsesOwnCreatedPropertyIds() {
User user = user(1000L, 108L, 1L);
user.setDeptId(5L);
user.setPostId(7L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
Properyt property = new Properyt();
property.setId(55L);
when(properytMapper.selectList(any())).thenReturn(List.of(property));
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<>();
permissionService.applyHardwareRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
assertTrue(sql.contains("property_id IN"));
assertFalse(sql.contains("agent_id"));
assertTrue(wrapper.getParamNameValuePairs().values().contains(55L));
verifyNoInteractions(agentManageDistrictAuthMapper);
}
@Test
void salesNonLeaderWithoutVisiblePropertiesFiltersToZero() {
User user = user(1000L, 108L, 1L);
user.setDeptId(5L);
user.setPostId(7L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
when(properytMapper.selectList(any())).thenReturn(List.of());
LambdaQueryWrapper<HardwareView> wrapper = new LambdaQueryWrapper<>();
permissionService.applyHardwareRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
assertTrue(sql.contains("property_id IN"));
assertFalse(sql.contains("agent_id"));
assertTrue(wrapper.getParamNameValuePairs().values().contains(0L));
verifyNoInteractions(agentManageDistrictAuthMapper);
}
@Test
void agentUserWithoutAgentIdIsDeniedForHardwareFilter() {
User user = user(1000L, 108L, null);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
assertThrows(IllegalStateException.class,
() -> permissionService.applyHardwareRowFilter(new LambdaQueryWrapper<>(), user));
}
private static Authority authority(Long id, int isAgent) {
Authority authority = new Authority();
authority.setId(id);
authority.setIsagent(isAgent);
authority.setSoftDelete(0);
return authority;
}
private static User user(Long id, Long authorityId, Long agentId) {
User user = new User();
user.setId(id);
user.setAuthorityId(authorityId);
user.setAgentId(agentId);
return user;
}
}