Bläddra i källkod

Merge remote-tracking branch 'origin/master'

zengy 1 vecka sedan
förälder
incheckning
36c84f78dd

+ 8 - 8
pipe-network-service/sql/early_warning_system.sql

@@ -7,18 +7,16 @@ DROP TABLE IF EXISTS `early_warning`;
 CREATE TABLE `early_warning` (
   `warning_id` varchar(64) NOT NULL COMMENT '预警ID(主键)',
   `warning_no` varchar(64) NOT NULL COMMENT '预警编号',
-  `warning_name` varchar(256) NOT NULL COMMENT '预警名称',
   `warning_type` varchar(64) NOT NULL COMMENT '预警类型',
   `warning_level` varchar(32) NOT NULL COMMENT '预警级别(1-最高 2-次高 3-一般 4-常态;驾驶舱展示色为1红2黄3蓝4绿)',
   `warning_special` varchar(128) DEFAULT NULL COMMENT '预警专项',
   `location` varchar(512) DEFAULT NULL COMMENT '预警位置',
-  `longitude` decimal(15,10) DEFAULT NULL COMMENT '经度',
-  `latitude` decimal(15,10) DEFAULT NULL COMMENT '纬度',
   `ownership_unit` varchar(256) DEFAULT NULL COMMENT '权属单位',
   `publisher` varchar(128) DEFAULT NULL COMMENT '发布人',
   `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
   `handler` varchar(128) DEFAULT NULL COMMENT '处置人',
-  `status` varchar(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态(DRAFT-草稿 PENDING-待办 PROCESSING-处置中 RELEASED-已发布 HANDLED-已处置 CLOSED-已解除)',
+  `reviewer` varchar(128) DEFAULT NULL COMMENT '核查人(默认发布人)',
+  `status` varchar(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态(DRAFT-草稿 PENDING-待办 RECEIVED-已接收 PROCESSING-处置中 HANDLED-已处置 CLOSED-已解除)',
   `warning_content` text COMMENT '预警内容描述',
   `process_instance_id` varchar(128) DEFAULT NULL COMMENT '工作流实例ID',
   `create_by` varchar(128) DEFAULT NULL COMMENT '创建人',
@@ -51,6 +49,8 @@ DROP TABLE IF EXISTS `warning_attachment`;
 CREATE TABLE `warning_attachment` (
   `attachment_id` varchar(64) NOT NULL COMMENT '附件ID',
   `warning_id` varchar(64) NOT NULL COMMENT '预警ID',
+  `disposal_id` varchar(64) DEFAULT NULL COMMENT '处置记录ID,报告附件为空',
+  `attachment_stage` varchar(32) DEFAULT 'REPORT' COMMENT '附件阶段(REPORT/PROCESS)',
   `attachment_name` varchar(256) NOT NULL COMMENT '附件名称',
   `attachment_url` varchar(512) NOT NULL COMMENT '附件地址',
   `attachment_type` varchar(255) DEFAULT NULL COMMENT '附件类型(MIME,标准 MIME 可达 70+ 字符)',
@@ -89,6 +89,8 @@ CREATE TABLE `warning_supervision` (
   `supervision_content` text NOT NULL COMMENT '督办意见',
   `notification_type` varchar(32) NOT NULL COMMENT '通知方式',
   `notification_content` text COMMENT '通知内容',
+  `notification_status` varchar(16) DEFAULT NULL COMMENT '通知结果:PENDING/SENT/FAILED',
+  `notification_error` varchar(256) DEFAULT NULL COMMENT '失败原因',
   `attachment_url` varchar(512) DEFAULT NULL COMMENT '附件地址',
   `create_by` varchar(128) DEFAULT NULL COMMENT '创建人',
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -116,7 +118,8 @@ CREATE TABLE `warning_todo` (
   `warning_id` varchar(64) NOT NULL COMMENT '预警ID',
   `user_id` varchar(64) NOT NULL COMMENT '用户ID',
   `user_name` varchar(128) DEFAULT NULL COMMENT '用户姓名',
-  `todo_type` varchar(32) NOT NULL COMMENT '待办类型(TODO-待办 DONE-已办)',
+  `todo_type` varchar(32) NOT NULL COMMENT '待办类型(TODO-待办 DONE-已办 NOTICE-系统通知)',
+  `action_type` varchar(32) DEFAULT NULL COMMENT '任务职责(HANDLE-处置 REVIEW-核查)',
   `read_status` varchar(32) DEFAULT 'UNREAD' COMMENT '阅读状态(UNREAD-未读 READ-已读)',
   `task_name` varchar(256) DEFAULT NULL COMMENT '任务名称',
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -163,12 +166,9 @@ CREATE TABLE `warning_archive` (
   `warning_id` varchar(64) NOT NULL COMMENT '预警ID',
   `archive_no` varchar(64) NOT NULL COMMENT '归档编号',
   `warning_no` varchar(64) NOT NULL COMMENT '预警编号',
-  `warning_name` varchar(256) NOT NULL COMMENT '预警名称',
   `warning_type` varchar(64) NOT NULL COMMENT '预警类型',
   `warning_level` varchar(32) NOT NULL COMMENT '预警级别',
   `location` varchar(512) DEFAULT NULL COMMENT '预警位置',
-  `longitude` decimal(15,10) DEFAULT NULL COMMENT '经度',
-  `latitude` decimal(15,10) DEFAULT NULL COMMENT '纬度',
   `ownership_unit` varchar(256) DEFAULT NULL COMMENT '权属单位',
   `publisher` varchar(128) DEFAULT NULL COMMENT '发布人',
   `publish_time` datetime DEFAULT NULL COMMENT '发布时间',

+ 42 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageVideoController.java

@@ -0,0 +1,42 @@
+package com.zksy.web.controller.drainage;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.drainage.domain.DrainageVideoResource;
+import com.zksy.drainage.service.DrainageVideoService;
+import lombok.Data;
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/drainage/video")
+public class DrainageVideoController {
+    private final DrainageVideoService service;
+    public DrainageVideoController(DrainageVideoService service) {this.service=service;}
+    @PreAuthorize("@ss.hasPermi('drainage:video:list')")
+    @GetMapping("/resources")
+    public AjaxResult list(@RequestParam(defaultValue="1") int pageNum,@RequestParam(defaultValue="10") int pageSize,
+        @RequestParam(required=false) String query,@RequestParam(required=false) Integer district,
+        @RequestParam(required=false) String pointId,@RequestParam(required=false) Boolean unbound,
+        @RequestParam(required=false) Boolean favoriteOnly,@RequestParam(required=false) String sourceType) {
+        Page<DrainageVideoResource> page=service.page(pageNum,pageSize,query,district,pointId,unbound,favoriteOnly,sourceType);
+        AjaxResult result=AjaxResult.success();result.put("rows",page.getRecords());result.put("total",page.getTotal());return result;
+    }
+    @PreAuthorize("@ss.hasPermi('drainage:video:list')") @GetMapping("/tree")
+    public AjaxResult tree() {return AjaxResult.success(service.tree());}
+    @PreAuthorize("@ss.hasPermi('drainage:video:list')") @GetMapping("/map")
+    public AjaxResult map() {return AjaxResult.success(service.all());}
+    @PreAuthorize("@ss.hasPermi('drainage:video:list')") @GetMapping("/points")
+    public AjaxResult points() {return AjaxResult.success(service.points());}
+    @PreAuthorize("@ss.hasPermi('drainage:video:bind')") @PutMapping("/devices/{id}/waterlogging")
+    @Log(title="排水视频点位关联",businessType=BusinessType.UPDATE)
+    public AjaxResult associate(@PathVariable String id,@RequestBody Association input) {service.associate(id,input.getPointId());return AjaxResult.success();}
+    @PreAuthorize("@ss.hasPermi('drainage:video:favorite')") @PutMapping("/devices/{id}/favorite")
+    public AjaxResult favorite(@PathVariable String id,@Valid @RequestBody Favorite input) {service.favorite(id,input.getEnabled());return AjaxResult.success();}
+    @Data public static class Association {private String pointId;}
+    @Data public static class Favorite {@NotNull private Boolean enabled;}
+}

+ 22 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainageVideoResource.java

@@ -0,0 +1,22 @@
+package com.zksy.drainage.domain;
+
+import java.math.BigDecimal;
+import lombok.Data;
+
+/** 排水视频展示视图。不返回源地址、人员联系方式或接入凭据。 */
+@Data
+public class DrainageVideoResource {
+    private String videoId;
+    private String deviceCode;
+    private String deviceName;
+    private Integer district;
+    private String districtName;
+    private String sourceType;
+    private String typeName;
+    private String location;
+    private BigDecimal longitude;
+    private BigDecimal latitude;
+    private String pointId;
+    private String pointName;
+    private Boolean favorite;
+}

+ 11 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/mapper/DrainageBindingLockMapper.java

@@ -0,0 +1,11 @@
+package com.zksy.drainage.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+/** 所有排水设备关联入口共用事务锁,避免旧接口与视频接口并发覆盖。 */
+@Mapper
+public interface DrainageBindingLockMapper {
+    @Select("SELECT id FROM app_user.drainage_equipment_binding_lock WHERE id=1 FOR UPDATE")
+    Integer lock();
+}

+ 46 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/mapper/DrainageVideoMapper.java

@@ -0,0 +1,46 @@
+package com.zksy.drainage.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.drainage.domain.DrainageVideoResource;
+import java.util.List;
+import java.util.Map;
+import org.apache.ibatis.annotations.*;
+
+@Mapper
+public interface DrainageVideoMapper {
+    String VIEW = "SELECT e.equipment_id AS video_id,e.equipment_code AS device_code,e.equipment_name AS device_name," +
+        "e.district,e.equipment_location AS location,e.longitude,e.latitude,v.source_type," +
+        "w.id AS point_id,w.name AS point_name,(f.user_id IS NOT NULL) AS favorite " +
+        "FROM app_user.equipment_base e JOIN app_user.equipment_video_binding v ON v.equipment_id=e.equipment_id " +
+        "LEFT JOIN app_user.psgw_waterlogging_point w ON STRPOS(CONCAT(',',REPLACE(COALESCE(w.equipment_id,''),' ',''),','),CONCAT(',',e.equipment_id,','))>0 " +
+        "LEFT JOIN app_user.drainage_video_favorite f ON f.equipment_id=e.equipment_id AND f.user_id=#{userId} ";
+    String FILTER = "WHERE e.equipment_type_id=20 AND e.del_flag='0' " +
+        "<if test='query != null and query != &quot;&quot;'>AND (e.equipment_name LIKE CONCAT('%',#{query},'%') OR e.equipment_code LIKE CONCAT('%',#{query},'%'))</if> " +
+        "<if test='district != null'>AND e.district=#{district}</if> " +
+        "<if test='pointId != null and pointId != &quot;&quot;'>AND w.id=#{pointId}</if> " +
+        "<if test='unbound == true'>AND w.id IS NULL</if> " +
+        "<if test='unbound == false'>AND w.id IS NOT NULL</if> " +
+        "<if test='favoriteOnly == true'>AND f.user_id IS NOT NULL</if> " +
+        "<if test='sourceType != null and sourceType != &quot;&quot;'>AND v.source_type=#{sourceType}</if> " +
+        "ORDER BY e.create_time DESC,e.equipment_id";
+
+    @Select("<script>" + VIEW + FILTER + "</script>")
+    Page<DrainageVideoResource> page(Page<DrainageVideoResource> page, @Param("userId") Long userId,
+        @Param("query") String query,@Param("district") Integer district,@Param("pointId") String pointId,
+        @Param("unbound") Boolean unbound,@Param("favoriteOnly") Boolean favoriteOnly,@Param("sourceType") String sourceType);
+
+    @Select(VIEW + "WHERE e.equipment_type_id=20 AND e.del_flag='0' ORDER BY e.district,e.equipment_id")
+    List<DrainageVideoResource> all(@Param("userId") Long userId);
+
+    @Select("SELECT id,name,area FROM app_user.psgw_waterlogging_point ORDER BY name,id")
+    List<Map<String,Object>> points();
+
+    @Insert("INSERT INTO app_user.drainage_video_favorite(user_id,equipment_id) VALUES(#{userId},#{videoId}) ON CONFLICT(user_id,equipment_id) DO NOTHING")
+    int favorite(@Param("userId") Long userId,@Param("videoId") String videoId);
+
+    @Delete("DELETE FROM app_user.drainage_video_favorite WHERE user_id=#{userId} AND equipment_id=#{videoId}")
+    int unfavorite(@Param("userId") Long userId,@Param("videoId") String videoId);
+
+    @Update("UPDATE app_user.psgw_waterlogging_point SET equipment_id=#{equipmentIds},update_time=CURRENT_TIMESTAMP WHERE id=#{pointId}")
+    int updatePoint(@Param("pointId") String pointId,@Param("equipmentIds") String equipmentIds);
+}

+ 100 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageVideoService.java

@@ -0,0 +1,100 @@
+package com.zksy.drainage.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.enums.DistrictEnum;
+import com.zksy.base.video.VideoDeviceService;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.drainage.domain.*;
+import com.zksy.drainage.mapper.*;
+import java.util.*;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class DrainageVideoService {
+    private final DrainageVideoMapper videos;
+    private final WaterloggingPointMapper points;
+    private final DrainageBindingLockMapper guard;
+    private final DrainageEquipmentBindingService binding;
+    private final VideoDeviceService devices;
+    public DrainageVideoService(DrainageVideoMapper videos,WaterloggingPointMapper points,DrainageBindingLockMapper guard,
+                               DrainageEquipmentBindingService binding,VideoDeviceService devices) {
+        this.videos=videos;this.points=points;this.guard=guard;this.binding=binding;this.devices=devices;
+    }
+    public Page<DrainageVideoResource> page(int pageNum,int pageSize,String query,Integer district,String pointId,
+                                          Boolean unbound,Boolean favoriteOnly,String sourceType) {
+        if(district!=null&&!DistrictEnum.isValid(district))throw new ServiceException("无效设备片区");
+        Page<DrainageVideoResource> page=videos.page(new Page<>(Math.max(1,pageNum),Math.max(1,Math.min(100,pageSize))),
+            SecurityUtils.getUserId(),query,district,pointId,unbound,favoriteOnly,sourceType);
+        page.getRecords().forEach(this::describe);return page;
+    }
+    public List<DrainageVideoResource> all() {
+        List<DrainageVideoResource> rows=videos.all(SecurityUtils.getUserId());rows.forEach(this::describe);return rows;
+    }
+    private void describe(DrainageVideoResource row) {
+        row.setDistrictName(Optional.ofNullable(DistrictEnum.getDescByCode(row.getDistrict())).orElse("未设置片区"));
+        row.setTypeName("GB28181".equals(row.getSourceType())?"国标视频":"地址视频");
+    }
+    public List<Map<String,Object>> points() {return videos.points();}
+    public List<Map<String,Object>> tree() {
+        List<DrainageVideoResource> rows=all();
+        List<Map<String,Object>> roots=new ArrayList<>();
+        Map<String,Object> favorites=node("favorites","我的收藏",rows.stream().filter(r->Boolean.TRUE.equals(r.getFavorite())).collect(Collectors.toList()));
+        favorites.put("children",leaves("favorites",rows.stream().filter(r->Boolean.TRUE.equals(r.getFavorite())).collect(Collectors.toList())));
+        favorites.put("favoriteOnly",true);roots.add(favorites);
+        Map<String,List<DrainageVideoResource>> districts=rows.stream().collect(Collectors.groupingBy(
+            r->r.getDistrict()==null?"unknown":r.getDistrict().toString(),LinkedHashMap::new,Collectors.toList()));
+        districts.forEach((key,list)->{
+            Map<String,Object> root=node("district-"+key,list.get(0).getDistrictName(),list);root.put("district",list.get(0).getDistrict());
+            Map<String,List<DrainageVideoResource>> groups=list.stream().collect(Collectors.groupingBy(
+                r->r.getPointId()==null?"unbound":r.getPointId(),LinkedHashMap::new,Collectors.toList()));
+            List<Map<String,Object>> children=new ArrayList<>();
+            groups.forEach((pointId,items)->{
+                Map<String,Object> child=node("point-"+key+"-"+pointId,items.get(0).getPointName()==null?"未关联易积水点":items.get(0).getPointName(),items);
+                child.put("pointId",items.get(0).getPointId());child.put("district",list.get(0).getDistrict());child.put("unbound","unbound".equals(pointId));
+                child.put("children",leaves(child.get("id").toString(),items));
+                children.add(child);
+            });root.put("children",children);roots.add(root);
+        });return roots;
+    }
+    private Map<String,Object> node(String id,String label,List<DrainageVideoResource> rows) {
+        Map<String,Object> result=new LinkedHashMap<>();result.put("id",id);result.put("label",label);result.put("count",rows.size());
+        result.put("videoIds",rows.stream().map(DrainageVideoResource::getVideoId).distinct().collect(Collectors.toList()));return result;
+    }
+    private List<Map<String,Object>> leaves(String parent,List<DrainageVideoResource> rows) {
+        return rows.stream().map(row->node("video-"+parent+"-"+row.getVideoId(),row.getDeviceName(),Collections.singletonList(row))).collect(Collectors.toList());
+    }
+    @Transactional
+    public void associate(String videoId,String pointId) {
+        if(guard.lock()==null)throw new ServiceException("设备关联事务锁未初始化,请执行迁移");
+        devices.one(videoId);
+        List<WaterloggingPoint> all=points.selectList(null);
+        WaterloggingPoint target=null;
+        if(pointId!=null&&!pointId.isBlank()) {
+            target=all.stream().filter(p->pointId.equals(p.getId())).findFirst().orElseThrow(()->new ServiceException("易积水点不存在"));
+            for(WaterloggingPoint point:all) {
+                if(!pointId.equals(point.getId())&&ids(point.getEquipmentId()).contains(videoId))
+                    throw new ServiceException("视频已关联其他易积水点,请先解除关联");
+            }
+            Map<String,String> occupied=binding.findBoundEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,pointId);
+            if(occupied.containsKey(videoId))throw new ServiceException("视频已被其他位置关联:"+occupied.get(videoId));
+            Set<String> merged=ids(target.getEquipmentId());merged.add(videoId);
+            videos.updatePoint(pointId,String.join(",",merged));
+        } else {
+            for(WaterloggingPoint point:all) {
+                Set<String> existing=ids(point.getEquipmentId());
+                if(existing.remove(videoId))videos.updatePoint(point.getId(),String.join(",",existing));
+            }
+        }
+    }
+    @Transactional
+    public void favorite(String videoId,boolean enabled) {
+        devices.one(videoId);
+        if(enabled)videos.favorite(SecurityUtils.getUserId(),videoId);else videos.unfavorite(SecurityUtils.getUserId(),videoId);
+    }
+    static Set<String> ids(String value) {
+        return value==null?new LinkedHashSet<>():Arrays.stream(value.split(",")).map(String::trim).filter(s->!s.isEmpty()).collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+}

+ 7 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageEquipmentBindingServiceImpl.java

@@ -6,10 +6,7 @@ import com.zksy.drainage.domain.DrainagePartition;
 import com.zksy.drainage.domain.EngineeringFacility;
 import com.zksy.drainage.domain.PipeNetworkInfo;
 import com.zksy.drainage.domain.WaterloggingPoint;
-import com.zksy.drainage.mapper.DrainagePartitionMapper;
-import com.zksy.drainage.mapper.EngineeringFacilityMapper;
-import com.zksy.drainage.mapper.PipeNetworkInfoMapper;
-import com.zksy.drainage.mapper.WaterloggingPointMapper;
+import com.zksy.drainage.mapper.*;
 import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -26,6 +23,9 @@ import java.util.Set;
 @Service
 public class DrainageEquipmentBindingServiceImpl implements DrainageEquipmentBindingService {
 
+    @Autowired
+    private DrainageBindingLockMapper bindingLock;
+
     @Autowired
     private DrainagePartitionMapper drainagePartitionMapper;
 
@@ -75,10 +75,11 @@ public class DrainageEquipmentBindingServiceImpl implements DrainageEquipmentBin
         return result;
     }
 
-    /** 同一个服务实例串行完成检查和保存,防止单实例内并发抢占同一设备。 */
+    /** 数据库行锁覆盖检查、保存和事务提交,所有实例及点位编辑入口共用。 */
     @Override
     @Transactional
-    public synchronized boolean bindEquipment(String sourceType, String sourceId, List<String> equipmentIds) {
+    public boolean bindEquipment(String sourceType, String sourceId, List<String> equipmentIds) {
+        if (bindingLock.lock() == null) throw new ServiceException("设备关联事务锁未初始化,请执行迁移");
         validateSourceType(sourceType, false);
         if (sourceId == null || sourceId.trim().isEmpty()) {
             throw new ServiceException("关联对象ID不能为空");

+ 24 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/WaterloggingPointServiceImpl.java

@@ -27,6 +27,9 @@ public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointM
     @org.springframework.beans.factory.annotation.Autowired
     private DrainageEquipmentBindingService equipmentBindingService;
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private com.zksy.drainage.mapper.DrainageBindingLockMapper bindingLock;
+
     @Override
     public Page<WaterloggingPoint> findByPage(long pageNum, long pageSize, String name, String area) {
         Page<WaterloggingPoint> page = new Page<>(pageNum, pageSize);
@@ -44,22 +47,40 @@ public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointM
     @Override
     @Transactional
     public boolean saveWithCheck(WaterloggingPoint entity) {
-        return this.save(entity);
+        bindingLock.lock();
+        String equipmentIds = entity.getEquipmentId();
+        entity.setEquipmentId("");
+        if (!this.save(entity)) return false;
+        if (equipmentIds != null && !equipmentIds.isBlank())
+            equipmentBindingService.bindEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,
+                    entity.getId(), java.util.Arrays.asList(equipmentIds.split(",")));
+        return true;
     }
 
     @Override
     @Transactional
     public boolean updateWithCheck(WaterloggingPoint entity) {
+        bindingLock.lock();
         String id = entity.getId();
         if (this.getById(id) == null) {
             throw new ServiceException("易积水点不存在,无法修改:" + id);
         }
-        return this.updateById(entity);
+        String equipmentIds = entity.getEquipmentId();
+        entity.setEquipmentId(null);
+        if (!this.updateById(entity)) return false;
+        if (equipmentIds != null && !equipmentIds.isBlank())
+            equipmentBindingService.bindEquipment(DrainageEquipmentBindingService.TYPE_WATERLOGGING,
+                    id, java.util.Arrays.asList(equipmentIds.split(",")));
+        else if (equipmentIds != null)
+            this.baseMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WaterloggingPoint>()
+                    .eq(WaterloggingPoint::getId, id).set(WaterloggingPoint::getEquipmentId, ""));
+        return true;
     }
 
     @Override
     @Transactional
     public boolean removeWithCheck(String id) {
+        bindingLock.lock();
         if (this.getById(id) == null) {
             throw new ServiceException("易积水点不存在:" + id);
         }
@@ -69,6 +90,7 @@ public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointM
     @Override
     @Transactional
     public boolean removeBatchWithCheck(List<String> ids) {
+        bindingLock.lock();
         if (CollectionUtils.isEmpty(ids)) {
             throw new ServiceException("批量删除失败:ID列表不能为空");
         }

+ 4 - 0
video-service/pom.xml

@@ -20,6 +20,10 @@
     <dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-core</artifactId></dependency>
     <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.34</version><scope>provided</scope></dependency>
     <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
+    <!-- 与其他采集服务一致:bootstrap 配置、Nacos 配置中心和服务注册。 -->
+    <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency>
+    <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>
+    <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
   </dependencies>
   <build><finalName>video-service</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><mainClass>com.zksy.video.VideoApplication</mainClass></configuration></plugin></plugins></build>
 </project>

+ 1 - 6
video-service/src/main/resources/application.yml → video-service/src/main/resources/application-dev.yaml

@@ -1,13 +1,8 @@
+# dev 环境;Nacos video-service-dev.yaml 可覆盖本文件的启动配置。
 server:
   port: ${VIDEO_HTTP_PORT:8330}
   address: ${VIDEO_BIND_ADDRESS:127.0.0.1}
 spring:
-  config:
-    import: "optional:file:./video-service/.runtime/video-local.properties,optional:file:./.runtime/video-local.properties"
-  application:
-    name: video-service
-  main:
-    allow-circular-references: true
   datasource:
     dynamic:
       primary: master

+ 69 - 0
video-service/src/main/resources/application-druid.yaml

@@ -0,0 +1,69 @@
+# druid 环境;Nacos video-service-druid.yaml 可覆盖本文件的启动配置。
+server:
+  port: ${VIDEO_HTTP_PORT:8330}
+  address: ${VIDEO_BIND_ADDRESS:0.0.0.0}
+spring:
+  datasource:
+    dynamic:
+      primary: master
+      strict: true
+      datasource:
+        master:
+          driver-class-name: com.kingbase8.Driver
+          url: ${VIDEO_DB_URL}
+          username: ${VIDEO_DB_USER}
+          password: ${VIDEO_DB_PASSWORD}
+  redis:
+    host: ${VIDEO_REDIS_HOST:127.0.0.1}
+    port: ${VIDEO_REDIS_PORT:6379}
+    password: ${VIDEO_REDIS_PASSWORD:}
+    database: ${VIDEO_REDIS_DATABASE:6}
+  mvc:
+    async:
+      request-timeout: 95000
+mybatis:
+  mapper-locations: classpath*:mapper/**/*.xml
+  configuration:
+    map-underscore-to-camel-case: true
+pagehelper:
+  helper-dialect: postgresql
+  reasonable: true
+sip:
+  ip: ${VIDEO_SIP_IP}
+  show-ip: ${VIDEO_SIP_ADVERTISED_IP:${VIDEO_SIP_IP}}
+  port: ${VIDEO_SIP_PORT:8116}
+  domain: ${VIDEO_SIP_DOMAIN:4101050000}
+  id: ${VIDEO_SIP_ID:41010500002000000002}
+  password: ${VIDEO_SIP_PASSWORD}
+  alarm: false
+media:
+  id: ${VIDEO_MEDIA_ID:city_video_zlm}
+  ip: ${VIDEO_MEDIA_HOST:127.0.0.1}
+  hook-ip: ${VIDEO_HOOK_IP}
+  sdp-ip: ${VIDEO_RTP_IP:${VIDEO_SIP_IP}}
+  stream-ip: ${VIDEO_STREAM_HOST}
+  http-port: ${VIDEO_MEDIA_HTTP_PORT:8094}
+  rtsp-port: ${VIDEO_MEDIA_RTSP_PORT:8556}
+  rtmp-port: ${VIDEO_MEDIA_RTMP_PORT:1937}
+  secret: ${VIDEO_MEDIA_SECRET}
+  auto-config: true
+  rtp:
+    enable: true
+    port-range: ${VIDEO_RTP_PORT_RANGE:31000,31500}
+    send-port-range: ${VIDEO_RTP_SEND_PORT_RANGE:31501,32000}
+user-settings:
+  server-id: ${VIDEO_SERVER_ID:city-video-01}
+  play-timeout: 20000
+  interface-authentication: true
+  sync-channel-on-device-online: true
+  record-sip: false
+  record-push-live: false
+  stream-on-demand: true
+  sip-log: false
+video:
+  internal-token: ${VIDEO_INTERNAL_TOKEN}
+  hook-allowed-addresses: ${VIDEO_HOOK_ALLOWED_ADDRESSES}
+version:
+  version: 1.0.0
+  artifactId: video-service
+  description: 城市生命线国标视频服务

+ 69 - 0
video-service/src/main/resources/application-prod.yaml

@@ -0,0 +1,69 @@
+# prod 环境;Nacos video-service-prod.yaml 可覆盖本文件的启动配置。
+server:
+  port: ${VIDEO_HTTP_PORT:8330}
+  address: ${VIDEO_BIND_ADDRESS:0.0.0.0}
+spring:
+  datasource:
+    dynamic:
+      primary: master
+      strict: true
+      datasource:
+        master:
+          driver-class-name: com.kingbase8.Driver
+          url: ${VIDEO_DB_URL}
+          username: ${VIDEO_DB_USER}
+          password: ${VIDEO_DB_PASSWORD}
+  redis:
+    host: ${VIDEO_REDIS_HOST:127.0.0.1}
+    port: ${VIDEO_REDIS_PORT:6379}
+    password: ${VIDEO_REDIS_PASSWORD:}
+    database: ${VIDEO_REDIS_DATABASE:6}
+  mvc:
+    async:
+      request-timeout: 95000
+mybatis:
+  mapper-locations: classpath*:mapper/**/*.xml
+  configuration:
+    map-underscore-to-camel-case: true
+pagehelper:
+  helper-dialect: postgresql
+  reasonable: true
+sip:
+  ip: ${VIDEO_SIP_IP}
+  show-ip: ${VIDEO_SIP_ADVERTISED_IP:${VIDEO_SIP_IP}}
+  port: ${VIDEO_SIP_PORT:8116}
+  domain: ${VIDEO_SIP_DOMAIN:4101050000}
+  id: ${VIDEO_SIP_ID:41010500002000000002}
+  password: ${VIDEO_SIP_PASSWORD}
+  alarm: false
+media:
+  id: ${VIDEO_MEDIA_ID:city_video_zlm}
+  ip: ${VIDEO_MEDIA_HOST:127.0.0.1}
+  hook-ip: ${VIDEO_HOOK_IP}
+  sdp-ip: ${VIDEO_RTP_IP:${VIDEO_SIP_IP}}
+  stream-ip: ${VIDEO_STREAM_HOST}
+  http-port: ${VIDEO_MEDIA_HTTP_PORT:8094}
+  rtsp-port: ${VIDEO_MEDIA_RTSP_PORT:8556}
+  rtmp-port: ${VIDEO_MEDIA_RTMP_PORT:1937}
+  secret: ${VIDEO_MEDIA_SECRET}
+  auto-config: true
+  rtp:
+    enable: true
+    port-range: ${VIDEO_RTP_PORT_RANGE:31000,31500}
+    send-port-range: ${VIDEO_RTP_SEND_PORT_RANGE:31501,32000}
+user-settings:
+  server-id: ${VIDEO_SERVER_ID:city-video-01}
+  play-timeout: 20000
+  interface-authentication: true
+  sync-channel-on-device-online: true
+  record-sip: false
+  record-push-live: false
+  stream-on-demand: true
+  sip-log: false
+video:
+  internal-token: ${VIDEO_INTERNAL_TOKEN}
+  hook-allowed-addresses: ${VIDEO_HOOK_ALLOWED_ADDRESSES}
+version:
+  version: 1.0.0
+  artifactId: video-service
+  description: 城市生命线国标视频服务

+ 37 - 0
video-service/src/main/resources/bootstrap.yaml

@@ -0,0 +1,37 @@
+# 与其他采集服务一致,支持 dev / druid / prod。命名空间与凭据由部署环境提供。
+spring:
+  application:
+    name: video-service
+  profiles:
+    active: ${SPRING_PROFILES_ACTIVE:dev}
+  main:
+    allow-circular-references: true
+  cloud:
+    nacos:
+      server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
+      username: ${NACOS_USERNAME:}
+      password: ${NACOS_PASSWORD:}
+      config:
+        enabled: ${VIDEO_NACOS_ENABLED:true}
+        server-addr: ${spring.cloud.nacos.server-addr}
+        namespace: ${NACOS_NAMESPACE:}
+        group: ${NACOS_GROUP:DEFAULT_GROUP}
+        file-extension: yaml
+        # SIP/RTP/HTTP 端口和认证配置在启动时绑定,发布后重启服务使其一致生效。
+        refresh-enabled: false
+        shared-configs:
+          - data-id: zksy-shared-log.yaml
+            group: ${NACOS_GROUP:DEFAULT_GROUP}
+            refresh: false
+      discovery:
+        enabled: ${VIDEO_NACOS_DISCOVERY_ENABLED:${VIDEO_NACOS_ENABLED:true}}
+        server-addr: ${spring.cloud.nacos.server-addr}
+        namespace: ${NACOS_NAMESPACE:}
+        group: ${NACOS_GROUP:DEFAULT_GROUP}
+---
+spring:
+  config:
+    activate:
+      on-profile: dev
+    # 本机私有文件仅供 dev;在 bootstrap 阶段读取,使 Nacos 连接配置也能从文件提供。
+    import: "optional:file:./video-service/.runtime/video-local.properties,optional:file:./.runtime/video-local.properties"