20260627:网关设备删除支持删除子设备&平台对接日志支持批量删除、系统设备支持查搜、支持设备编号查搜映射关系条目(细节优化)

This commit is contained in:
zhugongping 2026-06-28 08:22:03 +08:00
parent 8c12a70df2
commit 820bd95632
13 changed files with 264 additions and 32 deletions

View File

@ -277,7 +277,7 @@ public class DeviceController extends BaseController {
@DeleteMapping("/{deviceIds}")
@ApiOperation("批量删除设备")
public AjaxResult remove(@PathVariable Long[] deviceIds) throws SchedulerException {
return deviceService.deleteDeviceByDeviceId(deviceIds[0]);
return deviceService.deleteDeviceByDeviceIds(deviceIds);
}
/**

View File

@ -11,6 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
@RestController
@RequestMapping("/platform/push-log")
@Api(tags = "平台推送日志")
@ -41,4 +43,11 @@ public class PlatformPushLogController extends BaseController {
pushLogService.retry(id);
return success("重推成功");
}
@PreAuthorize("@ss.hasPermi('platform:pushLog:remove')")
@DeleteMapping("/{ids}")
@ApiOperation("批量删除推送日志")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(pushLogService.removeByIds(Arrays.asList(ids)));
}
}

View File

@ -110,4 +110,12 @@ public interface SubGatewayMapper {
* @param gwDeviceId
*/
void deleteGatewayByGwDeviceId(Long gwDeviceId);
/**
* 根据网关设备ID查询子设备ID列表
*
* @param gwDeviceId 网关设备ID
* @return 子设备ID列表
*/
List<Long> selectSubDeviceIdsByGwDeviceId(Long gwDeviceId);
}

View File

@ -9,4 +9,6 @@ import java.util.List;
public interface PlatformDeviceMappingMapper extends BaseMapperX<PlatformDeviceMapping> {
List<PlatformDeviceMapping> selectBySerialNumber(@Param("serialNumber") String serialNumber);
int countByReportSerialNumber(@Param("serialNumber") String serialNumber);
}

View File

@ -42,9 +42,16 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
@Override
public List<PlatformDeviceMapping> selectList(PlatformDeviceMapping query) {
String serialNumber = query.getSerialNumber();
LambdaQueryWrapper<PlatformDeviceMapping> wrapper = Wrappers.<PlatformDeviceMapping>lambdaQuery()
.eq(query.getPlatformId() != null, PlatformDeviceMapping::getPlatformId, query.getPlatformId())
.eq(query.getDeviceId() != null, PlatformDeviceMapping::getDeviceId, query.getDeviceId())
.and(StringUtils.hasText(serialNumber), w -> w
.like(PlatformDeviceMapping::getSerialNumber, serialNumber)
.or()
.apply("EXISTS (SELECT 1 FROM platform_device_sub_mapping s " +
"WHERE s.parent_mapping_id = platform_device_mapping.id AND s.sub_serial_number LIKE {0})",
"%" + serialNumber + "%"))
.eq(StringUtils.hasText(query.getSensorType()), PlatformDeviceMapping::getSensorType, query.getSensorType())
.eq(query.getNetStatus() != null, PlatformDeviceMapping::getNetStatus, query.getNetStatus())
.eq(StringUtils.hasText(query.getEnable()), PlatformDeviceMapping::getEnable, query.getEnable())

View File

@ -227,6 +227,14 @@ public interface IDeviceService {
*/
public AjaxResult deleteDeviceByDeviceId(Long deviceId) throws SchedulerException;
/**
* 批量删除设备
*
* @param deviceIds 需要删除的设备主键集合
* @return 结果
*/
public AjaxResult deleteDeviceByDeviceIds(Long[] deviceIds) throws SchedulerException;
/**
* 生成设备唯一编号
*

View File

@ -7,6 +7,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.constant.HttpStatus;
import com.fastbee.common.constant.ProductAuthConstant;
import com.fastbee.common.core.device.DeviceAndProtocol;
import com.fastbee.common.core.domain.AjaxResult;
@ -46,6 +47,7 @@ import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.ProductSubGatewayVO;
import com.fastbee.iot.model.gateWay.SubDeviceCreateByProductVO;
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper;
import com.fastbee.iot.service.*;
import com.fastbee.iot.tdengine.service.ILogService;
import com.fastbee.system.mapper.SysDeptMapper;
@ -143,6 +145,8 @@ public class DeviceServiceImpl implements IDeviceService {
private SubGatewayMapper subGatewayMapper;
@Autowired
private DeviceMapper deviceMapper;
@Resource
private PlatformDeviceMappingMapper platformDeviceMappingMapper;
@Autowired
private DeviceUserMapper deviceUserMapper;
@Resource
@ -331,14 +335,71 @@ public class DeviceServiceImpl implements IDeviceService {
/**
* 删除设备
*
* @param deviceId 需要删除的设备主键
* @param deviceIds 需要删除的设备主键
* @return 结果
*/
@CacheEvict(cacheNames = "Device", allEntries = true)
@Transactional(rollbackFor = Exception.class)
@Override
public AjaxResult deleteDeviceByDeviceIds(Long[] deviceIds) throws SchedulerException {
if (deviceIds == null || deviceIds.length == 0) {
return AjaxResult.error("请选择要删除的设备");
}
SysUser user = getLoginUser().getUser();
List<Device> devices = new ArrayList<>();
List<Long> existsDeviceIds = new ArrayList<>();
for (Long deviceId : deviceIds) {
if (deviceId == null || existsDeviceIds.contains(deviceId)) {
continue;
}
Device device = deviceMapper.selectDeviceByDeviceId(deviceId);
if (device == null) {
continue;
}
devices.add(device);
existsDeviceIds.add(deviceId);
}
if (CollectionUtils.isEmpty(devices)) {
return AjaxResult.error("设备不存在");
}
for (Device device : devices) {
if (isGeneralUserForDelete(device, user)) {
continue;
}
AjaxResult platformMappingCheck = checkPlatformMappingBeforeDelete(device);
if (!isSuccessResult(platformMappingCheck)) {
return platformMappingCheck;
}
}
int deletedCount = 0;
for (Device device : devices) {
if (deviceMapper.selectDeviceByDeviceId(device.getDeviceId()) == null) {
continue;
}
AjaxResult result = deleteDeviceByDeviceId(device.getDeviceId());
if (!isSuccessResult(result)) {
if (deletedCount == 0) {
return result;
}
throw new ServiceException(Objects.toString(result.get(AjaxResult.MSG_TAG), "删除设备失败"));
}
deletedCount++;
}
return AjaxResult.success();
}
@CacheEvict(cacheNames = "Device", allEntries = true)
@Transactional(rollbackFor = Exception.class)
@Override
public AjaxResult deleteDeviceByDeviceId(Long deviceId) {
// 查询设备
Device device = deviceMapper.selectDeviceByDeviceId(deviceId);
if (device == null) {
return AjaxResult.error("设备不存在");
}
SysUser user = getLoginUser().getUser();
Long sceneUserId;
Long deptUserId = user.getDept().getDeptUserId();
@ -349,19 +410,7 @@ public class DeviceServiceImpl implements IDeviceService {
}
// 2.3版本更改 设备管理者和设备拥有者为true,普通用户如果不是设备所有者,只能删除设备用户和用户自己的设备关联分组信息
// 删除当前层级分配记录,将设备改回到上一级
boolean isGeneralUser;
if (isAdmin(user.getUserId())) {
isGeneralUser = false;
} else {
// 终端用户和非设备所属机构用户不允许删除设备
if (null == user.getDeptId()) {
isGeneralUser = true;
} else if (!Objects.equals(device.getTenantId(), deptUserId)) {
isGeneralUser = true;
} else {
isGeneralUser = false;
}
}
boolean isGeneralUser = isGeneralUserForDelete(device, user);
// 设备下不能有场景联动
List<SceneDeviceBindVO> sceneDeviceBindVOList = sceneDeviceMapper.listSceneDeviceBind(device.getSerialNumber());
if (null != sceneDeviceBindVOList.stream().filter(s -> s.getUserId().equals(sceneUserId)).findAny()
@ -416,6 +465,13 @@ public class DeviceServiceImpl implements IDeviceService {
}
}
AjaxResult platformMappingCheck = checkPlatformMappingBeforeDelete(device);
if (!isSuccessResult(platformMappingCheck)) {
return platformMappingCheck;
}
// 删除网关设备
deleteGatewaySubDevices(device);
if (null != shareSceneIds && shareSceneIds.length > 0) {
sceneService.deleteSceneBySceneIds(shareSceneIds);
}
@ -463,7 +519,6 @@ public class DeviceServiceImpl implements IDeviceService {
// 删除网关绑定子设备关系
if (DeviceType.GATEWAY.getCode() == device.getDeviceType()) {
subGatewayMapper.deleteGatewayByGwDeviceId(deviceId);
// todo 需要删子设备
}
// 删除modbus轮询指令
String keyModbus = RedisKeyBuilder.buildModbusRuntimeCacheKey(device.getSerialNumber());
@ -472,6 +527,77 @@ public class DeviceServiceImpl implements IDeviceService {
return AjaxResult.success();
}
private boolean isGeneralUserForDelete(Device device, SysUser user) {
if (isAdmin(user.getUserId())) {
return false;
}
// 终端用户和非设备所属机构用户不是真正删除设备,只解除自己的设备关系。
if (user.getDeptId() == null) {
return true;
}
return !Objects.equals(device.getTenantId(), user.getDept().getDeptUserId());
}
private AjaxResult checkPlatformMappingBeforeDelete(Device device) {
List<Device> devices = new ArrayList<>();
devices.add(device);
if (DeviceType.GATEWAY.getCode() == device.getDeviceType()) {
devices.addAll(selectGatewaySubDevices(device));
}
for (Device item : devices) {
if (item == null || StringUtils.isEmpty(item.getSerialNumber())) {
continue;
}
if (platformDeviceMappingMapper.countByReportSerialNumber(item.getSerialNumber()) > 0) {
String prefix = Objects.equals(item.getDeviceId(), device.getDeviceId()) ? "设备" : "子设备";
String name = StringUtils.isNotEmpty(item.getDeviceName()) ? item.getDeviceName() : item.getSerialNumber();
return AjaxResult.error(prefix + "【" + name + "】存在数据推送关系,请先删除数据推送关系才能删除本设备");
}
}
return AjaxResult.success();
}
private void deleteGatewaySubDevices(Device device) {
if (DeviceType.GATEWAY.getCode() != device.getDeviceType()) {
return;
}
List<Device> subDevices = selectGatewaySubDevices(device);
for (Device subDevice : subDevices) {
if (subDevice == null || subDevice.getDeviceId() == null) {
continue;
}
AjaxResult result = deleteDeviceByDeviceId(subDevice.getDeviceId());
if (!isSuccessResult(result)) {
String msg = result == null ? "未知错误" : Objects.toString(result.get(AjaxResult.MSG_TAG), "未知错误");
String name = StringUtils.isNotEmpty(subDevice.getDeviceName())
? subDevice.getDeviceName() : subDevice.getSerialNumber();
throw new ServiceException("删除网关子设备【" + name + "】失败:" + msg);
}
}
}
private List<Device> selectGatewaySubDevices(Device device) {
if (device.getDeviceId() == null) {
return Collections.emptyList();
}
List<Long> subDeviceIds = subGatewayMapper.selectSubDeviceIdsByGwDeviceId(device.getDeviceId());
if (CollectionUtils.isEmpty(subDeviceIds)) {
return Collections.emptyList();
}
List<Device> subDevices = new ArrayList<>();
for (Long subDeviceId : subDeviceIds) {
Device subDevice = deviceMapper.selectDeviceByDeviceId(subDeviceId);
if (subDevice != null) {
subDevices.add(subDevice);
}
}
return subDevices;
}
private boolean isSuccessResult(AjaxResult result) {
return result != null && Objects.equals(result.get(AjaxResult.CODE_TAG), HttpStatus.SUCCESS);
}
/**
* TODO-2.3 查询设备统计信息
*

View File

@ -127,6 +127,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
delete from iot_sub_gateway where gw_device_id = #{gwDeviceId}
</delete>
<select id="selectSubDeviceIdsByGwDeviceId" resultType="java.lang.Long">
select sub_device_id
from iot_sub_gateway
where gw_device_id = #{gwDeviceId}
</select>
<select id="getIsSelectGateSubDevice" parameterType="com.fastbee.iot.model.gateWay.GateSubDeviceVO"
resultMap="GatewaySubDeviceResult">
select d.device_id,d.serial_number,d.device_name

View File

@ -34,4 +34,16 @@
WHERE s.sub_serial_number = #{serialNumber} AND m.enable = '1'
</select>
<select id="countByReportSerialNumber" resultType="int">
SELECT COUNT(1)
FROM platform_device_mapping m
WHERE m.serial_number = #{serialNumber}
OR EXISTS (
SELECT 1
FROM platform_device_sub_mapping s
WHERE s.parent_mapping_id = m.id
AND s.sub_serial_number = #{serialNumber}
)
</select>
</mapper>

View File

@ -42,6 +42,7 @@ INSERT INTO `sys_menu` VALUES(3524, '绑定删除', 3520, 4, '#', '', NULL, 1, 0
INSERT INTO `sys_menu` VALUES(3530, '推送日志', 3500, 4, 'push-log', 'platform/push-log/index', NULL, 1, 0, 'C', '0', '0', 'platform:pushLog:list', 'log', 'admin', NOW(), '', NULL, '推送日志菜单');
INSERT INTO `sys_menu` VALUES(3531, '日志查询', 3530, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'platform:pushLog:list', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO `sys_menu` VALUES(3532, '日志详情', 3530, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'platform:pushLog:query', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO `sys_menu` VALUES(3533, '日志删除', 3530, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'platform:pushLog:remove', '#', 'admin', NOW(), '', NULL, '');
-- ----------------------------
-- 二级菜单:接口配置(高级,默认隐藏)

View File

@ -11,3 +11,7 @@ export function getPlatformPushLog(id) {
export function retryPlatformPushLog(id) {
return request({ url: `/platform/push-log/${id}/retry`, method: 'post' })
}
export function delPlatformPushLog(ids) {
return request({ url: `/platform/push-log/${ids}`, method: 'delete' })
}

View File

@ -8,6 +8,9 @@
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="设备编号" prop="serialNumber">
<el-input v-model="queryParams.serialNumber" placeholder="主设备/子设备编号" clearable style="width:170px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="设备类型" prop="sensorType">
<el-select v-model="queryParams.sensorType" placeholder="全部" clearable filterable allow-create default-first-option style="width:140px">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
@ -115,10 +118,11 @@
</el-select>
</el-form-item>
<el-form-item label="系统设备" prop="deviceId">
<el-select v-model="form.deviceId" placeholder="输入设备名称/序列号搜索" filterable remote
<el-select v-model="form.deviceId" placeholder="选择直连或网关设备,支持名称/编号搜索" filterable remote clearable
:remote-method="searchDevice" :loading="deviceSearchLoading" style="width:100%"
@visible-change="onDeviceSelectVisible"
@change="onDeviceChange">
<el-option v-for="d in deviceOptions" :key="d.deviceId" :label="`${d.deviceName}(${d.serialNumber})`" :value="d.deviceId" />
<el-option v-for="d in deviceOptions" :key="d.deviceId" :label="deviceOptionLabel(d)" :value="d.deviceId" />
</el-select>
</el-form-item>
<el-form-item label="序列号">
@ -254,7 +258,7 @@
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping'
import { listPlatformInstance } from '@/api/platform/instance'
import { listPlatformInterface } from '@/api/platform/interface'
import { listDevice, listThingsModel, getDevice } from '@/api/iot/device'
import { listDevice, listThingsModel, getDevice, listDeviceShort } from '@/api/iot/device'
import { listGateway } from '@/api/iot/gateway'
import { getDicts } from '@/api/system/dict/data'
@ -278,7 +282,7 @@ export default {
// 子表缓存 (列表展开行使用)
subMappingCache: {},
sensorTypeDictOptions: [],
queryParams: { pageNum: 1, pageSize: 20, platformId: null, sensorType: '', netStatus: null },
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', sensorType: '', netStatus: null },
form: { enable: '1', deviceType: 0, reportIntervalSec: 300 },
rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
@ -353,6 +357,12 @@ export default {
},
platformName(id) { const p = this.platformList.find(x => x.id === id); return p ? p.name : id },
deviceName(id) { return this.deviceMap[id] || id },
deviceOptionLabel(device) {
const typeText = device.deviceType === 2 || device.subDeviceCount > 0 ? '网关' : '直连'
const name = device.deviceName || device.serialNumber || device.deviceId
const sn = device.serialNumber ? `(${device.serialNumber})` : ''
return `${name}${sn} - ${typeText}`
},
sensorLabel(v) {
const option = this.sensorTypeOptions.find(item => item.value === v)
return option ? option.label : v
@ -481,24 +491,43 @@ export default {
this.form.serialNumber = d.serialNumber
this.loadSubDeviceOptions(d)
} else {
this.form.serialNumber = ''
this.subDeviceOptions = []
this.thingsModelOptions = []
}
this.loadThingsModel(deviceId)
},
onDeviceSelectVisible(visible) {
if (visible && this.deviceOptions.length === 0) {
this.searchDevice('')
}
},
searchDevice(query) {
const keyword = (query || '').trim()
if (!keyword) return
this.deviceSearchLoading = true
listDevice({ pageNum: 1, pageSize: 50, ...this.buildDeviceSearchParams(keyword) }).then(res => {
this.deviceOptions = res.rows; this.deviceSearchLoading = false
const base = { pageNum: 1, pageSize: 50, entrance: 1 }
const requests = []
if (!keyword) {
requests.push(listDeviceShort(base))
} else {
requests.push(listDeviceShort({ ...base, deviceName: keyword }))
requests.push(listDeviceShort({ ...base, serialNumber: keyword }))
}
Promise.all(requests.map(req => req.catch(() => ({ rows: [] })))).then(results => {
this.deviceOptions = this.mergeDeviceOptions(results.flatMap(res => res.rows || []))
}).finally(() => {
this.deviceSearchLoading = false
})
},
buildDeviceSearchParams(query) {
const keyword = (query || '').trim()
if (keyword.length === 12) {
return { serialNumber: keyword }
}
return { deviceName: keyword }
mergeDeviceOptions(rows) {
const map = new Map()
;(rows || []).forEach(item => {
if (!item || !item.deviceId || item.gwDevCode) return
if (!map.has(item.deviceId)) {
map.set(item.deviceId, item)
}
})
return Array.from(map.values())
},
loadDeviceContext(deviceId) {
if (!deviceId) return

View File

@ -26,14 +26,20 @@
</el-card>
<el-card>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
<el-row :gutter="10" style="margin-bottom:12px">
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="selectedIds.length === 0" @click="handleBatchDelete" v-hasPermi="['platform:pushLog:remove']">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-tabs v-model="activeTab" @tab-click="onTabChange">
<el-tab-pane label="数据推送" name="1" />
<el-tab-pane label="操作日志" name="2" />
</el-tabs>
<el-table v-loading="loading" :data="list">
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="时间" prop="createTime" width="160" />
<el-table-column label="平台" width="120">
<template slot-scope="scope">{{ platformName(scope.row.platformId) }}</template>
@ -99,7 +105,7 @@
</template>
<script>
import { listPlatformPushLog, getPlatformPushLog, retryPlatformPushLog } from '@/api/platform/pushLog'
import { listPlatformPushLog, getPlatformPushLog, retryPlatformPushLog, delPlatformPushLog } from '@/api/platform/pushLog'
import { listPlatformInstance } from '@/api/platform/instance'
export default {
@ -111,6 +117,7 @@ export default {
total: 0,
list: [],
platformList: [],
selectedIds: [],
activeTab: '1',
retryingId: null,
detailOpen: false,
@ -130,6 +137,9 @@ export default {
this.list = res.rows; this.total = res.total; this.loading = false
})
},
handleSelectionChange(selection) {
this.selectedIds = (selection || []).map(item => item.id).filter(Boolean)
},
platformName(id) {
const p = this.platformList.find(x => x.id === id)
return p ? p.name : id
@ -159,6 +169,16 @@ export default {
this.retryingId = null
})
},
handleBatchDelete() {
if (this.selectedIds.length === 0) return
this.$confirm(`确认删除选中的 ${this.selectedIds.length} 条日志?`).then(() => {
delPlatformPushLog(this.selectedIds.join(',')).then(() => {
this.$message.success('删除成功')
this.selectedIds = []
this.getList()
})
})
},
formatJson(str) {
if (!str) return ''
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }