feat(mcp): 新增站点列表与详情工具 (REQ-20260908-0006)
This commit is contained in:
@@ -119,7 +119,66 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. sys.ping
|
||||
## 4. district.list.query
|
||||
|
||||
分页查询站点列表,对应老系统 `/district/districtlistByPage`,但使用固定轻量字段契约。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m0 库 `district_district_strategy` 视图,固定 `soft_delete = 0`、`id DESC`
|
||||
- 行级权限:总部看全部;代理商按自己、直属下级、代管代理及授权物业过滤;销售部门非负责人沿用老接口的互斥物业范围。筛选条件只能缩小权限结果。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 否 | 站点 id,正整数,精确匹配 |
|
||||
| districtName | string | 否 | 站点名称,模糊匹配 |
|
||||
| districtState | number | 否 | 0=测试 1=履约 2=演示 3=撤机 |
|
||||
| province | string | 否 | 省份,模糊匹配 |
|
||||
| city | string | 否 | 城市,模糊匹配 |
|
||||
| agentId | number | 否 | 代理商 id,正整数,精确匹配 |
|
||||
| propertyId | number | 否 | 物业 id,正整数,精确匹配 |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100 |
|
||||
|
||||
**出参 records 字段**:
|
||||
|
||||
`id`、`districtName`、`districtNumber`、`propertyId`、`propertyName`、`agentId`、`agentName`、`province`、`city`、`countyseat`、`address`、`districtState`、`onlineState`、`hardwareCount`、`isSmart`。
|
||||
|
||||
`propertyName`、`agentName` 均为批量富化,查不到时返回 `未查询到`。
|
||||
|
||||
---
|
||||
|
||||
## 5. district.detail.get
|
||||
|
||||
按站点 id 获取扩展详情。站点 id 与当前用户行权限在同一个数据库查询条件中组合。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 入参:`id`(number,必填,正整数)
|
||||
- 找到:`{ "found": true, "data": { ... } }`
|
||||
- 不存在或无权限:`{ "found": false, "message": "站点不存在或无权访问" }`
|
||||
|
||||
**data 字段**:
|
||||
|
||||
| 分组 | 字段 |
|
||||
|---|---|
|
||||
| 归属 | id、districtName、districtNumber、propertyId、propertyName、agentId、agentName、salesId、salesName、serviceCompanyId、serviceCompany |
|
||||
| 地址联系人 | province、city、countyseat、address、contact、phone、mail |
|
||||
| 站点属性 | businessFormat、attributeId、attributeName、installType、isSmart、lnfluenceLevel、buildCount、hardwareCount、airdetectorFlag |
|
||||
| 状态周期 | districtState、onlineState、testDate、honourDate、weaningDate、testDays |
|
||||
| 设备耗材 | deviceSalesType、paperSalesType、consumablesType、posDevicePaperNorm、toiletDevicePaperNorm |
|
||||
| 功能开关 | pointState、assignmentWorkState、airState、messageFlag、reportFlag、lockMachineFlag、eventRepairFlag、rfidUseFlag、calibratedFlag、illegalPaperDisable |
|
||||
| 语音配置 | lowerPaperState、noPaperState、powerWarning、waitTimes |
|
||||
| 报警配置 | warning、warning2、warning3、warningTime、paperPercentWarn、maintainOrderWarning、foamCountWarning、foamPowerWarning |
|
||||
| 业务配置 | pointDeduction、effectiveUseTime、consumablesPrice、billDate、billType、billTypeStr、fineMoney |
|
||||
| 运行配置 | districtRefresh(Redis 未配置默认 5)、forbidFunction(Redis 未配置默认 0) |
|
||||
| 审计信息 | createId、createName、createTime、modifyTime、memo |
|
||||
|
||||
固定不返回 `softDelete`、`enterpriseId`、`modifyId`、内部服务 URL、重复 `districtId`、语义未确认的 `supervisorId` 和 SKU 列表。
|
||||
|
||||
---
|
||||
|
||||
## 6. sys.ping
|
||||
|
||||
服务自检(demo 工具,证明链路连通)。
|
||||
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
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.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("district_district_strategy")
|
||||
public class DistrictDistrictStrategy implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private String districtName;
|
||||
private String address;
|
||||
private Long propertyId;
|
||||
private Long agentId;
|
||||
private String province;
|
||||
private String city;
|
||||
private String countyseat;
|
||||
private String memo;
|
||||
private String contact;
|
||||
private String mail;
|
||||
private Integer buildCount;
|
||||
private String districtNumber;
|
||||
private String phone;
|
||||
private Integer installType;
|
||||
private Integer warning;
|
||||
private Integer airdetectorFlag;
|
||||
private Integer warning2;
|
||||
private Integer warning3;
|
||||
private Integer warningTime;
|
||||
private Integer onlineState;
|
||||
private Integer paperPercentWarn;
|
||||
private Integer pointState;
|
||||
private Integer assignmentWorkState;
|
||||
private Integer airState;
|
||||
private String businessFormat;
|
||||
private String posDevicePaperNorm;
|
||||
private String toiletDevicePaperNorm;
|
||||
private Integer messageFlag;
|
||||
private Integer reportFlag;
|
||||
private Double fineMoney;
|
||||
private Integer lockMachineFlag;
|
||||
private Integer eventRepairFlag;
|
||||
private Integer rfidUseFlag;
|
||||
private Integer calibratedFlag;
|
||||
private String salesName;
|
||||
private Long salesId;
|
||||
private Integer waitTimes;
|
||||
private Integer noPaperState;
|
||||
private Integer lowerPaperState;
|
||||
private Integer districtState;
|
||||
private Integer deviceSalesType;
|
||||
private Integer paperSalesType;
|
||||
private LocalDate weaningDate;
|
||||
private LocalDate honourDate;
|
||||
private LocalDate testDate;
|
||||
private Integer powerWarning;
|
||||
private Integer pointDeduction;
|
||||
private Integer testDays;
|
||||
private Integer illegalPaperDisable;
|
||||
private Integer effectiveUseTime;
|
||||
private Long attributeId;
|
||||
private String attributeName;
|
||||
private Integer hardwareCount;
|
||||
private Double consumablesPrice;
|
||||
private Integer billDate;
|
||||
private Integer foamCountWarning;
|
||||
private Integer foamPowerWarning;
|
||||
private Long serviceCompanyId;
|
||||
private String serviceCompany;
|
||||
private String billType;
|
||||
private Integer consumablesType;
|
||||
private Integer maintainOrderWarning;
|
||||
private Integer lnfluenceLevel;
|
||||
private Integer isSmart;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
|
||||
|
||||
public interface DistrictDistrictStrategyMapper extends BaseMapper<DistrictDistrictStrategy> {
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class DistrictTools implements ToolObject {
|
||||
|
||||
private static final int DEFAULT_ROWS = 20;
|
||||
private static final int MAX_ROWS = 100;
|
||||
|
||||
final DistrictDistrictStrategyMapper districtMapper;
|
||||
final ProperytMapper properytMapper;
|
||||
final AgentMapper agentMapper;
|
||||
final UserMapper userMapper;
|
||||
final PermissionService permissionService;
|
||||
final StringRedisTemplate redisTemplate;
|
||||
|
||||
public DistrictTools(DistrictDistrictStrategyMapper districtMapper, ProperytMapper properytMapper,
|
||||
AgentMapper agentMapper, UserMapper userMapper,
|
||||
PermissionService permissionService, StringRedisTemplate redisTemplate) {
|
||||
this.districtMapper = districtMapper;
|
||||
this.properytMapper = properytMapper;
|
||||
this.agentMapper = agentMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.permissionService = permissionService;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@XqTool(category = "customer", kind = "read")
|
||||
@Tool(name = "district.list.query", description = "分页查询当前用户可见的站点。"
|
||||
+ "支持站点 id、名称、状态、省市、代理商和物业筛选;名称、省、市为模糊匹配,其余为精确匹配。"
|
||||
+ "page 默认 1,rows 默认 20、上限 100,固定按 id 倒序。"
|
||||
+ "返回轻量站点字段,物业名和代理商名使用批量查询富化。")
|
||||
public Map<String, Object> districtListQuery(
|
||||
@ToolParam(required = false, description = "站点 id,正整数,精确匹配") Long id,
|
||||
@ToolParam(required = false, description = "站点名称,模糊匹配") String districtName,
|
||||
@ToolParam(required = false, description = "站点状态:0=测试 1=履约 2=演示 3=撤机") Integer districtState,
|
||||
@ToolParam(required = false, description = "省份,模糊匹配") String province,
|
||||
@ToolParam(required = false, description = "城市,模糊匹配") String city,
|
||||
@ToolParam(required = false, description = "代理商 id,正整数,精确匹配") Long agentId,
|
||||
@ToolParam(required = false, description = "物业 id,正整数,精确匹配") Long propertyId,
|
||||
@ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page,
|
||||
@ToolParam(required = false, description = "每页条数,默认 20,上限 100") Integer rows) {
|
||||
validatePositive(id, "id");
|
||||
validatePositive(agentId, "agentId");
|
||||
validatePositive(propertyId, "propertyId");
|
||||
if (districtState != null && (districtState < 0 || districtState > 3)) {
|
||||
throw new IllegalArgumentException("districtState 仅支持 0、1、2、3");
|
||||
}
|
||||
|
||||
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<DistrictDistrictStrategy> wrapper = new LambdaQueryWrapper<DistrictDistrictStrategy>()
|
||||
.eq(DistrictDistrictStrategy::getSoftDelete, 0);
|
||||
if (id != null) {
|
||||
wrapper.eq(DistrictDistrictStrategy::getId, id);
|
||||
}
|
||||
if (StringUtils.hasText(districtName)) {
|
||||
wrapper.like(DistrictDistrictStrategy::getDistrictName, districtName);
|
||||
}
|
||||
if (districtState != null) {
|
||||
wrapper.eq(DistrictDistrictStrategy::getDistrictState, districtState);
|
||||
}
|
||||
if (StringUtils.hasText(province)) {
|
||||
wrapper.like(DistrictDistrictStrategy::getProvince, province);
|
||||
}
|
||||
if (StringUtils.hasText(city)) {
|
||||
wrapper.like(DistrictDistrictStrategy::getCity, city);
|
||||
}
|
||||
if (agentId != null) {
|
||||
wrapper.eq(DistrictDistrictStrategy::getAgentId, agentId);
|
||||
}
|
||||
if (propertyId != null) {
|
||||
wrapper.eq(DistrictDistrictStrategy::getPropertyId, propertyId);
|
||||
}
|
||||
permissionService.applyDistrictRowFilter(wrapper, user);
|
||||
|
||||
Long total = districtMapper.selectCount(wrapper);
|
||||
wrapper.select(DistrictDistrictStrategy::getId, DistrictDistrictStrategy::getDistrictName,
|
||||
DistrictDistrictStrategy::getDistrictNumber, DistrictDistrictStrategy::getPropertyId,
|
||||
DistrictDistrictStrategy::getAgentId, DistrictDistrictStrategy::getProvince,
|
||||
DistrictDistrictStrategy::getCity, DistrictDistrictStrategy::getCountyseat,
|
||||
DistrictDistrictStrategy::getAddress, DistrictDistrictStrategy::getDistrictState,
|
||||
DistrictDistrictStrategy::getOnlineState, DistrictDistrictStrategy::getHardwareCount,
|
||||
DistrictDistrictStrategy::getIsSmart)
|
||||
.orderByDesc(DistrictDistrictStrategy::getId)
|
||||
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
|
||||
List<DistrictDistrictStrategy> districts = districtMapper.selectList(wrapper);
|
||||
|
||||
Map<Long, String> propertyNames = propertyNames(districts);
|
||||
Map<Long, String> agentNames = agentNames(districts);
|
||||
List<Map<String, Object>> records = new ArrayList<>();
|
||||
for (DistrictDistrictStrategy district : districts) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", district.getId());
|
||||
record.put("districtName", district.getDistrictName());
|
||||
record.put("districtNumber", district.getDistrictNumber());
|
||||
record.put("propertyId", district.getPropertyId());
|
||||
record.put("propertyName", name(propertyNames, district.getPropertyId()));
|
||||
record.put("agentId", district.getAgentId());
|
||||
record.put("agentName", name(agentNames, district.getAgentId()));
|
||||
record.put("province", district.getProvince());
|
||||
record.put("city", district.getCity());
|
||||
record.put("countyseat", district.getCountyseat());
|
||||
record.put("address", district.getAddress());
|
||||
record.put("districtState", district.getDistrictState());
|
||||
record.put("onlineState", district.getOnlineState());
|
||||
record.put("hardwareCount", district.getHardwareCount());
|
||||
record.put("isSmart", district.getIsSmart());
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
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 = "customer", kind = "read")
|
||||
@Tool(name = "district.detail.get", description = "按站点 id 查询当前用户可见的站点详情。"
|
||||
+ "站点不存在或无权访问均返回 found=false 和相同提示,防止枚举越权站点。"
|
||||
+ "返回固定业务字段白名单,不包含软删标记、内部服务地址和重复字段。")
|
||||
public Map<String, Object> districtDetailGet(
|
||||
@ToolParam(description = "站点 id,正整数") Long id) {
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("id 必填");
|
||||
}
|
||||
validatePositive(id, "id");
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
LambdaQueryWrapper<DistrictDistrictStrategy> wrapper = new LambdaQueryWrapper<DistrictDistrictStrategy>()
|
||||
.eq(DistrictDistrictStrategy::getSoftDelete, 0)
|
||||
.eq(DistrictDistrictStrategy::getId, id);
|
||||
permissionService.applyDistrictRowFilter(wrapper, user);
|
||||
DistrictDistrictStrategy district = districtMapper.selectOne(wrapper);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (district == null) {
|
||||
result.put("found", false);
|
||||
result.put("message", "站点不存在或无权访问");
|
||||
return result;
|
||||
}
|
||||
result.put("found", true);
|
||||
result.put("data", detail(district));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> detail(DistrictDistrictStrategy district) {
|
||||
Properyt property = district.getPropertyId() == null ? null : properytMapper.selectById(district.getPropertyId());
|
||||
Agent agent = district.getAgentId() == null ? null : agentMapper.selectById(district.getAgentId());
|
||||
User creator = district.getCreateId() == null ? null : userMapper.selectById(district.getCreateId());
|
||||
String billType = StringUtils.hasText(district.getBillType()) ? district.getBillType() : "1month";
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("id", district.getId());
|
||||
data.put("districtName", district.getDistrictName());
|
||||
data.put("districtNumber", district.getDistrictNumber());
|
||||
data.put("propertyId", district.getPropertyId());
|
||||
data.put("propertyName", property == null ? "未查询到" : property.getName());
|
||||
data.put("agentId", district.getAgentId());
|
||||
data.put("agentName", agent == null ? "未查询到" : agent.getName());
|
||||
data.put("salesId", district.getSalesId());
|
||||
data.put("salesName", district.getSalesName());
|
||||
data.put("serviceCompanyId", district.getServiceCompanyId());
|
||||
data.put("serviceCompany", district.getServiceCompany());
|
||||
|
||||
data.put("province", district.getProvince());
|
||||
data.put("city", district.getCity());
|
||||
data.put("countyseat", district.getCountyseat());
|
||||
data.put("address", district.getAddress());
|
||||
data.put("contact", district.getContact());
|
||||
data.put("phone", district.getPhone());
|
||||
data.put("mail", district.getMail());
|
||||
|
||||
data.put("businessFormat", district.getBusinessFormat());
|
||||
data.put("attributeId", district.getAttributeId());
|
||||
data.put("attributeName", district.getAttributeName());
|
||||
data.put("installType", district.getInstallType());
|
||||
data.put("isSmart", district.getIsSmart());
|
||||
data.put("lnfluenceLevel", district.getLnfluenceLevel());
|
||||
data.put("buildCount", district.getBuildCount());
|
||||
data.put("hardwareCount", district.getHardwareCount());
|
||||
data.put("airdetectorFlag", district.getAirdetectorFlag());
|
||||
|
||||
data.put("districtState", district.getDistrictState());
|
||||
data.put("onlineState", district.getOnlineState());
|
||||
data.put("testDate", district.getTestDate());
|
||||
data.put("honourDate", district.getHonourDate());
|
||||
data.put("weaningDate", district.getWeaningDate());
|
||||
data.put("testDays", district.getTestDays());
|
||||
|
||||
data.put("deviceSalesType", district.getDeviceSalesType());
|
||||
data.put("paperSalesType", district.getPaperSalesType());
|
||||
data.put("consumablesType", district.getConsumablesType());
|
||||
data.put("posDevicePaperNorm", district.getPosDevicePaperNorm());
|
||||
data.put("toiletDevicePaperNorm", district.getToiletDevicePaperNorm());
|
||||
|
||||
data.put("pointState", district.getPointState());
|
||||
data.put("assignmentWorkState", district.getAssignmentWorkState());
|
||||
data.put("airState", district.getAirState());
|
||||
data.put("messageFlag", district.getMessageFlag());
|
||||
data.put("reportFlag", district.getReportFlag());
|
||||
data.put("lockMachineFlag", district.getLockMachineFlag());
|
||||
data.put("eventRepairFlag", district.getEventRepairFlag());
|
||||
data.put("rfidUseFlag", district.getRfidUseFlag());
|
||||
data.put("calibratedFlag", district.getCalibratedFlag());
|
||||
data.put("illegalPaperDisable", district.getIllegalPaperDisable());
|
||||
|
||||
data.put("lowerPaperState", district.getLowerPaperState());
|
||||
data.put("noPaperState", district.getNoPaperState());
|
||||
data.put("powerWarning", district.getPowerWarning());
|
||||
data.put("waitTimes", district.getWaitTimes());
|
||||
|
||||
data.put("warning", district.getWarning());
|
||||
data.put("warning2", district.getWarning2());
|
||||
data.put("warning3", district.getWarning3());
|
||||
data.put("warningTime", district.getWarningTime());
|
||||
data.put("paperPercentWarn", district.getPaperPercentWarn());
|
||||
data.put("maintainOrderWarning", district.getMaintainOrderWarning());
|
||||
data.put("foamCountWarning", district.getFoamCountWarning());
|
||||
data.put("foamPowerWarning", district.getFoamPowerWarning());
|
||||
|
||||
data.put("pointDeduction", district.getPointDeduction());
|
||||
data.put("effectiveUseTime", district.getEffectiveUseTime());
|
||||
data.put("consumablesPrice", district.getConsumablesPrice());
|
||||
data.put("billDate", district.getBillDate());
|
||||
data.put("billType", billType);
|
||||
data.put("billTypeStr", billTypeName(billType));
|
||||
data.put("fineMoney", district.getFineMoney());
|
||||
|
||||
data.put("districtRefresh", redisInteger("DistrictRefresh:" + district.getId(), 5));
|
||||
data.put("forbidFunction", redisInteger("DistrictForbidFunction:" + district.getId(), 0));
|
||||
|
||||
data.put("createId", district.getCreateId());
|
||||
data.put("createName", creator == null ? "未分配" : creator.getName());
|
||||
data.put("createTime", format(district.getCreateTime()));
|
||||
data.put("modifyTime", format(district.getModifyTime()));
|
||||
data.put("memo", district.getMemo());
|
||||
return data;
|
||||
}
|
||||
|
||||
private Map<Long, String> propertyNames(List<DistrictDistrictStrategy> districts) {
|
||||
List<Long> ids = districts.stream().map(DistrictDistrictStrategy::getPropertyId)
|
||||
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return properytMapper.selectBatchIds(ids).stream()
|
||||
.collect(Collectors.toMap(Properyt::getId,
|
||||
property -> property.getName() == null ? "未查询到" : property.getName(), (a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<Long, String> agentNames(List<DistrictDistrictStrategy> districts) {
|
||||
List<Long> ids = districts.stream().map(DistrictDistrictStrategy::getAgentId)
|
||||
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return agentMapper.selectBatchIds(ids).stream()
|
||||
.collect(Collectors.toMap(Agent::getId,
|
||||
agent -> agent.getName() == null ? "未查询到" : agent.getName(), (a, b) -> a));
|
||||
}
|
||||
|
||||
private static String name(Map<Long, String> names, Long id) {
|
||||
return id == null ? "未查询到" : names.getOrDefault(id, "未查询到");
|
||||
}
|
||||
|
||||
private int redisInteger(String key, int defaultValue) {
|
||||
String value = redisTemplate.opsForValue().get(key);
|
||||
return value == null ? defaultValue : Integer.parseInt(value);
|
||||
}
|
||||
|
||||
private static String billTypeName(String billType) {
|
||||
if (billType.contains("month")) {
|
||||
return billType.replace("month", "个月账期");
|
||||
}
|
||||
if (billType.contains("quarter")) {
|
||||
return billType.replace("quarter", "季度账期");
|
||||
}
|
||||
return billType;
|
||||
}
|
||||
|
||||
private static String format(java.util.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 + " 必须为正整数");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.DistrictDistrictStrategy;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
@@ -243,4 +244,50 @@ public class PermissionService {
|
||||
wrapper.in(Properyt::getAgentId, ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点行权限:1:1 对齐老接口 districtlistByPage 使用的
|
||||
* getByIsAgent(..., propertyFlag=true, className="property_id")。
|
||||
*/
|
||||
public void applyDistrictRowFilter(LambdaQueryWrapper<DistrictDistrictStrategy> wrapper, User user) {
|
||||
if (!isAgentAuthority(user.getAuthorityId())) {
|
||||
return;
|
||||
}
|
||||
if (user.getAgentId() == null) {
|
||||
throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据");
|
||||
}
|
||||
|
||||
List<Long> ids = new ArrayList<>(subordinateAndSelfIds(user));
|
||||
List<WarehouseAgentAuth> agentlist = managedAuths(user.getId());
|
||||
ids.addAll(agentlist.stream().map(WarehouseAgentAuth::getManagerAgentId).collect(Collectors.toList()));
|
||||
|
||||
if (isSalesNonLeader(user)) {
|
||||
List<Long> propertyIds = properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
|
||||
.eq(Properyt::getSoftDelete, 0)
|
||||
.eq(Properyt::getCreateId, user.getId())
|
||||
.select(Properyt::getId))
|
||||
.stream().map(Properyt::getId).collect(Collectors.toList());
|
||||
if (!agentlist.isEmpty()) {
|
||||
ids.remove(user.getAgentId());
|
||||
if (!ids.isEmpty()) {
|
||||
propertyIds.addAll(properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
|
||||
.eq(Properyt::getSoftDelete, 0)
|
||||
.in(Properyt::getAgentId, ids)
|
||||
.select(Properyt::getId))
|
||||
.stream().map(Properyt::getId).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
wrapper.in(DistrictDistrictStrategy::getPropertyId,
|
||||
propertyIds.isEmpty() ? List.of(0L) : propertyIds);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Long> managedPropertyIds = managedPropertyIds(ids);
|
||||
if (!managedPropertyIds.isEmpty()) {
|
||||
wrapper.and(w -> w.in(DistrictDistrictStrategy::getAgentId, ids)
|
||||
.or().in(DistrictDistrictStrategy::getPropertyId, managedPropertyIds));
|
||||
} else {
|
||||
wrapper.in(DistrictDistrictStrategy::getAgentId, ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.DistrictDistrictStrategyMapper;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContext;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
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.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
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 DistrictToolsTest {
|
||||
|
||||
@Mock
|
||||
DistrictDistrictStrategyMapper districtMapper;
|
||||
@Mock
|
||||
ProperytMapper properytMapper;
|
||||
@Mock
|
||||
AgentMapper agentMapper;
|
||||
@Mock
|
||||
UserMapper userMapper;
|
||||
@Mock
|
||||
PermissionService permissionService;
|
||||
@Mock
|
||||
StringRedisTemplate redisTemplate;
|
||||
@Mock
|
||||
ValueOperations<String, String> valueOperations;
|
||||
|
||||
DistrictTools tools;
|
||||
User user;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
DistrictDistrictStrategy.class);
|
||||
tools = new DistrictTools(districtMapper, properytMapper, agentMapper,
|
||||
userMapper, permissionService, redisTemplate);
|
||||
user = new User();
|
||||
user.setId(7L);
|
||||
AuthContextHolder.set(new AuthContext(null, user));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AuthContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsesReviewedContractAndTruncatesRows() {
|
||||
DistrictDistrictStrategy district = new DistrictDistrictStrategy();
|
||||
district.setId(9L);
|
||||
district.setDistrictName("测试站点");
|
||||
district.setDistrictNumber("D-009");
|
||||
district.setPropertyId(11L);
|
||||
district.setAgentId(12L);
|
||||
district.setProvince("广东省");
|
||||
district.setCity("深圳市");
|
||||
district.setCountyseat("南山区");
|
||||
district.setAddress("科技园");
|
||||
district.setDistrictState(1);
|
||||
district.setOnlineState(1);
|
||||
district.setHardwareCount(6);
|
||||
district.setIsSmart(1);
|
||||
when(districtMapper.selectCount(any())).thenReturn(1L);
|
||||
when(districtMapper.selectList(any())).thenReturn(List.of(district));
|
||||
|
||||
Properyt property = new Properyt();
|
||||
property.setId(11L);
|
||||
property.setName("测试物业");
|
||||
when(properytMapper.selectBatchIds(any())).thenReturn(List.of(property));
|
||||
Agent agent = new Agent();
|
||||
agent.setId(12L);
|
||||
agent.setName("测试代理");
|
||||
when(agentMapper.selectBatchIds(any())).thenReturn(List.of(agent));
|
||||
|
||||
Map<String, Object> result = tools.districtListQuery(
|
||||
null, null, null, null, null, null, null, 0, 500);
|
||||
|
||||
assertEquals(1, result.get("page"));
|
||||
assertEquals(100, result.get("rows"));
|
||||
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
|
||||
assertEquals(List.of("id", "districtName", "districtNumber", "propertyId", "propertyName",
|
||||
"agentId", "agentName", "province", "city", "countyseat", "address",
|
||||
"districtState", "onlineState", "hardwareCount", "isSmart"),
|
||||
record.keySet().stream().toList());
|
||||
assertEquals("测试物业", record.get("propertyName"));
|
||||
assertEquals("测试代理", record.get("agentName"));
|
||||
verify(permissionService).applyDistrictRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailDoesNotDistinguishMissingFromUnauthorized() {
|
||||
when(districtMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
Map<String, Object> result = tools.districtDetailGet(99L);
|
||||
|
||||
assertFalse((Boolean) result.get("found"));
|
||||
assertEquals("站点不存在或无权访问", result.get("message"));
|
||||
verify(permissionService).applyDistrictRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsExpandedWhitelistWithoutInternalFields() {
|
||||
DistrictDistrictStrategy district = new DistrictDistrictStrategy();
|
||||
district.setId(9L);
|
||||
when(districtMapper.selectOne(any())).thenReturn(district);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
|
||||
Map<String, Object> result = tools.districtDetailGet(9L);
|
||||
|
||||
assertTrue((Boolean) result.get("found"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
assertTrue(data.keySet().containsAll(List.of(
|
||||
"districtName", "propertyName", "agentName", "businessFormat", "districtState",
|
||||
"deviceSalesType", "pointState", "lowerPaperState", "warning", "billTypeStr",
|
||||
"districtRefresh", "forbidFunction", "createName", "modifyTime")));
|
||||
assertFalse(data.containsKey("softDelete"));
|
||||
assertFalse(data.containsKey("enterpriseId"));
|
||||
assertFalse(data.containsKey("modifyId"));
|
||||
assertFalse(data.containsKey("publicServiceUrl"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailRequiresPositiveId() {
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtDetailGet(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtDetailGet(0L));
|
||||
verifyNoInteractions(districtMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void springAiPublishesBothReviewedToolSchemas() throws Exception {
|
||||
ToolCallback[] callbacks = 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.get("district.list.query").contains("districtName"));
|
||||
assertTrue(schemas.get("district.list.query").contains("propertyId"));
|
||||
assertTrue(new ObjectMapper().readTree(schemas.get("district.detail.get"))
|
||||
.path("required").toString().contains("\"id\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRejectsUnknownDistrictStateBeforeQuerying() {
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.districtListQuery(
|
||||
null, null, 4, null, null, null, null, 1, 20));
|
||||
|
||||
verifyNoInteractions(districtMapper);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
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.DistrictDistrictStrategy;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
@@ -22,6 +24,7 @@ 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;
|
||||
@@ -51,6 +54,8 @@ class PermissionServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Agent.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Properyt.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DistrictDistrictStrategy.class);
|
||||
permissionService = new PermissionService(authorityMapper, agentMapper,
|
||||
warehouseAgentAuthMapper, agentManageDistrictAuthMapper, properytMapper, redisTemplate);
|
||||
}
|
||||
@@ -140,6 +145,61 @@ class PermissionServiceTest {
|
||||
() -> permissionService.isSalesNonLeader(user));
|
||||
}
|
||||
|
||||
@Test
|
||||
void districtFilterKeepsAgentOrAuthorizedPropertyBoundary() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
when(agentMapper.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
when(warehouseAgentAuthMapper.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
AgentManageDistrictAuth auth = new AgentManageDistrictAuth();
|
||||
auth.setPropertyId(44L);
|
||||
when(agentManageDistrictAuthMapper.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(auth));
|
||||
|
||||
LambdaQueryWrapper<DistrictDistrictStrategy> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyDistrictRowFilter(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(values.contains(1L));
|
||||
assertTrue(values.contains(44L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void headquartersDistrictFilterAddsNoBoundary() {
|
||||
User user = user(46L, 1L, 0L);
|
||||
when(authorityMapper.selectById(1L)).thenReturn(authority(1L, 0));
|
||||
|
||||
LambdaQueryWrapper<DistrictDistrictStrategy> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyDistrictRowFilter(wrapper, user);
|
||||
|
||||
assertTrue(wrapper.getSqlSegment().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void salesDistrictFilterUsesOnlyVisiblePropertyIds() {
|
||||
User user = user(1000L, 108L, 1L);
|
||||
user.setDeptId(5L);
|
||||
user.setPostId(7L);
|
||||
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
|
||||
when(agentMapper.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
when(warehouseAgentAuthMapper.selectList(org.mockito.ArgumentMatchers.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(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(property));
|
||||
|
||||
LambdaQueryWrapper<DistrictDistrictStrategy> wrapper = new LambdaQueryWrapper<>();
|
||||
permissionService.applyDistrictRowFilter(wrapper, user);
|
||||
|
||||
assertTrue(wrapper.getSqlSegment().contains("property_id IN"));
|
||||
assertFalse(wrapper.getSqlSegment().contains("agent_id IN"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(55L));
|
||||
}
|
||||
|
||||
private static Authority authority(Long id, int isAgent) {
|
||||
Authority authority = new Authority();
|
||||
authority.setId(id);
|
||||
|
||||
Reference in New Issue
Block a user