Selaa lähdekoodia

fix 规则引擎集群状态变化

18339543638 3 vuotta sitten
vanhempi
commit
a213098a43

+ 5 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/controller/MediaDeviceController.java

@@ -82,6 +82,11 @@ public class MediaDeviceController implements ReactiveServiceCrudController<Medi
                 .switchIfEmpty(Mono.error(new BusinessException("设备已注销")))
                 //获取设备相连的媒体流服务器信息
                 .flatMap(playService::getNewMediaServerItem)
+                .doOnNext(mediaServerItem -> {
+                    if(!mediaServerItem.isConnect()){
+                        throw new BusinessException("zlm媒体服务器尚未连接成功");
+                    }
+                })
                 .switchIfEmpty(Mono.error(new BusinessException("未找到可用的zlm媒体服务器")))
                 .flatMapMany(mediaServerItem -> {
                     String key = SubscribeKeyGenerate.getSubscribeKey(DeferredResultHolder.CALLBACK_CMD_PLAY,deviceId,channelId);

+ 0 - 240
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/controller/PlayController.java

@@ -1,240 +0,0 @@
-package org.jetlinks.community.media.controller;
-
-
-import cn.hutool.core.lang.UUID;
-import cn.hutool.json.JSONObject;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.hswebframework.web.authorization.annotation.Authorize;
-import org.hswebframework.web.authorization.annotation.QueryAction;
-import org.hswebframework.web.authorization.annotation.Resource;
-import org.hswebframework.web.exception.BusinessException;
-import org.jetlinks.community.media.bean.StreamInfo;
-import org.jetlinks.community.media.gb28181.result.PlayResult;
-import org.jetlinks.community.media.service.LocalMediaDeviceChannelService;
-import org.jetlinks.community.media.service.LocalMediaDeviceService;
-import org.jetlinks.community.media.service.LocalMediaServerItemService;
-import org.jetlinks.community.media.service.LocalPlayService;
-import org.jetlinks.community.media.storage.impl.RedisCacheStorageImpl;
-import org.jetlinks.community.media.transmit.callback.DeferredResultHolder;
-import org.jetlinks.community.media.transmit.callback.RequestMessage;
-import org.jetlinks.community.media.transmit.cmd.SipCommander;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.context.request.async.DeferredResult;
-import reactor.core.publisher.Mono;
-import reactor.util.function.Tuple2;
-
-
-@RestController
-@RequestMapping("/media/play")
-@Slf4j
-@Authorize(ignore = true)
-@Resource(id="gb28181-play",name = "国标设备点播")
-@AllArgsConstructor
-@Tag(name = "GB媒体设备操作")
-public class PlayController{
-
-    private final SipCommander cmder;
-
-    private final RedisCacheStorageImpl redisCacheStorage;
-
-    private final LocalMediaDeviceChannelService deviceChannelService;
-
-    private final LocalMediaDeviceService mediaDeviceService;
-
-    private final LocalPlayService playService;
-
-    private final DeferredResultHolder resultHolder;
-
-    private final LocalMediaServerItemService mediaServerItemService;
-
-
-
-
-//
-//	/**
-//	 * 将不是h264的视频通过ffmpeg 转码为h264 + aac
-//	 * @param streamId 流ID
-//	 * @return
-//	 */
-//	@ApiOperation("将不是h264的视频通过ffmpeg 转码为h264 + aac")
-//	@ApiImplicitParams({
-//			@ApiImplicitParam(name = "streamId", value = "视频流ID", dataTypeClass = String.class),
-//	})
-//	@PostMapping("/convert/{streamId}")
-//	public ResponseEntity<String> playConvert(@PathVariable String streamId) {
-//		StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId);
-//		if (streamInfo == null) {
-//			streamInfo = redisCatchStorage.queryPlaybackByStreamId(streamId);
-//		}
-//		if (streamInfo == null) {
-//			logger.warn("视频转码API调用失败!, 视频流已经停止!");
-//			return new ResponseEntity<String>("未找到视频流信息, 视频流可能已经停止", HttpStatus.OK);
-//		}
-//		MediaServerItem mediaInfo = mediaServerService.getOneByServerId(streamInfo.getMediaServerId());
-//		JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaInfo, streamId);
-//		if (!rtpInfo.getBoolean("exist")) {
-//			logger.warn("视频转码API调用失败!, 视频流已停止推流!");
-//			return new ResponseEntity<String>("推流信息在流媒体中不存在, 视频流可能已停止推流", HttpStatus.OK);
-//		} else {
-//			String dstUrl = String.format("rtmp://%s:%s/convert/%s", "127.0.0.1", mediaInfo.getRtmpPort(),
-//					streamId );
-//			String srcUrl = String.format("rtsp://%s:%s/rtp/%s", "127.0.0.1", mediaInfo.getRtspPort(), streamId);
-//			JSONObject jsonObject = zlmresTfulUtils.addFFmpegSource(mediaInfo, srcUrl, dstUrl, "1000000", true, false, null);
-//			logger.info(jsonObject.toJSONString());
-//			JSONObject result = new JSONObject();
-//			if (jsonObject != null && jsonObject.getInteger("code") == 0) {
-//				   result.put("code", 0);
-//				JSONObject data = jsonObject.getJSONObject("data");
-//				if (data != null) {
-//				   	result.put("key", data.getString("key"));
-//					StreamInfo streamInfoResult = mediaService.getStreamInfoByAppAndStreamWithCheck("convert", streamId, mediaInfo.getId());
-//					result.put("data", streamInfoResult);
-//				}
-//			}else {
-//				result.put("code", 1);
-//				result.put("msg", "cover fail");
-//			}
-//			return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
-//		}
-//	}
-//
-//	/**
-//	 * 结束转码
-//	 * @param key
-//	 * @return
-//	 */
-//	@ApiOperation("结束转码")
-//	@ApiImplicitParams({
-//			@ApiImplicitParam(name = "key", value = "视频流key", dataTypeClass = String.class),
-//	})
-//	@PostMapping("/convertStop/{key}")
-//	public ResponseEntity<String> playConvertStop(@PathVariable String key, String mediaServerId) {
-//		JSONObject result = new JSONObject();
-//		if (mediaServerId == null) {
-//			result.put("code", 400);
-//			result.put("msg", "mediaServerId is null");
-//			return new ResponseEntity<String>( result.toJSONString(), HttpStatus.BAD_REQUEST);
-//		}
-//		MediaServerItem mediaInfo = mediaServerService.getOneByServerId(mediaServerId);
-//		if (mediaInfo == null) {
-//			result.put("code", 0);
-//			result.put("msg", "使用的流媒体已经停止运行");
-//			return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
-//		}else {
-//			JSONObject jsonObject = zlmresTfulUtils.delFFmpegSource(mediaInfo, key);
-//			logger.info(jsonObject.toJSONString());
-//			if (jsonObject != null && jsonObject.getInteger("code") == 0) {
-//				result.put("code", 0);
-//				JSONObject data = jsonObject.getJSONObject("data");
-//				if (data != null && data.getBoolean("flag")) {
-//					result.put("code", "0");
-//					result.put("msg", "success");
-//				}else {
-//
-//				}
-//			}else {
-//				result.put("code", 1);
-//				result.put("msg", "delFFmpegSource fail");
-//			}
-//			return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
-//		}
-//
-//
-//	}
-//
-//	@ApiOperation("语音广播命令")
-//	@ApiImplicitParams({
-//			@ApiImplicitParam(name = "deviceId", value = "设备Id", dataTypeClass = String.class),
-//	})
-//    @GetMapping("/broadcast/{deviceId}")
-//    @PostMapping("/broadcast/{deviceId}")
-//    public DeferredResult<ResponseEntity<String>> broadcastApi(@PathVariable String deviceId) {
-//        if (logger.isDebugEnabled()) {
-//            logger.debug("语音广播API调用");
-//        }
-//        Device device = storager.queryVideoDevice(deviceId);
-//		DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(3 * 1000L);
-//		String key  = DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId;
-//		if (resultHolder.exist(key, null)) {
-//			result.setResult(new ResponseEntity<>("设备使用中", HttpStatus.OK));
-//			return result;
-//		}
-//		String uuid  = UUID.randomUUID().toString();
-//        if (device == null) {
-//
-//			resultHolder.put(key, key,  result);
-//			RequestMessage msg = new RequestMessage();
-//			msg.setKey(key);
-//			msg.setId(uuid);
-//			JSONObject json = new JSONObject();
-//			json.put("DeviceID", deviceId);
-//			json.put("CmdType", "Broadcast");
-//			json.put("Result", "Failed");
-//			json.put("Description", "Device 不存在");
-//			msg.setData(json);
-//			resultHolder.invokeResult(msg);
-//			return result;
-//		}
-//		cmder.audioBroadcastCmd(device, (event) -> {
-//			RequestMessage msg = new RequestMessage();
-//			msg.setKey(key);
-//			msg.setId(uuid);
-//			JSONObject json = new JSONObject();
-//			json.put("DeviceID", deviceId);
-//			json.put("CmdType", "Broadcast");
-//			json.put("Result", "Failed");
-//			json.put("Description", String.format("语音广播操作失败,错误码: %s, %s", event.statusCode, event.msg));
-//			msg.setData(json);
-//			resultHolder.invokeResult(msg);
-//		});
-//
-//		result.onTimeout(() -> {
-//			logger.warn(String.format("语音广播操作超时, 设备未返回应答指令"));
-//			RequestMessage msg = new RequestMessage();
-//			msg.setKey(key);
-//			msg.setId(uuid);
-//			JSONObject json = new JSONObject();
-//			json.put("DeviceID", deviceId);
-//			json.put("CmdType", "Broadcast");
-//			json.put("Result", "Failed");
-//			json.put("Error", "Timeout. Device did not response to broadcast command.");
-//			msg.setData(json);
-//			resultHolder.invokeResult(msg);
-//		});
-//		resultHolder.put(key, uuid, result);
-//		return result;
-//	}
-//
-//	@ApiOperation("获取所有的ssrc")
-//	@GetMapping("/ssrc")
-//	public WVPResult<JSONObject> getSSRC() {
-//		if (logger.isDebugEnabled()) {
-//			logger.debug("获取所有的ssrc");
-//		}
-//		JSONArray objects = new JSONArray();
-//		List<SsrcTransaction> allSsrc = streamSession.getAllSsrc();
-//		for (SsrcTransaction transaction : allSsrc) {
-//			JSONObject jsonObject = new JSONObject();
-//			jsonObject.put("deviceId", transaction.getDeviceId());
-//			jsonObject.put("channelId", transaction.getChannelId());
-//			jsonObject.put("ssrc", transaction.getSsrc());
-//			jsonObject.put("streamId", transaction.getStreamId());
-//			objects.add(jsonObject);
-//		}
-//
-//		WVPResult<JSONObject> result = new WVPResult<>();
-//		result.setCode(0);
-//		result.setMsg("success");
-//		JSONObject jsonObject = new JSONObject();
-//		jsonObject.put("data", objects);
-//		jsonObject.put("count", objects.size());
-//		result.setData(jsonObject);
-//		return result;
-//	}
-//
-}
-

+ 0 - 1
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/MediaDevice.java

@@ -127,7 +127,6 @@ public class MediaDevice extends GenericEntity<String> implements RecordCreation
     )
 	private DeviceState state;
 
-
 	/**
 	 * 注册时间
 	 */

+ 2 - 29
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalMediaServerItemService.java

@@ -2,7 +2,6 @@ package org.jetlinks.community.media.service;
 
 
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSON;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
@@ -71,33 +70,6 @@ public class LocalMediaServerItemService extends GenericReactiveCrudService<Medi
         this.streamSession=streamSession;
     }
 
-    //    @Autowired
-//    private final UserSetup userSetup;
-//
-
-//
-//    @Autowired
-//    private  final MediaServerMapper mediaServerMapper;
-//
-//    @Autowired
-//    private  final VideoStreamSessionManager streamSession;
-//
-//
-//    @Autowired
-//    private final  RedisUtil redisUtil;
-//
-//    @Autowired
-//    private  final IVideoManagerStorager storager;
-//
-//    @Autowired
-//    private final  IStreamProxyService streamProxyService;
-//
-//    @Autowired
-//    private  final EventPublisher publisher;
-//
-//    @Autowired
-//    JedisUtil jedisUtil;
-
     /**
      * 初始化媒体流服务器信息,将所有信息放入缓存中
      */
@@ -329,7 +301,7 @@ public class LocalMediaServerItemService extends GenericReactiveCrudService<Medi
 //                mediaServerMapper.add(mediaServerItem);
 //                zlmServerOnline(zlmServerConfig);
 //                result.setCode(0);
-//                result.setMsg("success");
+//                result.setMsg("connect");
 //            }else {
 //                result.setCode(-1);
 //                result.setMsg("连接失败");
@@ -386,6 +358,7 @@ public class LocalMediaServerItemService extends GenericReactiveCrudService<Medi
             .doOnNext(this::refreshServerId)
             //更新zlm服务器信息
             .doOnNext(serverItem->{
+                serverItem.setConnect(true);
                 if (StrUtil.isEmpty(serverItem.getServerId())) {
                     serverItem.setServerId(zlmServerConfig.getGeneralMediaServerId());
                     this.createUpdate()

+ 0 - 4
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalPlayService.java

@@ -241,10 +241,6 @@ public class LocalPlayService  {
             return Mono.empty();
         }
         return mediaServerItemService.getDefaultMediaServer();
-//        return Mono.justOrEmpty(device.getMediaServerId())
-//            .flatMap(mediaServerItemService::findById)
-//            //找不到设备相应的媒体流服务器则根据负载均衡获取相应的媒体流服务器
-//            .switchIfEmpty(mediaServerItemService.getMediaServerForMinimumLoad());
     }
 
     private Mono<StreamInfo> onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId) {

+ 11 - 3
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipContext.java

@@ -40,9 +40,9 @@ public class SipContext {
      */
     public static void updateSipServerConfig(SipGateway config,boolean reboot) throws TransportNotSupportedException, InvalidArgumentException {
         SipStack sipStack = tuple3Map._2();
-        SipProviderImpl provider = (SipProviderImpl) tuple3Map._3();
+        SipProvider provider =getSipProvider();
         if(provider!=null&&reboot){
-            provider.removeListeningPoints();
+            ((SipProviderImpl)provider).removeListeningPoints();
         }
         if(sipStack!=null&&reboot){
             sipStack.stop();
@@ -70,7 +70,15 @@ public class SipContext {
     }
 
     public static SipProvider getSipProvider(){
-        return tuple3Map._3().values().stream().findFirst().get();
+        Map<String, SipProvider> stringSipProviderMap = tuple3Map._3();
+        if(stringSipProviderMap==null){
+            return null;
+        }
+        Collection<SipProvider> values = stringSipProviderMap.values();
+        if(values.isEmpty()){
+            return null;
+        }
+        return values.stream().findFirst().get();
     }
 
 

+ 9 - 9
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMHttpHookListener.java

@@ -86,7 +86,7 @@ public class ZLMHttpHookListener {
         }
         return Mono.just(ResponseEntity.ok(new JSONObject()
             .putOpt("code", 0)
-            .putOpt("msg", "success").toString()));
+            .putOpt("msg", "connect").toString()));
     }
 
 //	/**
@@ -103,7 +103,7 @@ public class ZLMHttpHookListener {
 //		String mediaServerId = json.getString("mediaServerId");
 //		JSONObject ret = new JSONObject();
 //		ret.put("code", 0);
-//		ret.put("msg", "success");
+//		ret.put("msg", "connect");
 //		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
 //	}
 //
@@ -149,7 +149,7 @@ public class ZLMHttpHookListener {
         }
         JSONObject ret = new JSONObject()
             .putOpt("code", 0)
-            .putOpt("msg", "success");
+            .putOpt("msg", "connect");
         return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
     }
 
@@ -164,7 +164,7 @@ public class ZLMHttpHookListener {
         log.debug("[ ZLM HOOK ]on_publish API调用,参数:" + json.toString());
         JSONObject ret = new JSONObject()
             .putOpt("code", 0)
-            .putOpt("msg", "success")
+            .putOpt("msg", "connect")
             .putOpt("enableHls", true)
             .putOpt("enableMP4", userSetup.isRecordPushLive());
 //        String mediaServerId = json.getStr("mediaServerId");
@@ -206,7 +206,7 @@ public class ZLMHttpHookListener {
 //		String mediaServerId = json.getString("mediaServerId");
 //		JSONObject ret = new JSONObject();
 //		ret.put("code", 0);
-//		ret.put("msg", "success");
+//		ret.put("msg", "connect");
 //		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
 //	}
 
@@ -274,7 +274,7 @@ public class ZLMHttpHookListener {
 //
 //		JSONObject ret = new JSONObject();
 //		ret.put("code", 0);
-//		ret.put("msg", "success");
+//		ret.put("msg", "connect");
 //		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
 //	}
 
@@ -413,7 +413,7 @@ public class ZLMHttpHookListener {
         return    result
             .then(Mono.just(ResponseEntity.ok( new JSONObject()
                 .putOpt("code", 0)
-                .putOpt("msg", "success").toString())));
+                .putOpt("msg", "connect").toString())));
     }
 
     /**
@@ -523,7 +523,7 @@ public class ZLMHttpHookListener {
             })
             .thenReturn(ResponseEntity.ok( new JSONObject()
                 .putOpt("code", 0)
-                .putOpt("msg", "success").toString()));
+                .putOpt("msg", "connect").toString()));
     }
 
     /**
@@ -543,7 +543,7 @@ public class ZLMHttpHookListener {
         }
         return Mono.just(ResponseEntity.ok(new JSONObject()
             .putOpt("code", 0)
-            .putOpt("msg", "success").toString()));
+            .putOpt("msg", "connect").toString()));
     }
 
     public Flux<JSONObject> handleKeepAlive(){

+ 5 - 19
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRunner.java

@@ -10,7 +10,6 @@ import org.jetlinks.community.media.service.LocalMediaServerItemService;
 import org.jetlinks.community.media.zlm.entity.MediaServerItem;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 import reactor.core.publisher.Mono;
@@ -40,26 +39,9 @@ public class ZLMRunner implements CommandLineRunner {
 
         //获取默认服务器配置
         mediaServerItemService.getDefaultMediaServer()
-            //添加默认媒体服务器配置
-//            .switchIfEmpty(mediaServerItemService.save(mediaConfig.getMediaSerItem()).thenReturn(mediaConfig.getMediaSerItem()))
             //清除在线服务器信息
             .mergeWith(mediaServerItemService.clearMediaServerForOnline().then(Mono.empty()))
             .mergeWith(Mono.delay(Duration.ofSeconds(20)).flatMap(ignore-> timeoutHandle()).then(Mono.empty()))
-            //更新默认服务器配置
-//            .flatMap(defaultMedia->{
-//                //判断默认服务器是否发生变动
-//                MediaServerItem mediaSerItem = mediaConfig.getMediaSerItem();
-//                if(!mediaSerItem.equals(defaultMedia)){
-//                    mediaSerItem.setDefaultServer(true);
-//                    return mediaServerItemService.deleteById(defaultMedia.getId())
-//                        .concatWith(
-//                            mediaServerItemService.save(mediaSerItem).
-//                                then(Mono.empty()))
-//                        .then(Mono.just(mediaSerItem));
-//                }
-//                return Mono.just(defaultMedia);
-//            })
-
             //订阅 zlm启动事件
             .doOnNext(ignore->subscribeOnServerStarted())
             //订阅 zlm保活事件
@@ -87,8 +69,12 @@ public class ZLMRunner implements CommandLineRunner {
                 log.error("[ {} ]]主动连接失败,不再主动连接", id);
                 if (!Boolean.TRUE.equals(startGetMedia.get(id))){
                     startGetMedia.remove(id);
-
                     //  TODO 清理数据库中与redis不匹配的zlm,TODO 并对重启前使用此在zlm的通道发送bye
+                    mediaServerItemService.createUpdate()
+                        .where(MediaServerItem::getId,id)
+                        .set(MediaServerItem::isConnect,false)
+                        .execute()
+                        .subscribe();
                 };
             }
 

+ 5 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/entity/MediaServerItem.java

@@ -145,6 +145,11 @@ public class MediaServerItem extends GenericEntity<String> {
     @Column(name = "current_port")
     private int currentPort;
 
+    @Column(name = "connect")
+    @Schema(
+        description = "是否连接成功"
+    )
+    private boolean connect;
 
     /**
      * 每一台ZLM都有一套独立的SSRC列表

+ 19 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java

@@ -1,6 +1,7 @@
 package org.jetlinks.community.rule.engine.service;
 
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.ezorm.core.param.QueryParam;
 import org.hswebframework.web.api.crud.entity.PagerResult;
@@ -146,4 +147,22 @@ public class RuleInstanceService extends GenericReactiveCrudService<RuleInstance
             .subscribe();
     }
 
+    public Mono<Void> update(RuleInstanceEntity instance) {
+        return this.findById(instance.getId())
+            .flatMap(oldInstance->{
+                if(ObjectUtil.equal(instance.getModelMeta(),oldInstance.getModelMeta())){
+                    return this.update(instance);
+                }else {
+                    instance.setState(oldInstance.getState());
+                    RuleModel model = instance.toRule(modelParser);
+                    return Flux.fromIterable(new ScheduleJobCompiler(instance.getId(), model).compile())
+                        .flatMap(scheduler::schedule)
+                        .collectList()
+                        .flatMapIterable(Function.identity())
+                        .filter(ignore->instance.getState()==RuleInstanceState.started)
+                        .flatMap(Task::start)
+                        .then();
+                }
+            });
+    }
 }

+ 8 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java

@@ -26,6 +26,7 @@ import org.jetlinks.rule.engine.api.RuleEngine;
 import org.jetlinks.rule.engine.api.model.RuleEngineModelParser;
 import org.jetlinks.rule.engine.api.task.Task;
 import org.jetlinks.rule.engine.api.task.TaskSnapshot;
+import org.jetlinks.rule.engine.cluster.RuleInstance;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
@@ -102,6 +103,13 @@ public class RuleInstanceController implements ReactiveServiceCrudController<Rul
             .thenReturn(true);
     }
 
+    @PatchMapping("_update")
+    @ResourceAction(id = "update", name = "更新规则")
+    @Operation(summary = "更新规则")
+    public Mono<Boolean> update(@RequestBody RuleInstanceEntity instance) {
+        return instanceService.update(instance)
+            .thenReturn(true);
+    }
 
     @GetMapping("/{id}/logs")
     @QueryAction