国标通道编辑支持仅保存修改的字段

This commit is contained in:
lin
2025-11-06 15:50:42 +08:00
parent f89dee6393
commit 01e72407ac
10 changed files with 198 additions and 66 deletions

View File

@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
@@ -33,6 +34,7 @@ import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import java.beans.PropertyDescriptor;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
@@ -86,13 +88,26 @@ public class ChannelController {
@Operation(summary = "更新通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
@PostMapping("/update")
public void update(@RequestBody CommonGBChannel channel){
BeanWrapperImpl wrapper = new BeanWrapperImpl(channel);
int count = 0;
for (PropertyDescriptor pd : wrapper.getPropertyDescriptors()) {
String name = pd.getName();
if ("class".equals(name)) continue;
if (pd.getReadMethod() == null) continue;
Object val = wrapper.getPropertyValue(name);
if (val != null) count++;
}
Assert.isTrue(count > 1, "未进行任何修改");
channelService.update(channel);
}
@Operation(summary = "重置国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
@PostMapping("/reset")
public void reset(Integer id){
channelService.reset(id);
public void reset(ResetParam param){
Assert.notNull(param.getId(), "通道ID不能为空");
Assert.notEmpty(param.getChanelFields(), "待重置字段不可以空");
channelService.reset(param.getId(), param.getChanelFields());
}
@Operation(summary = "增加通道", security = @SecurityRequirement(name = JwtUtils.HEADER))

View File

@@ -0,0 +1,15 @@
package com.genersoft.iot.vmp.gb28181.controller.bean;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ResetParam {
private Integer id;
private List<String> chanelFields;
}

View File

@@ -117,40 +117,40 @@ public interface CommonGBChannelMapper {
@Update(value = {" <script>" +
"UPDATE wvp_device_channel " +
"SET update_time=#{updateTime}" +
", gb_device_id = #{gbDeviceId}" +
", gb_name = #{gbName}" +
", gb_manufacturer = #{gbManufacturer}" +
", gb_model = #{gbModel}" +
", gb_owner = #{gbOwner}" +
", gb_civil_code = #{gbCivilCode}" +
", gb_block = #{gbBlock}" +
", gb_address = #{gbAddress}" +
", gb_parental = #{gbParental}" +
", gb_parent_id = #{gbParentId}" +
", gb_safety_way = #{gbSafetyWay}" +
", gb_register_way = #{gbRegisterWay}" +
", gb_cert_num = #{gbCertNum}" +
", gb_certifiable = #{gbCertifiable}" +
", gb_err_code = #{gbErrCode}" +
", gb_end_time = #{gbEndTime}" +
", gb_ip_address = #{gbIpAddress}" +
", gb_port = #{gbPort}" +
", gb_password = #{gbPassword}" +
", gb_status = #{gbStatus}" +
", gb_longitude = #{gbLongitude}" +
", gb_latitude = #{gbLatitude}" +
", gb_ptz_type = #{gbPtzType}" +
", gb_position_type = #{gbPositionType}" +
", gb_room_type = #{gbRoomType}" +
", gb_use_type = #{gbUseType}" +
", gb_supply_light_type = #{gbSupplyLightType}" +
", gb_direction_type = #{gbDirectionType}" +
", gb_resolution = #{gbResolution}" +
", gb_business_group_id = #{gbBusinessGroupId}" +
", gb_download_speed = #{gbDownloadSpeed}" +
", gb_svc_space_support_mod = #{gbSvcSpaceSupportMod}" +
", gb_svc_time_support_mode = #{gbSvcTimeSupportMode}" +
", enable_broadcast = #{enableBroadcast}" +
"<if test='gbDeviceId != null' > , gb_device_id = #{gbDeviceId}</if> " +
"<if test='gbName != null' > , gb_name = #{gbName}</if> " +
"<if test='gbManufacturer != null' > , gb_manufacturer = #{gbManufacturer}</if> " +
"<if test='gbModel != null' > , gb_model = #{gbModel}</if> " +
"<if test='gbOwner != null' > , gb_owner = #{gbOwner}</if> " +
"<if test='gbCivilCode != null' > , gb_civil_code = #{gbCivilCode}</if> " +
"<if test='gbBlock != null' > , gb_block = #{gbBlock}</if> " +
"<if test='gbAddress != null' > , gb_address = #{gbAddress}</if> " +
"<if test='gbParental != null' > , gb_parental = #{gbParental}</if> " +
"<if test='gbParentId != null' > , gb_parent_id = #{gbParentId}</if> " +
"<if test='gbSafetyWay != null' > , gb_safety_way = #{gbSafetyWay}</if> " +
"<if test='gbRegisterWay != null' > , gb_register_way = #{gbRegisterWay}</if> " +
"<if test='gbCertNum != null' > , gb_cert_num = #{gbCertNum}</if> " +
"<if test='gbCertifiable != null' > , gb_certifiable = #{gbCertifiable}</if> " +
"<if test='gbErrCode != null' > , gb_err_code = #{gbErrCode}</if> " +
"<if test='gbEndTime != null' > , gb_end_time = #{gbEndTime}</if> " +
"<if test='gbIpAddress != null' > , gb_ip_address = #{gbIpAddress}</if> " +
"<if test='gbPort != null' > , gb_port = #{gbPort}</if> " +
"<if test='gbPassword != null' > , gb_password = #{gbPassword}</if> " +
"<if test='gbStatus != null' > , gb_status = #{gbStatus}</if> " +
"<if test='gbLongitude != null' > , gb_longitude = #{gbLongitude}</if> " +
"<if test='gbLatitude != null' > , gb_latitude = #{gbLatitude}</if> " +
"<if test='gbPtzType != null' > , gb_ptz_type = #{gbPtzType}</if> " +
"<if test='gbPositionType != null' > , gb_position_type = #{gbPositionType}</if> " +
"<if test='gbRoomType != null' > , gb_room_type = #{gbRoomType}</if> " +
"<if test='gbUseType != null' > , gb_use_type = #{gbUseType}</if> " +
"<if test='gbSupplyLightType != null' > , gb_supply_light_type = #{gbSupplyLightType}</if> " +
"<if test='gbDirectionType != null' > , gb_direction_type = #{gbDirectionType}</if> " +
"<if test='gbResolution != null' > , gb_resolution = #{gbResolution}</if> " +
"<if test='gbBusinessGroupId != null' > , gb_business_group_id = #{gbBusinessGroupId}</if> " +
"<if test='gbDownloadSpeed != null' > , gb_download_speed = #{gbDownloadSpeed}</if> " +
"<if test='gbSvcSpaceSupportMod != null' > , gb_svc_space_support_mod = #{gbSvcSpaceSupportMod}</if> " +
"<if test='gbSvcTimeSupportMode != null' > , gb_svc_time_support_mode = #{gbSvcTimeSupportMode}</if> " +
"<if test='enableBroadcast != null' > , enable_broadcast = #{enableBroadcast}</if> " +
" WHERE id = #{gbId}"+
" </script>"})
int update(CommonGBChannel commonGBChannel);
@@ -236,17 +236,13 @@ public interface CommonGBChannelMapper {
@Update(value = {" <script>" +
" UPDATE wvp_device_channel " +
" SET update_time=#{updateTime}, gb_device_id = null, gb_name = null, gb_manufacturer = null," +
" gb_model = null, gb_owner = null, gb_block = null, gb_address = null," +
" gb_parental = null, gb_parent_id = null, gb_safety_way = null, gb_register_way = null, gb_cert_num = null," +
" gb_certifiable = null, gb_err_code = null, gb_end_time = null, gb_secrecy = null, gb_ip_address = null, " +
" gb_port = null, gb_password = null, gb_status = null, gb_longitude = null, gb_latitude = null, " +
" gb_ptz_type = null, gb_position_type = null, gb_room_type = null, gb_use_type = null, gb_supply_light_type = null, " +
" gb_direction_type = null, gb_resolution = null, gb_business_group_id = null, gb_download_speed = null, gb_svc_space_support_mod = null, " +
" gb_svc_time_support_mode = null" +
" WHERE id = #{id} and data_type = #{dataType} and data_device_id = #{dataDeviceId}"+
" SET update_time=#{updateTime}" +
"<foreach collection='fields' index='index' item='item' separator=';'> " +
", #{item} = null" +
"</foreach> " +
" WHERE id = #{id}"+
" </script>"})
void reset(@Param("id") int id, @Param("dataType") Integer dataType, @Param("dataDeviceId") int dataDeviceId, @Param("updateTime") String updateTime);
void reset(@Param("id") int id, List<String> fields, @Param("updateTime") String updateTime);
@SelectProvider(type = ChannelProvider.class, method = "queryByIds")
@@ -421,7 +417,7 @@ public interface CommonGBChannelMapper {
", gb_ip_address=#{item.gbIpAddress}" +
", gb_port=#{item.gbPort}" +
", gb_password=#{item.gbPassword}" +
", gb_status=#{item.gbStatus}" +
"<if test='item.gbStatus != null' > , gb_status=#{item.gbStatus}</if> " +
", gb_longitude=#{item.gbLongitude}" +
", gb_latitude=#{item.gbLatitude}" +
", gb_ptz_type=#{item.gbPtzType}" +

View File

@@ -309,12 +309,12 @@ public interface GroupMapper {
@Select("<script>" +
" SELECT " +
" ANY_VALUE(coalesce( wdc.gb_parent_id, wdc.parent_id)) as deviceId," +
" MIN(coalesce( wdc.gb_parent_id, wdc.parent_id)) as deviceId," +
" COUNT(*) AS allCount," +
" SUM(CASE WHEN coalesce( wdc.gb_status, wdc.status) = 'ON' THEN 1 ELSE 0 END) AS onlineCount" +
" FROM " +
" wvp_device_channel wdc " +
" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null || ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " +
" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " +
" AND coalesce( wdc.gb_parent_id, wdc.parent_id) in " +
" <foreach collection='groupList' item='item' open='(' separator=',' close=')' > #{item.deviceId}</foreach>" +
" GROUP BY coalesce(wdc.gb_parent_id, wdc.parent_id)" +

View File

@@ -42,7 +42,7 @@ public interface IGbChannelService {
List<NetworkIdentificationType> getNetworkIdentificationTypeList();
void reset(int id);
void reset(int id, List<String> chanelFields);
PageInfo<CommonGBChannel> queryListByCivilCode(int page, int count, String query, Boolean online, Integer channelType, String civilCode);

View File

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.gb28181.service.impl;
import com.alibaba.excel.support.cglib.beans.BeanMap;
import com.alibaba.excel.util.BeanMapUtils;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.common.enums.ChannelDataType;
import com.genersoft.iot.vmp.conf.DynamicTask;
@@ -25,11 +26,13 @@ import com.genersoft.iot.vmp.utils.TileUtils;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.base.CaseFormat;
import lombok.extern.slf4j.Slf4j;
import no.ecc.vectortile.VectorTileEncoder;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@@ -165,26 +168,28 @@ public class GbChannelServiceImpl implements IGbChannelService {
CommonGBChannel oldChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId());
commonGBChannel.setUpdateTime(DateUtil.getNow());
int result = commonGBChannelMapper.update(commonGBChannel);
if (result > 0) {
try {
CommonGBChannel newChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId());
// 发送通知
eventPublisher.channelEventPublishForUpdate(commonGBChannel, oldChannel);
eventPublisher.channelEventPublishForUpdate(newChannel, oldChannel);
if (commonGBChannel.getGbLongitude() != null && !Objects.equals(oldChannel.getGbLongitude(), commonGBChannel.getGbLongitude())
&& commonGBChannel.getGbLatitude() != null && !Objects.equals(oldChannel.getGbLatitude(), commonGBChannel.getGbLatitude())) {
if (newChannel.getGbLongitude() != null && !Objects.equals(oldChannel.getGbLongitude(), newChannel.getGbLongitude())
&& newChannel.getGbLatitude() != null && !Objects.equals(oldChannel.getGbLatitude(), newChannel.getGbLatitude())) {
MobilePosition mobilePosition = new MobilePosition();
mobilePosition.setDeviceId(commonGBChannel.getGbDeviceId());
mobilePosition.setChannelId(commonGBChannel.getGbId());
mobilePosition.setDeviceName(commonGBChannel.getGbName());
mobilePosition.setDeviceId(newChannel.getGbDeviceId());
mobilePosition.setChannelId(newChannel.getGbId());
mobilePosition.setDeviceName(newChannel.getGbName());
mobilePosition.setCreateTime(DateUtil.getNow());
mobilePosition.setTime(DateUtil.getNow());
mobilePosition.setLongitude(commonGBChannel.getGbLongitude());
mobilePosition.setLatitude(commonGBChannel.getGbLatitude());
mobilePosition.setLongitude(newChannel.getGbLongitude());
mobilePosition.setLatitude(newChannel.getGbLatitude());
eventPublisher.mobilePositionEventPublish(mobilePosition);
}
} catch (Exception e) {
log.warn("[更新通道通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e);
log.warn("[更新通道通知] 发送失败,{}", JSONObject.toJSONString(commonGBChannel), e);
}
}
return result;
@@ -422,8 +427,9 @@ public class GbChannelServiceImpl implements IGbChannelService {
}
@Override
public void reset(int id) {
public void reset(int id, List<String> chanelFields) {
log.info("[重置国标通道] id: {}", id);
Assert.notEmpty(chanelFields, "待重置字段为空");
CommonGBChannel channel = getOne(id);
if (channel == null) {
log.warn("[重置国标通道] 未找到对应Id的通道: id: {}", id);
@@ -433,8 +439,18 @@ public class GbChannelServiceImpl implements IGbChannelService {
log.warn("[重置国标通道] 非国标下级通道无法重置: id: {}", id);
throw new ControllerException(ErrorCode.ERROR100.getCode(), "非国标下级通道无法重置");
}
List<String> dbFields = new ArrayList<>();
for (String chanelField : chanelFields) {
BeanWrapperImpl wrapper = new BeanWrapperImpl(channel);
if (wrapper.isReadableProperty(chanelField)) {
dbFields.add(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, chanelField));
}
}
Assert.notEmpty(dbFields, "待重置字段为空");
// 这个多加一个参数,为了防止将非国标的通道通过此方法清空内容,导致意外发生
commonGBChannelMapper.reset(id, ChannelDataType.GB28181, channel.getDataDeviceId(), DateUtil.getNow());
commonGBChannelMapper.reset(id, dbFields, DateUtil.getNow());
CommonGBChannel channelNew = getOne(id);
// 发送通过更新通知
try {

View File

@@ -102,7 +102,9 @@ public class RedisPushStreamListMsgListener implements MessageListener {
streamPush.setUpdateTime(DateUtil.getNow());
streamPush.setGbDeviceId(pushStreamMessage.getGbId());
streamPush.setGbName(pushStreamMessage.getName());
streamPush.setGbStatus(pushStreamMessage.isStatus() ? "ON" : "OFF");
if (pushStreamMessage.getStatus() != null) {
streamPush.setGbStatus(pushStreamMessage.getStatus() ? "ON" : "OFF");
}
//存在就只修改 name和gbId
streamPushItemForUpdate.add(streamPush);
}

View File

@@ -9,7 +9,7 @@ public class RedisPushStreamMessage {
private String app;
private String stream;
private String name;
private boolean status;
private Boolean status;
// 终端所属的虚拟组织
private String groupGbId;
// 终端所属的虚拟组织别名 可选可作为地方同步组织结构到wvp时的关联关系
@@ -31,7 +31,9 @@ public class RedisPushStreamMessage {
push.setGbManufacturer(manufacturer);
push.setGbModel(model);
push.setGbPtzType(ptzType);
push.setGbStatus(status?"ON":"OFF");
if (status != null) {
push.setGbStatus(status?"ON":"OFF");
}
push.setEnableBroadcast(0);
return push;
}

76
web/src/utils/diff.js Normal file
View File

@@ -0,0 +1,76 @@
// utils/diff.js
// 返回 newObj 相对于 oldObj 的变化部分(新增/修改)。
// 可选 includeRemoved=true 时把被删除字段以 removedValue 标记返回。
// 使用示例: diff(oldObj, newObj) 或 diff(oldObj, newObj, { includeRemoved: true })
export function diff(oldObj, newObj, options = {}) {
const { includeRemoved = false, removedValue = null, comparator } = options
function isObject(v) {
return v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)
}
function isDate(v) {
return v instanceof Date
}
function equal(a, b) {
if (typeof comparator === 'function') return comparator(a, b)
if (isDate(a) && isDate(b)) return a.getTime() === b.getTime()
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false
return true
}
if (isObject(a) && isObject(b)) {
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length) return false
for (const k of aKeys) {
if (!Object.prototype.hasOwnProperty.call(b, k) || !equal(a[k], b[k])) return false
}
return true
}
return a === b
}
function inner(o, n) {
// Normalize undefined -> empty object for easier handling in object branch.
const oIsObj = isObject(o)
const nIsObj = isObject(n)
if (equal(o, n)) return undefined
if (oIsObj && nIsObj) {
const result = {}
const keys = new Set([...Object.keys(o || {}), ...Object.keys(n || {})])
for (const k of keys) {
console.log(k)
const hasO = Object.prototype.hasOwnProperty.call(o || {}, k)
const hasN = Object.prototype.hasOwnProperty.call(n || {}, k)
if (hasN) {
const sub = inner(hasO ? o[k] : undefined, n[k])
if (sub !== undefined) result[k] = sub
} else if (hasO && includeRemoved) {
// key existed before but removed now
result[k] = removedValue
}
}
return Object.keys(result).length ? result : undefined
}
if (Array.isArray(o) && Array.isArray(n)) {
return equal(o, n) ? undefined : n
}
if (isDate(n)) return n
// Different primitive or type change -> return new value
return n
}
const res = inner(oldObj ?? {}, newObj ?? {})
return res === undefined ? {} : res
}
export default diff

View File

@@ -248,6 +248,7 @@
import channelCode from './../dialog/channelCode'
import ChooseCivilCode from '../dialog/chooseCivilCode.vue'
import ChooseGroup from '../dialog/chooseGroup.vue'
import diff from '../../utils/diff'
export default {
name: 'CommonChannelEdit',
@@ -288,7 +289,7 @@ export default {
if (!this.dataForm.gbDeviceId) {
this.dataForm.gbDeviceId = ''
}
this.form = this.dataForm
this.form = window.structuredClone(this.dataForm)
this.getPaths()
}
},
@@ -309,8 +310,16 @@ export default {
this.form.gbDownloadSpeed = this.form.gbDownloadSpeedArray.join('/')
}
this.form.enableBroadcast = this.form.enableBroadcastForBool ? 1 : 0
// 判断哪些字段变化
let diffData = diff(this.dataForm, this.form)
diffData['gbId'] = this.form.gbId
console.log(diffData)
console.log(this.dataForm)
console.log(this.form)
if (this.form.gbId) {
this.$store.dispatch('commonChanel/update', this.form)
this.$store.dispatch('commonChanel/update', diffData)
.then(data => {
this.$message.success({
showClose: true,
@@ -368,6 +377,7 @@ export default {
if (data.gbDownloadSpeed) {
data.gbDownloadSpeedArray = data.gbDownloadSpeed.split('/')
}
this.dataForm = window.structuredClone(data)
this.form = data
this.$set(this.form, 'enableBroadcastForBool', this.form.enableBroadcast === 1)
this.getPaths()